码个蛋(codeegg)第 690 次推文

作者: 二娃_

原文: 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.143fmMinuteR = mWidth * 0.35fmSecondR = mWidth * 0.35f
}//在onDraw方法将画布原点平移到中心位置
override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)if (canvas == null) returncanvas.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 = 255mPaint.textAlign = Paint.Align.CENTERcanvas.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 = 255mPaint.textAlign = Paint.Align.CENTERcanvas.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 * icanvas.rotate(iDeg)//这里处理当前时间点的透明度,因为degrees控制整体逆时针旋转//iDeg控制绘制时顺时针,所以两者和为0时,刚好在x正半轴上,也就是起始绘制位置。mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()mPaint.textAlign = Paint.Align.LEFTcanvas.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 * icanvas.rotate(iDeg)mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()mPaint.textAlign = Paint.Align.RIGHTif (i < 59) {canvas.drawText("${(i + 1).toText()}分", mMinuteR, mPaint.getCenteredY(), mPaint)}canvas.restore()}canvas.restore()
}/*** 绘制秒*/
private fun drawSecond(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 * icanvas.rotate(iDeg)mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()mPaint.textAlign = Paint.Align.LEFTif (i < 59) {canvas.drawText("${(i + 1).toText()}秒", mSecondR, mPaint.getCenteredY(), mPaint)}canvas.restore()}canvas.restore()
}

DuangDuang!!效果出来啦~

4. 让时钟转起来

那么如何可以让时钟转起来呢?我们再看一下 onDraw() 中的代码,绘制三个圈的方法都会接收一个相应的 degrees: Float 参数,这个是控制一个圈的整体旋转的,而且要逆时针转,所以始终得是负数。

这样一来就好说了,只要控制这三个角度变化,就能让时钟动起来。

override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)...//省略//绘制各元件,后文会涉及到drawCenterInfo(canvas)drawHour(canvas, mHourDeg)drawMinute(canvas, mMinuteDeg)drawSecond(canvas, mSecondDeg)...//省略
}

那么首先定义三个角度的全局变量,并把他们与实际的时间关联起来,然后每隔一秒触发一次 View 的重绘即可。

//定义三个角度的全局变量
private var mHourDeg: Float by Delegates.notNull()
private var mMinuteDeg: Float by Delegates.notNull()
private var mSecondDeg: Float by Delegates.notNull()/*** 绘制方法*/
fun doInvalidate() {Calendar.getInstance().run {val hour = get(Calendar.HOUR)val minute = get(Calendar.MINUTE)val second = get(Calendar.SECOND)//这里将三个角度与实际时间关联起来,当前几点几分几秒,就把相应的圈逆时针旋转多少mHourDeg = -360 / 12f * (hour - 1)mMinuteDeg = -360 / 60f * (minute - 1)mSecondDeg = -360 / 60f * (second - 1)invalidate()}
}

然后只需在 Activity 中使用 timer 每秒钟刷新一次 View 即可。效果如下图,会发现转是转起来的,但是却每秒一跳。再看一下咱们当时的分析:

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

所以是还差一个线性旋转的效果。

//Activity中的代码
private var mTimer: Timer? = null
private fun caseTextClock() {setContentView(R.layout.activity_stage_text_clock)mTimer = timer(period = 1000) {runOnUiThread {stage_textClock.doInvalidate()}}}override fun onDestroy() {super.onDestroy()mTimer?.cancel()
}

5. 让时钟转的优雅点

基于我们已经知道了,时钟动起来的本质就是在一段时间内(比如 150ms)不断的改变参数 degrees: Float 的值并触发重绘方法,这样就产生了人眼看到的动画效果。

所以,我们想让「秒圈」(三个圈的代表)转的更线性更优雅一点,就可以在要开始绘制新的一秒的时候,在前 150ms 线性的旋转 6°

init {//处理动画,声明全局的处理器mAnimator = ValueAnimator.ofFloat(6f, 0f)//由6降到1mAnimator.duration = 150mAnimator.interpolator = LinearInterpolator()//插值器设为线性doInvalidate()
}/*** 开始绘制*/
fun doInvalidate() {Calendar.getInstance().run {val hour = get(Calendar.HOUR)val minute = get(Calendar.MINUTE)val second = get(Calendar.SECOND)mHourDeg = -360 / 12f * (hour - 1)mMinuteDeg = -360 / 60f * (minute - 1)mSecondDeg = -360 / 60f * (second - 1)//记录当前角度,然后让秒圈线性的旋转6°val hd = mHourDegval md = mMinuteDegval sd = mSecondDeg//处理动画mAnimator.removeAllUpdateListeners()//需要移除先前的监听mAnimator.addUpdateListener {val av = (it.animatedValue as Float)if (minute == 0 && second == 0) {mHourDeg = hd + av * 5//时圈旋转角度是分秒的5倍,线性的旋转30°}if (second == 0) {mMinuteDeg = md + av//线性的旋转6°}mSecondDeg = sd + av//线性的旋转6°invalidate()}mAnimator.start()}
}

就用这美丽优雅的时钟结尾吧~

文末

个人能力有限,如有不正之处欢迎大家批评指出,我会虚心接受并第一时间修改,以不误导大家。写作时参考以下文章,特别感谢。

  • 自定义 View 1-3 drawText() 文字的绘制(https://hencoder.com/ui-1-3/

  • Android关于Paint你所知道的和不知道的一切(https://juejin.im/post/5be29c206fb9a049ab0d1663

近期文章:

  • Android Q 适配 之 存储新特性

  • RxJava 不是上帝,真不推荐再用了

  • Activity、View、Window关系,进程间通信,责任链模式,Https,数据存储

日问题:

你写过什么酷炫的效果,悄悄后台告诉我?

专属升级社区:《这件事情,我终于想明白了》

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

  1. 【AI产品】产品小姐姐分析抖音背后的计算机视觉技术

    大家好,今天开设新的专栏<AI产品>,在这个专栏中,我们将以产品体验为主,简单剖析背后的核心技术,这是一个更加贴近工业实践的专栏. 今天就体验抖音基于计算机视觉技术实现的几项高大上的功能, ...

  2. python人脸识别源码_Python 抖音机器人,让你找到漂亮小姐姐

    本项目作者沉迷于抖音无法自拔,常常花好几个小时在抖音漂亮小姐姐身上. 本着高效.直接地找到漂亮小姐姐的核心思想,我用 Python + ADB 做了一个 Python 抖音机器人 Douyin-Bot ...

  3. 文字旋转_技术宅大白教你用软件做抖音爆火的文字旋转视频

    大家好,你的大白上线咯~ 相信爱刷抖音的你经常会在抖音里看到这种文字旋转的视频,那怎么做这种视频呢? 文字旋转 视频 做这种视频,有几种常见的方法,下面我们就来一一介绍一下~ 手机App 快影 这个A ...

  4. 分析抖音爆红原因,看抖音的未来发展

    抖音火爆,已经是近两年移动互联网行业无法忽视的现象,对于抖音为什么会火,或许答案有千百个,比如算法机制上瘾,闲得无聊可以打发时间,自己本身就是短视频创作者等等.不若与众认为除去平台自身的各种功能特性能 ...

  5. excel表格末尾添加一行_七夕表白,用Excel试试!抖音爆红,一晚点赞破百万

    "Excel"对于我们来说是再熟悉不过了,毕竟谁没有过一段被表格折磨的岁月?稳住~ 但是但是!听到老外对你说"You excel me"--"你表格我 ...

  6. 这块抖音爆红的支架,搞到一波优惠,按需而入!

    它们对于老司机或者菜鸟,随着智能手机的快速发展,手机支架已经成了新司机.老司机们都离不开的小配件 车载支架万万千,但它们都有一个共同的毛病--不低调!放在车内实在太抢眼,影响美感.而我们今天要介绍的这 ...

  7. Android 抖音爆红的口红挑战爬坑总结

    系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 例如:第一章 Python 机器学习入门之pandas的使用 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮 ...

  8. win10 SystemParametersInfo 设置屏保 不好使_抖音爆火的电子时钟罗盘屏保怎么搞呢?!...

    导语: 视频里面出现的就是抖音最近火了的电子罗盘屏保 也是各位粉丝心心念念 希望出教程的 下面和大家 分享一下 首先,各位将打包好的 Word Clock 压缩包下载下来.下载之后,我们将文件进行解压 ...

  9. TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!

    开源地址 首先抛出GitHub地址吧~多多支持指点,谢谢. AYTikTokPod 简述 iOS逆向工程指的是软件层面上进行逆向分析的过程. 在一般的软件开发流程中,都是过程导向结果.在逆向中,你首先 ...

最新文章

  1. 《深入理解ES6》笔记——块级作用域绑定(1)
  2. win7 64位系统oracle客户端访问远程数据库
  3. hdu5246 超级赛亚ACMer (百度之星初赛)(模拟)
  4. 硬中断、软中断和信号
  5. 命令行参数的模式匹配
  6. android语音识别开源代码,android语音识别,有没有相应的源码,教程可以推荐啊?
  7. 【机器学习】网格搜索、随机搜索和贝叶斯搜索实用教程
  8. WEB渗透测试工程师需要具备的技能
  9. 将WinPE安装至硬盘
  10. 图像处理 var_threshold与binary_threshold
  11. springMVC注解的意思
  12. 制作U盘引导盘,安装Ubuntu18.04系统
  13. Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)
  14. Python迷宫游戏
  15. 这么多处理器(CPU/SOC)牌子,到底哪家强
  16. MySQL 多表关联修改语句
  17. ERLANG日期与时间
  18. python 日历壁纸_Excel+Python=精美DIY壁纸日历
  19. 漫说Android 中SurfaceView蕴含的美
  20. 微信编辑器都有什么功能?

热门文章

  1. Python流体动力学共形映射库埃特式流
  2. 图形用户界面GUI(二)
  3. 【读书笔记】《算法竞赛进阶指南》读书笔记——0x00基本算法
  4. 什么是灵活的软件授权模式,如何选择软件加密狗?
  5. base64、图片相互转
  6. 房企数据中台:核心是提高销售,客户开什么车也有用 | 地产圆桌会⑤
  7. 心理学在生活中的表现和应用_生活中的心理学现象与应用
  8. Dota2发布自定义游戏
  9. 未知的事情,发生在未知的时候
  10. 网欣房地产成本管理系统,成本软件