前言
今天继续说绘制三部曲之最后一曲——draw
。
从performDraw
同样,draw流程还是开始于ViewRootImpl
的performDraw
方法:
//ViewRootImpl.java private void performDraw() { boolean canUseAsync = draw(fullRedrawNeeded); } private boolean draw(boolean fullRedrawNeeded){ if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) { return false; } } return useAsyncReport; } private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { mView.draw(canvas); return true; }
在经过performDraw() -> draw() -> drawSoftware()
三连跳之后,会转到View类中的draw方法:
//View.java public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; // Step 1, draw the background, if needed drawBackground(canvas); // Step 2, save the canvas' layers canvas.saveUnclippedLayer.. // Step 3, draw the content onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers canvas.drawRect.. // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
在View.draw()
方法中,就开始了一系列绘制方法:
- 1、绘制背景
- 2、保存图层信息
- 3、绘制内容(onDraw)
- 4、绘制children
- 5、绘制边缘
- 6、绘制装饰
其中,第三步也就是我们自定义View必用的onDraw
方法,在该方法中,需要我们绘制View本身的内容。
到此,draw
的整个流程也就结束了,可以看到,相比于mearsure(测量)
和layout(布局)
两个流程,draw的流程相对比较简单,因为它不会和父View或者子View产生过多的联系,只需要将自己的部分进行绘画即可。
接下来,我们就重点看看这个onDraw
方法。怎么看?像上次一样,我们实现一个自定义View——时钟⏰View
自定义时钟View
构思
首先,给大家看看我们最终需要完成的效果图:
我们可以大致解析下,这个时钟包括几个部分:
- 1、外表盘
- 2、表盘刻度
- 3、中心点
- 4、时分秒三条线
大概就是这么个组成结构,为了方便,我们把很多属性都设置为固定值了,测量的部分(onMearsure)
我们也省略了,直接使用固定值来确定view的宽高。
当然,实际情况下的自定义View需要把每个参数值比如颜色、大小、宽度
等都设置为可配置的,然后写进style里面,而且对于测量方法也要进行重写,针对不同测量规格进行判断,今天我们就把重点放在onDraw
上面,这些细节下次我们再单独一节进行讲解。
构造函数
身为一个自定义View,首先还是要写构造函数,我们知道自定义View一般需要四种构造函数,在kotlin中其实有一种比较简便的写法:
class JimuClockView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : View(context, attrs, defStyleAttr, defStyleRes){ }
就是用到这个@JvmOverloads
注解。
由于kotlin中的方法参数可以设定默认值,而对于这种有默认值参数的方法利用@JvmOverloads
注解就可以自动生成多个重载方法。
绘制时钟表盘和中心点
下面就开始进行onDraw
方法里面的内容,首先就是表盘和中心点。
表盘是一个有宽度的圆,用到的方法就是Canvas.drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
其中,(cx,cy)
就是圆心点,而radius
就是圆的半径,paint
就是画笔。
而表盘和中心点都是通过drawCircle
画的圆,只不过表盘是空心圆(STROKE),中心点是实心圆(FILL)。
init { mPaint = Paint() mPaint.style = Paint.Style.STROKE mPaint.isAntiAlias = true mPaint.strokeWidth = roundWidth.toFloat() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //中心点 val pointWidth = width / 2.0f //绘制表盘 mPaint.strokeWidth = roundWidth.toFloat() mPaint.style = Paint.Style.STROKE mPaint.color = Color.BLACK canvas.drawCircle(pointWidth, pointWidth, pointWidth - roundWidth, mPaint) //绘制中心点 mPaint.style = Paint.Style.FILL mPaint.color = Color.BLACK canvas.drawCircle(pointWidth, pointWidth, 30f, mPaint) }
搞定,效果图如下:
绘制表盘刻度
根据效果图可知,刻度是分为两种:
- 长刻度,代表小时,一圈12个长刻度。
- 短刻度,代表分钟,一圈60个短刻度。
对于刻度的绘画,用到的就是drawline
方法,不同的刻度可以通过rotat
e旋转画布的坐标系来实现。
//刻度长度 var lineWidth = 0f //刻度离边界高度 var startHeight = 30f canvas.save() for (i in 0 until 60) { if (i % 5 == 0) { lineWidth = 40f mPaint.strokeWidth = 4f mPaint.color = Color.BLACK } else { lineWidth = 30f mPaint.strokeWidth = 2f mPaint.color = Color.BLUE } //绘画刻度 canvas.drawLine(pointWidth, startHeight, pointWidth, startHeight+lineWidth, mPaint) //旋转画布坐标系 canvas.rotate(6f, pointWidth, pointWidth) } canvas.restore()
大概逻辑就是通过循环画出短刻度,再每隔5单位画一次长刻度,还需要注意的一点是针对坐标系做的一些改变,需要在完成这部分绘制之后对画布的坐标系进行恢复。
比如上述的canvas.rotate
方法,在这之前需要调用save
保存画布的原始状态,最后在调用restore
方法恢复画布,完整调用链如下:
canvas.save() //... canvas.rotate() / canvas.translate() //... canvas.restore()
最后运行看看效果:
绘制时分秒针
最后就是画出时分秒三种针。
根据效果图可以得知三种针的一些需要注意的点:
- 1、时分秒的长度是从短到长顺序,宽度是从粗到细顺序。
- 2、每个针从中心点指向对应时间点。
- 3、针并不是纯粹的线,而是圆角矩形,所以我们可以通过drawRoundRect方法来实现这个针的绘制。
- 4、和刻度一样,还是通过旋转画布的坐标系来完成绘制。
- 5、由于中心点要压住时分秒针,所以中心点的绘制移到最后。
然后就是获取对应时间点,我们可以通过Calendar
类来获取,要注意的我们要获取的不是具体的时分秒,而是在圆盘中的角度,所以:
- 时针指向的点,对应的角度应该是
(小时+分钟/60)/12 * 360
,例如10:30,对应的角度就是 10.5/12 * 360= 315度。
分针、秒针以此类比。
calendar = Calendar.getInstance() val hour = calendar.get(Calendar.HOUR) val minute = calendar.get(Calendar.MINUTE) val second = calendar.get(Calendar.SECOND) angleHour = hour + minute / 60f angleMinute = minute + second / 60f angleSecond = second mPaint.style = Paint.Style.FILL //绘制时针 canvas.save() canvas.rotate(angleHour / 12.0f * 360.0f, pointWidth, pointWidth) val mHourWidth = 20f val rectHour = RectF( pointWidth - mHourWidth / 2, pointWidth * 0.5f, pointWidth + mHourWidth / 2, pointWidth ) mPaint.color = Color.BLACK canvas.drawRoundRect(rectHour, pointWidth, pointWidth, mPaint) canvas.restore() //绘制分针 canvas.save() canvas.rotate(angleMinute / 60.0f * 360.0f, pointWidth, pointWidth) val mMinuteWidth = 15f val rectMinute = RectF( pointWidth - mMinuteWidth / 2, pointWidth * 0.4f, pointWidth + mMinuteWidth / 2, pointWidth ) mPaint.color = Color.BLACK canvas.drawRoundRect(rectMinute, pointWidth, pointWidth, mPaint) canvas.restore() //绘制秒针 canvas.save() canvas.rotate(angleSecond / 60.0f * 360.0f, pointWidth, pointWidth) val mSecondWidth = 10f val rectSecond = RectF( pointWidth - mSecondWidth / 2, pointWidth * 0.2f, pointWidth + mSecondWidth / 2, pointWidth ) mPaint.color = Color.RED canvas.drawRoundRect(rectSecond, pointWidth, pointWidth, mPaint) canvas.restore() //绘制中心点 mPaint.style = Paint.Style.FILL mPaint.color = Color.BLACK canvas.drawCircle(pointWidth, pointWidth, 30f, mPaint)
效果图:
让⏰动起来~
最后,就是让它动起来,开启一个定时器,每隔一秒重新绘制即可。
init { timerHandler = TimerHandler(this) } /** * 定时器 */ class TimerHandler(clockView: JimuClockView) : Handler() { private val clockViewWeakReference: WeakReference<JimuClockView> = WeakReference(clockView) override fun handleMessage(msg: Message) { when (msg.what) { 0 -> { val view = clockViewWeakReference.get() if (view != null) { view.getNowtime() view.invalidate() sendEmptyMessageDelayed(0, 1000) } } } } } /** * 开启定时 */ fun startTimer() { timerHandler.removeMessages(0) timerHandler.sendEmptyMessage(0) } /** * 关闭定时 */ private fun stopTimer() { timerHandler.removeMessages(0) } override fun onVisibilityChanged( changedView: View, visibility: Int ) { super.onVisibilityChanged(changedView, visibility) if (visibility == VISIBLE) { startTimer() } else { stopTimer() } }
效果图: