前言
上班摸鱼的某个上午,一铁哥们突然发了个链接给我,说这个效果顶不顶、啥水平,效果如下:
git效果图看起来是有点点卡的,实际效果还是很好看的!!!
目前已实现并且上传到MavenCentral,详细用法可以去项目README里面看,项目地址:
觉得可以的给个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!!!
遇到的问题
动画时间和我们设置的不一样?
在使用过程中,会看到某些时候,动画速度突然变快,我觉得很奇怪,我们不是设置的好好的吗?为啥会这样?
从效果中可以看出,几乎没有了动画效果,这是为啥呢?
经过我一顿分析,原来是在启动动画的时机存在问题
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)
}
至此,目前就没发现类似的问题了。