专栏首页码上积木从自定义时钟⏰了解draw流程

从自定义时钟⏰了解draw流程

前言

今天继续说绘制三部曲之最后一曲——draw

从performDraw

同样,draw流程还是开始于ViewRootImplperformDraw方法:

//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方法,不同的刻度可以通过rotate旋转画布的坐标系来实现。

        //刻度长度
        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()
        }
    }

效果图:

感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—码上积木❤️ 每日一个知识点,建立完整体系架构。

本文分享自微信公众号 - 码上积木(Lzjimu),作者:积木zz

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • LeetCode通关:数组十七连,真是不简单

    数组基本上是我们最熟悉的数据结构了,刚会写“Hello World”不久,接着就是“杨辉三角”之类的练习。

    三分恶
  • 复工没效率?用Python做个番茄工作时钟吧!

    说到番茄工作法,其实就是一种简单的时间管理方法。具体来说,首先需要明确一个工作任务,设定好番茄时间(一般是25分钟),在这个时间内专注工作,中途不允许做任何与该...

    大数据文摘
  • ❤️使用 HTML、CSS 和 JavaScript 的简单模拟时钟❤️

    正如你在上图中所看到的,这里我借助 HTML、CSS 和 JavaScript 制作了一个简单的模拟时钟。早些时候我制作了更多类型的模拟和数字手表。如果你愿意,...

    海拥
  • Android硬件加速介绍与实现

    概述 在手机客户端尤其是Android应用的开发过程中,我们经常会接触到“硬件加速”这个词。由于操作系统对底层软硬件封装非常完善,上层软件开发者往往对硬件加速的...

    xiangzhihong
  • Android硬件加速介绍与实现

    概述 在手机客户端尤其是Android应用的开发过程中,我们经常会接触到“硬件加速”这个词。由于操作系统对底层软硬件封装非常完善,上层软件开发者往往对硬件加速的...

    xiangzhihong
  • Android硬件加速原理与实现简介

    在手机客户端尤其是Android应用的开发过程中,我们经常会接触到“硬件加速”这个词。由于操作系统对底层软硬件封装非常完善,上层软件开发者往往对硬件加速的底层原...

    美团技术团队
  • 你为什么要加入X-MAN?| X加速计划(第七期)正式招募

    历经6期,经近150位同为技术背景出身的X-MAN共同打磨,我们已经形成了一套适合于技术背景出身的创始人们快速提升的课程体系,且每堂课的好评度都在9分以上。其中...

    镁客网
  • Android:手把手带你清晰梳理自定义View的工作全流程!

    了解自定义View流程前,需了解一定的自定义View基础,具体请看文章:(1)自定义View基础 - 最易懂的自定义View原理系列

    Carson.Ho
  • View的绘制-draw流程详解

    根据 measure 测量出的宽高,layout 布局的位置,渲染整个 View 树,将界面呈现出来。

    用户5546570
  • 自定义View Draw过程- 最易懂的自定义View原理系列(4)

    类似measure过程、layout过程,draw过程根据View的类型分为2种情况:

    Carson.Ho
  • 【效果高能】你不知道的 Animation 动画技巧

    在大多数需求中,css3 的 transition / animation 都能满足我们的需求,并且相对于 js 实现,可以大大提升我们的开发效率,降低开发成本...

    一只图雀
  • 自定义View三问—字节真题

    星期一的早上,还没从假期缓过来的你,遇到产品给的新需求,要做一个你没看过的View,是不是有点崩溃。哎,抹干眼泪,拿起自定义View开始埋头苦干吧~

    码上积木
  • View绘制流程

    1. View 树的绘图流程 当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始...

    xiangzhihong
  • Android组件View绘制流程原理分析

    如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是Dec...

    Anymarvel
  • android View层的绘制流程

    还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇文章吗?我们有分析到Activity中界面加...

    xiangzhihong
  • 自定义View(九)-View的工作原理- View的layout()和draw()

    上一节我们将View的测量流程理的差不多了,这篇我们来看下View的剩下的2大流程layout(布局)和draw(绘制)。相对测量来说,布局与绘制就简单了许多,...

    g小志
  • 16 Python 基础: 重点知识点--Pygame的基础知识梳理

    本文首发于腾讯云+社区,也可关注微信公众号【离不开的网】支持一下,就差你的关注支持了。

    小Gy
  • Android自定义控件实现带数值和动画的圆形进度条

    本文实例实现一个如下图所示的Android自定义控件,可以直观地展示某个球队在某个赛季的积分数和胜场、负场、平局数

    砸漏
  • 使用LaTeX的TikZ宏包绘制流程图

    类似于css的思想,这个其实就是先定义一下样式然后调用,调用的时候也可以修改,大多数参数也能猜(确信

    gyro永不抽风

扫码关注云+社区

领取腾讯云代金券