源码地址

抖音网红文字时钟-TextClockView

起源

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

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

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

然而实现两者的基础便是拿起Canvas Paint等把它绘制出来,所以「上篇」我先用自定View的方式把时钟画出来,在Activity中展示效果。「下篇」的时候再把该View结合LiveWallPaper设置到壁纸。

思考分析

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

  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() 文字的绘制
  • Android关于Paint你所知道的和不知道的一切

我的其它文章

  • 【自定义View】抖音网红文字时钟-上篇
  • 【自定义View】洋葱数学同款阴影布局-ShadowLayout
  • 【自定义View】洋葱数学同款雷达图深入解析-RadarView

转载于:https://juejin.im/post/5cb53e93e51d456e55623b07

【自定义View】抖音网红文字时钟-上篇相关推荐

  1. 抖音文字时钟壁纸html源码,这次要把抖音网红文字时钟设置为壁纸了~

    原标题:这次要把抖音网红文字时钟设置为壁纸了~ 本文作者 作者:二娃_ https://juejin.im/post/5d52aea86fb9a06ae61aad5b 还记得上篇吗?我们先实现了抖音网 ...

  2. 抖音文字时钟壁纸html,网红文字时钟怎么弄 抖音网红文字时钟主题壁纸设置教程...

    类型:桌面主题大小:54KB语言:中文 评分:10.0 标签: 立即下载 最近很多小伙伴在抖音上看到了网红文字时钟主题壁纸,很有意思,自己也想要制作这种的,网红文字时钟怎么弄,西西小编为大家带来抖音网 ...

  3. python 角度判断_python的turtle也能仿抖音网红文字时钟的代码及分析

    1.说明: 1.1 推荐指数:★★★ 1.2 推荐环境:python3.8和微软vscode编辑器 1.3 感言: python的turtle也能画出文字时钟,效果虽然不理想,有闪跳,但是也很棒,未采 ...

  4. android 时钟字体下载,网红文字时钟轮盘手机版下载-网红文字时钟work clock安卓版下载 v1.0_5577安卓网...

    网红文字时钟work clock最新手机版提供,一款能够实现桌面时钟的手机壁纸.锁屏类软件,通过软件你可以让自己的手机桌面变成趣味的文字时钟哦,最近抖音和网红都在使用的壁纸APP,你也来下载使用吧! ...

  5. mac安静执行脚本_Desktop Goose for Mac在屏幕上到处乱跑的抖音网红桌面宠物鹅

    原标题:Desktop Goose for Mac在屏幕上到处乱跑的抖音网红桌面宠物鹅 最近抖音上火起来的桌面宠物鹅如何获取?这只抖音桌宠鹅可是吸引了大批抖友的眼球,那么今天小编就为大家分享一下Des ...

  6. python画旺仔代码_美术生把旺仔牛奶画成抖音网红,看清画的是谁,网友:确认过眼神...

    现在关于美术生和设计师画出来的很多新奇的东西非常火爆.因为我很爱吃辣条,所以我最有印象的就是卫龙辣条风衣外套,我觉得成品一定会卖断货,走出来非常的有气场,看来他们笔下的世界确实是很丰富多彩的.去吃个火 ...

  7. 音量计算机的网红音乐,网红音乐10首抖音网红歌,抖音网红歌曲100首BGM精选

    抖音是网红聚集地,但同时也诞生了非常多的好听神曲,这些抖音网红歌曲听起来是那么的悦耳,大多都是搭配BGM,那种感觉令人陶醉释压.下面网红音乐10首抖音网红歌,抖音网红歌曲100首BGM精选,快收藏慢慢 ...

  8. 抖音网红是如何推广产品的?

    在做Socialbook产品之前,咱们家做了相当长一段时间的网红推广,在业务开展期间,接触到了很多客户,涵盖各行各业,比如电商.实体店.游戏.应用.甚至黑五类的也来咨询,怎样让网红推广自家的产品,是大 ...

  9. 关于抖音网红推广,你想知道的50个问题都在这里!

    之前写了一些关于抖音的文章,就有很多朋友后台问关于抖音推广的问题,今天整理了一下并加了一些小编认为大家会有疑问的地方. 1.抖音网红推广是怎么收费的? 是按视频发布的条数收费的,跟公众号和微博差不多, ...

最新文章

  1. 最全面的卷积神经网络介绍,都在这里了(附代码)
  2. 简单总结一下.net获得虚拟目录几个基本操作
  3. android class newinstance 构造函数 参数,android Fragment里的newInstance和构造函数
  4. iar升级芯片库_IAR 发布支持ARM Cortex系列的开发工具包
  5. C和指针 (pointers on C)——第七章:函数(上)
  6. 一台服务器能承载多少用户_一台入门级服务器能为你的办公应用带来哪些效率?评测告诉你...
  7. War-Driving(战争驾驶***)
  8. Spring boot Tomcat配置
  9. android studio安装Kotlin(零)
  10. 如何在没有域的环境中搭建AlwaysOn(二)
  11. 数据库MySQL/mariadb知识点——操作篇(4)数据操作语句
  12. COLING 2020 | 字符感知预训练模型CharBERT
  13. 股票历史数据-股票前复权数据下载
  14. Apex弹窗闪退报错问题解决方案清晰讲解(系统软件层面解决,已亲测可行)
  15. 大数据学习入门难,中科天玑给初学者支几招
  16. scala_day01_安装_基础_IO_函数_递归_异常_方法_样例类_伴生对象
  17. 小车高速怎么收费标准_高速如何计费 2019高速公路收费标准及计算方法
  18. 综述 - 染色质可及性与调控表观基因组 | Chromatin accessibility and the regulatory epigenome...
  19. PHP接入微信官方支付(native·APIv3)
  20. docker run --device-write-bps doesn't effect

热门文章

  1. 软件工程(Software Engineering)
  2. 陈力:传智播客古代 珍宝币 泡泡龙游戏开发第26讲:PHP函数
  3. Windows开机自启动jar包
  4. QQ找茬辅助源码下载
  5. 想要创建个人博客只需五步骤——所有人看了都能学会的步骤
  6. Linux 系统必备入门级自学手册!
  7. python 化学计量学_如何入门化学计量学?有哪些推荐书目?
  8. python 化学计量学_强烈推荐!八个鲜为人知的大数据学习网站!
  9. 支付宝与微信转战刷脸支付,多年相爱相杀情归何处?
  10. 什么是Vue生命周期?Vue生命周期的作用是什么?vue八种钩子函数