记一次有趣的时钟效果复刻实现过程

0 阅读21分钟

前言

上班摸鱼的某个上午,一铁哥们突然发了个链接给我,说这个效果顶不顶、啥水平,效果如下:

git效果图看起来是有点点卡的,实际效果还是很好看的!!!

目前已实现并且上传到MavenCentral,详细用法可以去项目README里面看,项目地址:

github.com/fastcv-cc/L…

觉得可以的给个Star!!!

本着学习的目的(主要是他问我能不能做出来这样的效果,都问了肯定是能做的!!),我开始思考这玩意怎么去实现?

思路

如何将整个效果拆分出来?

首先,我们根据效果来看,我想到的第一个思路就是:

将整个内容分成了很多个小圆,然后每个小圆根据自己的职责显示不同的内容,小圆的类型分为以下几种:

  • 基础圆,带个白色圆心点
  • 数字部分的圆,负责数字的线性拼接效果
  • 分隔圆,带个黑色圆心点

然后根据View的宽高,将整个View区域划分出14 * 3个小圆,然后去控制不同的圆,在不同的时间点显示对应的效果,但是,延伸思考之后,发现了以下这些问题:

  • 我得去仔细的找每个数字变化时,线条的变化过程的规律,然后根据时间、数字的变化,去控制每个小圆的显示效果,这样想想都头疼。
  • 得仔细的编码,精确的控制小圆坐标,这也太麻烦了。

基于这个想法的基础上,我衍生了第二个想法,画区域!!!

既然我们是做数字时钟,那整块区域就被分为了以下几个部分:

我们只需要将整个区域的小圆,划分成这几个区域既可,按照这个图,我们的区域大体分为两种类型:

  • 时间类型
  • 分割线类型

然后不同区域的小圆所拥有的小圆坐标集合如下:

分割线类型有两个坐标集合:

  • [ [0,4] , [1,4] , [2,4] ]
  • [ [0,9] , [1,9] , [2,9] ]

每个分割线拥有3个小圆,所以,将大坐标系传入转化为分割线内部坐标系时,分割线就是一个长度为3的一维坐标数组了。

时间类型有三个坐标集合:

  • 小时 [ [[0-2] , [0-3]] ]
  • 分钟 [ [[0-2] , [5-8]] ]
  • 秒 [ [[0-2] , [10-13]] ]

每个时间类型拥有12个小圆,所以,将大坐标系传入转化为时间类型内部坐标系时,时间类型就是一个 4 x 3 二维坐标数组了。

效果如下:

在这个基础上面,我们可以无需关心小圆了,我们只需要关系分割线和时间类型的实现思路了。

分割线的效果如何实现?

根据UI效果:

可以看到,分割线内,除了基础小圆的UI效果外,它只是针对中间小圆,修改了下圆心点的颜色而已。

所以针对分割线的做法就是,将它持有的坐标系中[0 , 1] 这个小圆做特殊处理即可。

时间类型的效果如何实现?

在这里很容易陷入一个牛角尖,就是这个变化效果我该如何实现?

我刚开始也在这个问题上思考了一会,比如:

  • 数字之间的变化(动画)效果,我要怎么实现?写死0-1,1-2,....,9-0的过度效果?
  • 按照毫秒去显示变化? 那一毫秒刷新一次? 一秒钟1000帧?

很明显,这两个问题或者说思路,现实起来都有很大的问题,那我们不如先简单化,一步一步的走,我们先考虑时间固定显示要如何实现?这个问题又可以简化为单个数字要如何显示?

单个数字要如何显示?

我们先观察下0-9的显示效果,如下图:

我们拿数字6举例子,我们将数字占有的坐标区域分为6个部分:

根据安卓角度规则,各下标部分显示线条的角度为:


0° 显示
90° 显示

180° 显示
180° 显示

90° 显示
-90° 显示

90° 显示
180° 显示

-90° 显示
0° 显示

-90° 显示
180° 显示

而数字1的各下标部分显示线条的角度为:


-90° 不显示
-90° 不显示

90° 显示
135° 显示

-90° 不显示
-90° 不显示

90° 显示
-90° 显示

-90° 不显示
-90° 不显示

-90° 不显示
-90° 显示

定义小圆基础类

从这些样式和数据可以看出,我们每个小圆最多有2条线,最少0条,而且每条线还要能控制显示隐藏。所以这里我们定义一个小圆绘制参数类:

data class CircleDrawParam(
    var line1Angle: Float,
    var line1Alpha: Int,
    var line2Angle: Float,
    var line2Alpha: Int
)

用 line1Angle 和 line1Alpha 控制线段1的角度和显示/隐藏。用 line2Angle 和 line2Alpha 控制线段2的角度和显示/隐藏。

在这个基础上面,定义出小圆绘制对象类:

//一个圆单元参数
class CircleDrawer {

    var x: Float = 0.0f
    var y: Float = 0.0f
    var radius: Float = 0.0f
    var paint: Paint? = null


    //绘制基础圆
    fun drawBaseCircle(canvas: Canvas) {
        paint?.let {
            //画圆
            //TODO 定义颜色
            it.color = Color.parseColor("#F4EBEB")
            canvas.drawCircle(x, y, radius, it)
            //画小白点
            it.color = Color.WHITE
            canvas.drawCircle(x, y, it.strokeWidth / 2, it)
        }
    }

    //绘制圆中心点
    fun drawBlackPoint(canvas: Canvas) {
        paint?.let {
            //画小黑点
            it.color = Color.BLACK
            canvas.drawCircle(x, y, it.strokeWidth / 2, it)
        }
    }

    //通过绘制参数绘制线段
    fun drawByCircleDrawParam(
        canvas: Canvas,
        param: CircleDrawParam
    ) {
        paint?.let {
            //画小黑点
            it.color = Color.BLACK
            it.alpha = param.line1Alpha
            canvas.drawCircle(x, y, it.strokeWidth / 2, it)
            //画线1
            canvas.save()
            canvas.translate(x, y)
            canvas.rotate(param.line1Angle)
            it.alpha = param.line1Alpha
            canvas.drawLine(0f, 0f, radius, 0f, it)
            canvas.restore()
            canvas.save()
            canvas.translate(x, y)
            canvas.rotate(param.line2Angle)
            it.alpha = param.line2Alpha
            canvas.drawLine(0f, 0f, radius, 0f, it)
            canvas.restore()
        }
    }

}

定义组件类

在CircleDrawer的基础上,我将分割线和时间类型定义为时钟View中的两类组件:

/**
 * 数字时钟组件类型
 * 这里格局ui效果分为两个类型:
 * - 时间组件
 * - 分割线组件
 */
enum class ComponentsType {
    COMPONENTS_TYPE_TIME, COMPONENTS_SPLIT_LINE
}
abstract class AbsComponents {

    abstract val type: ComponentsType

    abstract fun draw(canvas: Canvas)

}

分割线组件

而分割线组件类的实现

/**
 * 数字分割线 由3个圆点组成
 */
class SplitLineComponents(
    private val array: Array<CircleDrawer>,
) : AbsComponents() {

    override fun draw(canvas: Canvas) {
        for ((index, circle) in array.withIndex()) {
            circle.drawBaseCircle(canvas)
            if (index == 1) {
                circle.drawBlackPoint(canvas)
            }
        }
    }

    override val type: ComponentsType
        get() = ComponentsType.COMPONENTS_SPLIT_LINE

}

时间类型组件

而时间类型的实现就相对比较复杂了。

通过UI效果可以看出,一个时间由两个数字组成,每个数字由6个小圆组成。

定义数字类

所以这个定义数字参数类

/**
 * 数字绘制参数
 * 指代每个数据默认的绘制参数
 * 每个数字由6个圆点组成
 */
abstract class AbsNumberDrawParam {
    abstract val params: Array<CircleDrawParam>
}

数字0-9的数字参数类的实现如下:

class Number0 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(), 90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), 180f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(), -90f, (1f * 255).toInt())
    )
}

class Number1 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),135f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (1f * 255).toInt())
    )
}

class Number2 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(),180f, (1f * 255).toInt())
    )
}

class Number3 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(),-90f, (1f * 255).toInt())
    )
}

class Number4 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(90f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
    )
}

class Number5 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 180f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

class Number6 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 180f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

class Number7 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
    )
}

class Number8 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 180f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

class Number9 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

而数字绘制对象的定义如下:

/**
 * 指的是数字时钟中的一个数字单位
 * 从设计图中可以看出 一个数字单位 由6个圆点组成
 * 然后需要具体数字的绘制参数
 * @param array 传入的6个圆点信息数组
 * @param number 当前数字图形(绘制)参数
 */
class NumberProxy(
    private val array: Array<Array<CircleDrawer>>,
    private val number: AbsNumberDrawParam
) {

    //绘制数字
    fun draw(canvas: Canvas) {
        for ((index, minimalUhrCircles) in array.withIndex()) {
            for ((index1, minimalUhrCircle) in minimalUhrCircles.withIndex()) {
                minimalUhrCircle.drawBaseCircle(canvas)
                minimalUhrCircle.drawByCircleDrawParam(canvas, number.params[index * 2 + index1])
            }
        }
    }

}
定义时间组件类

而时间类的组件由两个数字组成,我将它们分为左、右两个数字对象:

class TimeComponents(private val array: Array<Array<CircleDrawer>>) :
    AbsComponents() {
    override val type: ComponentsType
        get() = ComponentsType.COMPONENTS_TYPE_TIME

    private var newLeftNumber: AbsNumberDrawParam? = null
    private var newRightNumber: AbsNumberDrawParam? = null

    fun setNumber(number: Int) {
        newLeftNumber = transformNumber(number / 10)
        newRightNumber = transformNumber(number % 10)
    }

    override fun draw(canvas: Canvas) {
        val leftNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][0], array[0][1]),
                arrayOf(array[1][0], array[1][1]),
                arrayOf(array[2][0], array[2][1]),
            ), newLeftNumber!!
        )
        leftNumber.draw(canvas)
        val rightNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][2], array[0][3]),
                arrayOf(array[1][2], array[1][3]),
                arrayOf(array[2][2], array[2][3]),
            ), newRightNumber!!
        )
        rightNumber.draw(canvas)

    }

    private fun transformNumber(i: Int): AbsNumberDrawParam? {
        return when (i) {
            0 -> Number0()
            1 -> Number1()
            2 -> Number2()
            3 -> Number3()
            4 -> Number4()
            5 -> Number5()
            6 -> Number6()
            7 -> Number7()
            8 -> Number8()
            9 -> Number9()
            else -> null
        }
    }
}

定义组件管理类

由于需要将整个区域的小圆进行初始化,并且要将初始化后的小圆数字拆分到各个组件里面,所以我们需要一个管理类来处理这些逻辑。

这里我们总共分成了5个组件:

  • 小时时间类型组件
  • 分割线1组件
  • 分钟时间类型组件
  • 分割线2组件
  • 秒时间类型组件
class ComponentsManager {

    //小圆半径
    private var radius = 0f

    //圆之间的间距
    private var circleMargin = 0f

    //原点半径
    private var pointRadius = 0f

    //宽高比
    private var aspectRatio = 0f

    //使用的宽高
    private var useWidth = 0f
    private var useHeight = 0f

    //小圆的二维数组
    private val circleArray = Array(3) {
        return@Array Array(14) {
            CircleDrawer()
        }
    }

    //画线的画笔
    private val paint = Paint().apply {
        setColor(Color.BLACK)
        style = Paint.Style.FILL
    }

    private lateinit var hourComponents: TimeComponents
    private lateinit var split1Components: SplitLineComponents
    private lateinit var minuteComponents: TimeComponents
    private lateinit var split2Components: SplitLineComponents
    private lateinit var secondComponents: TimeComponents

    fun initConfigureInfo(context: Context, attrs: AttributeSet?) {
        radius = dp2px(context, 20)
        circleMargin = dp2px(context, 4)
        pointRadius = dp2px(context, 2)
        paint.strokeWidth = circleMargin
        aspectRatio =
            (14 * radius * 2 + circleMargin * 15) * 1.0f / (3 * radius * 2 + circleMargin * 4)
    }

    fun initComponents(w: Int, h: Int) {
        val realAspectRatio = w * 1.0f / h
        if (realAspectRatio > aspectRatio) {
            //以高为基准
            useHeight = h * 1.0f
            useWidth = useHeight * aspectRatio
        } else {
            //以宽为基准
            useWidth = w * 1.0f
            useHeight = useWidth / aspectRatio
        }
        //重新计算圆半径
        radius = (useWidth - circleMargin * 15) / 14 / 2
        //以画布左上角为原点计算小圆的分布和分组
        countCirclePosition()
    }

    fun draw(canvas: Canvas) {
        val calendar: Calendar = Calendar.getInstance()
        val hour = calendar[Calendar.HOUR_OF_DAY]
        val minute = calendar[Calendar.MINUTE]
        val second = calendar[Calendar.SECOND]
        hourComponents.setNumber(hour)
        minuteComponents.setNumber(minute)
        secondComponents.setNumber(second)
        drawComponents(canvas)
    }

    private fun drawComponents(canvas: Canvas) {
        hourComponents.draw(canvas)
        split1Components.draw(canvas)
        minuteComponents.draw(canvas)
        split2Components.draw(canvas)
        secondComponents.draw(canvas)
    }

    private fun countCirclePosition() {
        //根据属性计算所有圆的位置
        initAllCircleParamArray()
        initHourComponents()
        initSplit1Components()
        initMinuteComponents()
        initSplit2Components()
        initSecondComponents()
    }

    private fun initSecondComponents() {
        secondComponents = TimeComponents(
            arrayOf(
                arrayOf(
                    circleArray[0][10], circleArray[0][11], circleArray[0][12], circleArray[0][13]
                ),
                arrayOf(
                    circleArray[1][10], circleArray[1][11], circleArray[1][12], circleArray[1][13]
                ),
                arrayOf(
                    circleArray[2][10], circleArray[2][11], circleArray[2][12], circleArray[2][13]
                ),
            )
        )
    }

    private fun initSplit2Components() {
        split2Components = SplitLineComponents(
            arrayOf(
                circleArray[0][9],
                circleArray[1][9],
                circleArray[2][9],
            )
        )
    }

    private fun initMinuteComponents() {
        minuteComponents = TimeComponents(
            arrayOf(
                arrayOf(circleArray[0][5], circleArray[0][6], circleArray[0][7], circleArray[0][8]),
                arrayOf(circleArray[1][5], circleArray[1][6], circleArray[1][7], circleArray[1][8]),
                arrayOf(circleArray[2][5], circleArray[2][6], circleArray[2][7], circleArray[2][8]),
            )
        )
    }

    private fun initSplit1Components() {
        split1Components = SplitLineComponents(
            arrayOf(
                circleArray[0][4],
                circleArray[1][4],
                circleArray[2][4],
            )
        )
    }

    private fun initHourComponents() {
        hourComponents = TimeComponents(
            arrayOf(
                arrayOf(circleArray[0][0], circleArray[0][1], circleArray[0][2], circleArray[0][3]),
                arrayOf(circleArray[1][0], circleArray[1][1], circleArray[1][2], circleArray[1][3]),
                arrayOf(circleArray[2][0], circleArray[2][1], circleArray[2][2], circleArray[2][3]),
            )
        )
    }

    private fun initAllCircleParamArray() {
        var positionY: Float
        var positionX: Float
        for (i in 0 until 3) {
            positionY = (i + 1) * circleMargin + (1 + 2 * i) * radius * 1.0f
            for (i1 in 0 until 14) {
                positionX = (i1 + 1) * circleMargin + (1 + 2 * i1) * radius * 1.0f
                circleArray[i][i1].x = positionX
                circleArray[i][i1].y = positionY
                circleArray[i][i1].radius = radius
                circleArray[i][i1].paint = paint
            }
        }
    }

    private fun dp2px(context: Context, dpValue: Int): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, dpValue.toFloat(), context.resources.displayMetrics
        )
    }
}

通过管理类,我们控制小圆半径、间距、绘制颜色等等。

定义线段数字时钟View类

而View类的定义就很简单了,因为逻辑全部交由管理类进行了。

class LineNumberClockView : View {

    constructor(context: Context?) : super(context) {
        initParams(context, null)
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        initParams(context, attrs)
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        initParams(context, attrs)
    }

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        initParams(context, attrs)
    }

    private val manager = ComponentsManager()

    private fun initParams(context: Context?, attrs: AttributeSet?) {
        if (context == null) {
            return
        }
        manager.initConfigureInfo(context, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //计算最小宽高值
        manager.initComponents(w, h)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        manager.draw(canvas)
    }
}

在Activity中显示如下:

至此,我们使用线段显示时间的功能实现了,但是,我们想要的是动起来的时钟,这个不是我们想要的,这样最多定个1秒刷一次更新时间,没有那炫酷的数字过度动画效果怎么能行。那这个效果如何实现了?

动画效果如何实现?

结合上面的实现方式,我们应该知道了,每个数字依赖于6个小圆的绘制参数进行绘制的,0-9每个数字的参数是固定的,那有没有一种不固定的数字参数,用它来绘制动态的效果?

于是我们定义出这个类TransitionNumberDrawParam,它用于保存动画过程中临时的数字绘制参数。

/**
 * 过渡数字绘制参数
 */
class TransitionNumberDrawParam(override val params: Array<CircleDrawParam>) : AbsNumberDrawParam()

那这个类怎么计算得出呢?举个例子。

我们看下数字1到2的参数对比

class Number0 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(), 90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), 180f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(), -90f, (1f * 255).toInt())
    )
}

class Number1 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),135f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (1f * 255).toInt())
    )
}

是不是好像就是4个参数的变化。

没错,我们可以直接通过进度控制两个数字对象相减,计算出过渡数字参数,我们定义个AbsNumberDrawParam的扩展方法用以计算TransitionNumberDrawParam,而这个方法内部就是相应的两个小圆的参数做减法处理。

//计算过渡数字绘制参数
fun AbsNumberDrawParam.transition(newNumber: AbsNumberDrawParam, progress: Float): AbsNumberDrawParam {
    val circleDrawParam0 = this.params[0].transition(newNumber.params[0],progress)
    val circleDrawParam1 = this.params[1].transition(newNumber.params[1],progress)
    val circleDrawParam2 = this.params[2].transition(newNumber.params[2],progress)
    val circleDrawParam3 = this.params[3].transition(newNumber.params[3],progress)
    val circleDrawParam4 = this.params[4].transition(newNumber.params[4],progress)
    val circleDrawParam5 = this.params[5].transition(newNumber.params[5],progress)
    return TransitionNumberDrawParam(
        arrayOf(
            circleDrawParam0,
            circleDrawParam1,
            circleDrawParam2,
            circleDrawParam3,
            circleDrawParam4,
            circleDrawParam5,
        )
    )
}

小圆参数类添加transition方法

package cc.fastcv.line_number_clock

/**
 * 圆绘制参数
 * angle: -180 ~ 180
 * alpha: 0 ~ 255
 */
data class CircleDrawParam(
    var line1Angle: Float,
    var line1Alpha: Int,
    var line2Angle: Float,
    var line2Alpha: Int
) {
    //参数变更差计算
    fun transition(newParam: CircleDrawParam, progress: Float): CircleDrawParam {
        val tempLine1Angle: Float = line1Angle + (newParam.line1Angle - line1Angle) * progress
        val tempLine1Alpha: Int =
            (line1Alpha + (newParam.line1Alpha - line1Alpha) * progress).toInt()
        val tempLine2Angle: Float = line2Angle + (newParam.line2Angle - line2Angle) * progress
        val tempLine2Alpha: Int =
            (line2Alpha + (newParam.line2Alpha - line2Alpha) * progress).toInt()
        return CircleDrawParam(tempLine1Angle, tempLine1Alpha, tempLine2Angle, tempLine2Alpha)
    }
}

新增了个参数计算方法得到临时的CircleDrawParam,然后组合成TransitionNumberDrawParam,最后在两个数字变换的过程中,使用TransitionNumberDrawParam来显示效果。

那得到中间过程的产物了,我们就把动画相关的逻辑在LineNumberClockView中加上。

    private var progress = 999

    private val handler = Handler(Looper.getMainLooper())

    private val showRunnable = Runnable {
        showInAnim()
        showNext()
    }

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000)
    }

    private fun showInAnim() {
        progress = 0
        val anim = ValueAnimator.ofInt(0, 999).apply {
            addUpdateListener {
                progress = it.animatedValue as Int
                invalidate()
            }
            duration = 650L
        }
        anim.start()
    }

这里我们考虑到使用了Handler重复的发送message,所以要和生命周期绑定在一起,不然可能会产生内存泄漏,于是又加上了DefaultLifecycleObserver

class LineNumberClockView : View, DefaultLifecycleObserver {

    ...
    
    fun bindLifecycle(lifecycle: Lifecycle) {
        lifecycle.addObserver(this)
    }

    ...

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        postDelayed(showRunnable, 1000L - Calendar.getInstance().get(Calendar.MILLISECOND))
    }

    override fun onStop(owner: LifecycleOwner) {
        super.onStop(owner)
        handler.removeCallbacks(showRunnable)
    }

    ...
}

最后,我们将progress传入下层去控制绘制进度。完整的LineNumberClockView如下:

class LineNumberClockView : View, DefaultLifecycleObserver {

    constructor(context: Context?) : super(context) {
        initParams(context, null)
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        initParams(context, attrs)
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        initParams(context, attrs)
    }

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        initParams(context, attrs)
    }

    private val manager = ComponentsManager()

    private fun initParams(context: Context?, attrs: AttributeSet?) {
        if (context == null) {
            return
        }
        manager.initConfigureInfo(context, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //计算最小宽高值
        manager.initComponents(w, h)
    }

    private var progress = 999

    private val handler = Handler(Looper.getMainLooper())

    private val showRunnable = Runnable {
        showInAnim()
        showNext()
    }

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000)
    }

    fun bindLifecycle(lifecycle: Lifecycle) {
        lifecycle.addObserver(this)
    }

    private fun showInAnim() {
        progress = 0
        val anim = ValueAnimator.ofInt(0, 999).apply {
            addUpdateListener {
                progress = it.animatedValue as Int
                invalidate()
            }
            duration = 650L
        }
        anim.start()
    }

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        postDelayed(showRunnable, 1000L - Calendar.getInstance().get(Calendar.MILLISECOND))
    }

    override fun onStop(owner: LifecycleOwner) {
        super.onStop(owner)
        handler.removeCallbacks(showRunnable)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        manager.draw(canvas, progress)
    }
}

与之对应的manager的draw方法也要添加参数

    fun draw(canvas: Canvas, progress: Int) {
        val calendar: Calendar = Calendar.getInstance()
        val hour = calendar[Calendar.HOUR_OF_DAY]
        val minute = calendar[Calendar.MINUTE]
        val second = calendar[Calendar.SECOND]
        val drawProgress = (progress + 1) * 1f / 1000
        hourComponents.setNumberAndProgress(hour,drawProgress)
        minuteComponents.setNumberAndProgress(minute,drawProgress)
        secondComponents.setNumberAndProgress(second,drawProgress)
        drawComponents(canvas)
    }

然后就是时间类型的组件的setNumber方法改成setNumberAndProgress

    fun setNumberAndProgress(number: Int, process: Float) {
        newLeftNumber = transformNumber(number / 10)
        newRightNumber = transformNumber(number % 10)
        drawProgress = process
    }

与之对应的绘制方法也要改动

    override fun draw(canvas: Canvas) {
        if (drawProgress == 1f) {
            lastLeftNumber = newLeftNumber
            lastRightNumber = newRightNumber
        }
        val leftTempNumber: AbsNumberDrawParam =
            lastLeftNumber!!.transition(newLeftNumber!!, drawProgress)
        val rightTempNumber: AbsNumberDrawParam =
            lastRightNumber!!.transition(newRightNumber!!, drawProgress)
        val leftNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][0], array[0][1]),
                arrayOf(array[1][0], array[1][1]),
                arrayOf(array[2][0], array[2][1]),
            ), leftTempNumber
        )
        leftNumber.draw(canvas)
        val rightNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][2], array[0][3]),
                arrayOf(array[1][2], array[1][3]),
                arrayOf(array[2][2], array[2][3]),
            ), rightTempNumber
        )
        rightNumber.draw(canvas)
    }

到这里我们的动画效果就编码完成了,运行看下效果。

完整的代码可以去仓库里面查看,地址如下,可以的话,给个Star!!!

github.com/fastcv-cc/L…

遇到的问题

动画时间和我们设置的不一样?

在使用过程中,会看到某些时候,动画速度突然变快,我觉得很奇怪,我们不是设置的好好的吗?为啥会这样?

从效果中可以看出,几乎没有了动画效果,这是为啥呢?

经过我一顿分析,原来是在启动动画的时机存在问题

    private val handler = Handler(Looper.getMainLooper())

    private val showRunnable = Runnable {
        showInAnim()
        showNext()
    }

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000)
    }

    private fun showInAnim() {
        progress = 0
        val anim = ValueAnimator.ofInt(0, 999).apply {
            addUpdateListener {
                progress = it.animatedValue as Int
                invalidate()
            }
            duration = 650L
        }
        anim.start()
    }

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        post(showRunnable)
    }

通过代码我们可以知道,在界面恢复可交互的时候,我们会去启动这个Handler的1s定时任务,然后在动画过程中通过对比上次的时间来计算动画效果。

理想情况是我在初始化的时候会设置当前秒,比如7s,然后在8s的0ms的时候启动动画,会在650ms内完成上一秒到下一秒动画过渡,然后在9s 0ms的时候启动下一次动画,依次反复。

问题就在这里,如果现在初始化的时候就是7s,然后启动动画的时机是7s 500ms的时候,此时记录的上次的秒是7,然后获取的当前的秒也是7,那么动画时长650ms中的前500ms都是7 -7 的变化,等于是没有变化。

然后最后150ms的时候,等于是进度值比较大的时候开始的 7 - 8的变化,所以动画效果很快。

然后记录下来上次的秒是8,下次启动的时间是8s 500ms,就会一直出现相同的问题。

于是我们控制下启动时间点,控制到下一秒0ms的时候启动,修改onResume方法的内容

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        postDelayed(showRunnable, 1000L - System.currentTimeMillis() % 1000L)
    }

这样修改完,就可以解决上面的问题了。

但是吧,跑一段时间后,还是出现了相同的问题。啊这?

不是解决了吗?为啥还会出现?

这里我们陷入了一个误区,就是就算在7秒0ms的时候定时下一秒执行的任务,Handler很难在8秒0ms的时候启动这个任务,多多少少会有几毫米的延迟,那么一定时间后,就会出现上面说的问题。

那么解决方案也很简单,就是尽量控制任务在8秒0ms附近启动。修改showNext方法

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000L - System.currentTimeMillis() % 1000L)
    }

至此,目前就没发现类似的问题了。