专栏名称: Java爱好者
分享android开发编程知识和相关技术应用
目录
相关文章推荐
51好读  ›  专栏  ›  Java爱好者

用Kotlin实现抖音爆红的文字时钟,征服产品小姐姐

Java爱好者  · 公众号  ·  · 2019-09-03 09:48

正文

作者:二娃_
链接:https://juejin.im/post/5cb53e93e51d456e55623b07

起源

周末在家刷抖音的时候看到了这款网红时钟,都是 Android 平台的,想来何不自己实现一把。看抖音里大家发的视频,这款时钟基本分两类,一类是展示在「壁纸」上,一类是展示在「锁屏」上。


  • 展示到「壁纸」通过 LiveWallPaper 相关 API 可以做到,这也是本专题要实现的方式。

  • 展示到「锁屏」目测是使用各 ROM 厂商的相关 API,开发锁屏主题可以做到。


然而实现两者的基础便是拿起 Canvas Paint 等把它绘制出来,所以「上篇」我先用自定义 View 的方式把时钟画出来,在 Activity 中展示效果。「下篇」的时候再把该 View 结合 LiveWallPaper 设置到壁纸。抖音爆红文字时钟项目源码( https://github.com/drawf/SourceSet/blob/master/app/src/main/java/me/erwa/sourceset/view/TextClockView.kt

思考分析


这是我当时截图下来的参考,先分析下涉及到的元素及样式表现:


  1. 「圆中信息」圆中心的数字时间 + 数字日期 + 文字星期几,始终为白色

  2. 「时圈」一圈文字小时,一点、二点.. 十二点, 当前点数为白色,其它为白色 + 透明度 ,如图中十点就是白色。

  3. 「分圈」一圈文字分钟,一分、二分.. 五十九分, 六十分显示为空 ,同理,当前分钟为白色,其它白色 + 透明度。

  4. 「秒圈」一圈文字秒,一秒、二秒.. 五十九秒, 六十秒显示为空 ,也是同理。


然后分析下动画效果:


  1. 每秒钟「秒圈」走一下,这一下的旋转角度为 360°/60=6° ,并且走这一下的时候有个 线性旋转 过去的动画效果。

  2. 每分钟「分圈」走一下,旋转角度和动画效果跟「秒圈」相同。

  3. 每小时「时圈」走一下,旋转角度为 360°/12=30° ,动画效果同上。

绘制静态图

1、画布准备


基本是将画布背景填充黑色,然后将画布的原点移动到 View 大小的中心,这样方便思维理解与绘制。


//在onLayout方法中计算View去除padding后的宽高
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mWidth = (measuredWidth - paddingLeft - paddingRight).toFloat()
mHeight = (measuredHeight - paddingTop - paddingBottom).toFloat()

//后文会涉及到
//统一用View宽度*系数来处理大小,这样可以联动适配样式
mHourR = mWidth * 0.143f
mMinuteR = mWidth * 0.35f
mSecondR = mWidth * 0.35f
}

//在onDraw方法将画布原点平移到中心位置
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) return
canvas.drawColor(Color.BLACK)//填充背景
canvas.save()
canvas.translate(mWidth / 2, mHeight / 2)//原点移动到中心

//绘制各元件,后文会涉及到
drawCenterInfo(canvas)
drawHour(canvas, mHourDeg)
drawMinute(canvas, mMinuteDeg)
drawSecond(canvas, mSecondDeg)

//从原点处向右画一条辅助线,之后要处理文字与x轴的对齐问题,稍后再说
canvas.drawLine(0f, 0f, mWidth, 0f, mHelperPaint)

canvas.restore()
}

2、画「圆中信息」


经过第一步,可以在 AS 的 Xml Preview 中看到一屏黑色 + 一条从屏幕中心到右边界的红线。(一眼望去,还是挺美的)


/**
* 绘制圆中信息
*/

private fun drawCenterInfo(canvas: Canvas) {
Calendar.getInstance().run {
//绘制数字时间
val hour = get(Calendar.HOUR_OF_DAY)
val minute = get(Calendar.MINUTE)

mPaint.textSize = mHourR * 0.4f//字体大小根据「时圈」半径来计算
mPaint.alpha = 255
mPaint.textAlign = Paint.Align.CENTER
canvas.drawText("$hour:$minute", 0f, mPaint.getBottomedY(), mPaint)

//绘制月份、星期
val month = (this.get(Calendar.MONTH) + 1).let {
if (it < 10) "0$it" else "$it"
}
val day = this.get(Calendar.DAY_OF_MONTH)
val dayOfWeek = (get(Calendar.DAY_OF_WEEK) - 1).toText()//私有的扩展方法,将Int数字转换为 一、十一、二十等,后文绘制三个文字圈都会用该方法

mPaint.textSize = mHourR * 0.16f//字体大小根据「时圈」半径来计算
mPaint.alpha = 255
mPaint.textAlign = Paint.Align.CENTER
canvas.drawText("$month.$day 星期$dayOfWeek", 0f, mPaint.getTopedY(), mPaint)
}
}

/**
* 扩展获取绘制文字时在x轴上 垂直居中的y坐标
*/

private fun Paint.getCenteredY(): Float {
return this.fontSpacing / 2 - this.fontMetrics.bottom
}

/**
* 扩展获取绘制文字时在x轴上 贴紧x轴的上边缘的y坐标
*/

private fun Paint.getBottomedY(): Float {
return -this.fontMetrics.bottom
}

/**
* 扩展获取绘制文字时在x轴上 贴近x轴的下边缘的y坐标
*/

private fun Paint.getToppedY(): Float {
return -this.fontMetrics.ascent
}


其中要说一下 mPaint.getBottomedY() mPaint.getToppedY() , 这是两个扩展到 Paint 画笔上的两个 kotlin 方法。他们的作用是为了处理绘制文字时与 x 轴的对齐关系。 canvas.drawText() 方法的第三个参数是 y 坐标 ,但这个指的是文字的 Baseline 的 y 坐标 , 所以写了工具方法来得到矫正后的 y 坐标 。(这里就只抛出这个点吧,具体实现原理可先查阅 Paint 类的相关 API 就会明白,文末会贴出我拜读的文章链接)


拿绘制数字时间举例,展示下不同效果:


mPaint.getBottomedY() 替换成 0f (y 坐标为 0,就是文字的 Baseline 坐标为 0),文字使用 15:67 abc jqk ,可以看到两者区别。(红线就是前文画的那条好美的辅助线)


canvas.drawText("15:67 测试文字 abc jqk", 0f, 0f, mPaint)

canvas.drawText("15:67 测试文字 abc jqk", 0f, mPaint.getBottomedY(), mPaint)



ok,「圆中信息」绘制后长这个样子:



3、画「时圈」「分圈」「秒圈」


绘制思路就是 for 循环 12 次,每次将画布旋转 30° 乘以 i ,然后在指定位置绘制文字,12 次后刚好一个圆圈。


该方法接收一个 degrees: Float 参数,是控制「时圈」整体的旋转的,后文就是不断改变该值,而产生动画效果的。并且因为三个圈的动画方向都是逆时针,所以这个 degrees 是个始终会是个负数。


/**
* 绘制小时
*/

private fun drawHour(canvas: Canvas, degrees: Float) {
mPaint.textSize = mHourR * 0.16f

//处理整体旋转
canvas.save()
canvas.rotate(degrees)

for (i in 0 until 12) {
canvas.save()

//从x轴开始旋转,每30°绘制一下「几点」,12次就画完了「时圈」
val iDeg = 360 / 12f * i
canvas.rotate(iDeg)

//这里处理当前时间点的透明度,因为degrees控制整体逆时针旋转
//iDeg控制绘制时顺时针,所以两者和为0时,刚好在x正半轴上,也就是起始绘制位置。
mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()
mPaint.textAlign = Paint.Align.LEFT

canvas.drawText("${(i + 1).toText()}点", mHourR, mPaint.getCenteredY(), mPaint)
canvas.restore()
}

canvas.restore()
}



同理绘制「分圈」「秒圈」

/**
* 绘制分钟
*/

private fun drawMinute(canvas: Canvas, degrees: Float) {
mPaint.textSize = mHourR * 0.16f

//处理整体旋转
canvas.save()
canvas.rotate(degrees)

for (i in 0 until 60) {
canvas.save()

val iDeg = 360 / 60f * i
canvas.rotate(iDeg)

mPaint.alpha = if (iDeg + degrees == 0f) 255 else






请到「今天看啥」查看全文