自定义|自定义 View 之抖音时钟罗盘仪效果

博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
偶然间看到了一个时钟罗盘的动画效果,那个是桌面版的,用来当屏保效果还不错。于是呢,在抖音视频上搜了一下,果然找到这种时钟的效果视频,当然还有设置的教程。至于什么效果,插一段抖音视频的动态图:
自定义|自定义 View 之抖音时钟罗盘仪效果
文章图片
image 就是这个样子的,由于它这个视频格式是 mp4 的,也无法上传,就录了一点点效果,也可以看了。
首先呢,看到这个效果,感觉还是可以的,正好博主这几天都在搞自定义 View 这一块,恰好也有这个兴致可以玩一玩。之前还没做过类似于时钟的效果,刚好可以尝试一下。
于是呢,我就开始盯着这个动画看了好一会儿,把里面的一些信息给记录了下来。首先呢,它是以罗盘的形式在转动的,可以观察它的罗盘指针,那个高亮文本的信息指出的就是当前的系统时间,而且它是始终固定在那里的。
罗盘呢,是一个联动效果的仪器,从最外圈带动内圈转动,起到更新时间的效果。但这些都是我们的视觉效果,其实不就是绘制一个一个圆,计算好它们的半径,然后圆上面都是文字嘛。
经过了上面的初步分析,然后我就开始起手写代码了。我刚开始也是照着视频中的效果还原的,不过很可惜,这个视频中的信息量太大了,由于我们的手机屏幕比较小,不太适合视频中的那么多信息,于是我就把其中的月份、星期等给去除了,我们剩下的就是这样的效果:
自定义|自定义 View 之抖音时钟罗盘仪效果
文章图片
image 细心的小伙伴可能一眼就发现,你这个效果明显和视频里面的有差距,视频里面有旋转动画,这个没有啊。这个确实,我个人能力有限,在代码中也添加了旋转动画效果,可能计算动画时,会有一个 bug,目前呢,还没有得到改善,还望大佬们指点指点。
不过呢,实现这个效果,才是我们的首要目的,动画什么的只是锦上添花。接下来,我们来看看实现的步骤和要点吧。
首先呢,我们从最里面的 12 个时辰开始,这里需要获取一下系统的时间,然后取匹配我们的对应的字符,因为系统的默认格式是:01~12 这样的,显然我们需要中文的格式,但这部分也比较简单。
接着我们需要把文字绘制成一圈的形式,重点开始。如何绘制一圈的文字,我在这也卡了挺久的,我的做法是这样的,首先把画布的中心点平移到屏幕的中心,这个好说。然后 12 个时辰绘制一圈,就是 360°/12 吧,这个也好说。但是呢,这里我们不能直接进行绘制,那会出现这个效果:
自定义|自定义 View 之抖音时钟罗盘仪效果
文章图片
image 文本是水平的,但是效果中是有偏移角度的。于是呢,我就想到用 canvas 的 rotate 方法,没绘制一个文本,旋转 360°/12 的角度即可,因为有 12 个时辰,只需要来个循环就搞定了。
private void drawHour(Canvas canvas) { float perAngle = 360f / 12f; int minuteIndex = Integer.valueOf(getTime("hh")) - 1; String[] preString = Arrays.copyOfRange(mHour, 0, minuteIndex); String[] sufString = Arrays.copyOfRange(mHour, minuteIndex, 12); String[] newHour = concat(sufString, preString); for (int i = 0; i < 12; i++) { canvas.save(); //设置当前画笔颜色 float curAngle = perAngle * i; setCurrentColor(curAngle); //镜像效果 canvas.scale(-1, 1, 0, 0); //旋转画布 canvas.rotate(curAngle, 0, 0); mPaints[1].setTextScaleX(-1); canvas.drawText(newHour[i], -180, 0 + mTextHeight, mPaints[1]); canvas.restore(); } }

就是上面的代码,旋转了画布。不过呢,这里旋转画布之后,我们的起始位置是在左边的,就是那个高亮的文本会在左边位置,而且文字是倒过来的,所以要对画布进行 scale 镜像处理,让高亮文本移动右边,并且文字为正常显示。
除了这个细节的处理,还有一个是 paint 笔的处理,默认的话,画布被我们镜像了之后,会出现这样的情况,文本的 “十点” 变成倒过来了 “点十”,并且呢它是向内的,这就有点难受了。不过还好,paint 也有提供镜像的功能,我们上面的代码,也对 paint 进行了镜像操作,顺利解决诸多问题,终于把一 到十二点给绘制成了一圈的样式了。
接下来就是 1 ~ 59 分和1 ~ 59 秒了呗,这就与 1~12 时辰一个方法,只不过要主要的是,它们都有 60 个,是从 00 ~ 59 的,所以每一度要用 360°/60 才行,并且半径要算好,刚刚好留点小间距,别让文字重合即可。
private void drawMinute(Canvas canvas) { float perAngle = 360f / 60f; int minuteIndex = Integer.valueOf(getTime("mm")); String[] preString = Arrays.copyOfRange(mMinute, 0, minuteIndex); String[] sufString = Arrays.copyOfRange(mMinute, minuteIndex, 60); String[] newMinute = concat(sufString, preString); for (int i = 0; i < 60; i++) { canvas.save(); //设置当前画笔颜色 float curAngle = perAngle * i; setCurrentColor(curAngle); //镜像效果 canvas.scale(-1, 1, 0, 0); //旋转画布 canvas.rotate(curAngle, 0, 0); mPaints[1].setTextScaleX(-1); canvas.drawText(newMinute[i], -getBound().width() * 6f - 120, 0 + mTextHeight, mPaints[1]); canvas.restore(); } }

上面的是绘制分钟的代码,绘制小时的我就不贴出来了,后面会贴完整代码。接着就是中心部分的时间了,这部分没上面好说的,就是计算坐标,绘制文本,代码如下:
private void drawCenterTime(Canvas canvas) { String time = getTime("HH:mm:ss"); mPaints[0].setColor(Color.WHITE); mPaints[0].setTextSize(70f); Rect bounds = new Rect(); mPaints[0].getTextBounds(time, 0, time.length(), bounds); Paint.FontMetrics fontMetrics = mPaints[0].getFontMetrics(); float y = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent; canvas.drawText(time, -bounds.width() / 2, y, mPaints[0]); }

接下来就是动画了,我们就每 1 秒获取系统时间,然后刷新一次 View,就完成了。
private void setTimeAndAnimator() { if (timeAnimator == null) { timeAnimator = ObjectAnimator.ofFloat(0f, -6f); timeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { diff = (float) animation.getAnimatedValue(); //invalidate(); } }); timeAnimator.setDuration(1000); timeAnimator.start(); timeAnimator.setInterpolator(new LinearInterpolator()); timeAnimator.setRepeatCount(-1); timeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationRepeat(Animator animation) { invalidate(); } }); } }

这里的动画监听,如上面注释的那行刷新代码,它是会开启动画效果的,但是有点细节没有处理好,不知到如何计算坐标了,动画不是特别流畅,所以我给它屏蔽了。
好了,下面是完整的代码:
package nd.no.xww.qqmessagedragview; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.LinearInterpolator; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Timer; import java.util.TimerTask; /** * @author xww * @desciption : 抖音视频里的一个时钟罗盘效果 * @date 2019/8/10 * @time 14:48 * 博主:威威喵 * 博客:https://blog.csdn.net/smile_Running */ public class DYClockCompass extends View {/** * 1、当前时间的获取,简单 * 2、当前时间的颜色(判断是否当前时间) * 3、绘制刻度,罗盘指针固定位置,变动的只有刻度 * * 4、刻度信息,由内到外:月份、号数、周数、小时、分钟、秒 */private String[] mHour = new String[]{"一点", "二点", "三点", "四点", "五点", "六点", "七点", "八点", "九点", "十点", "十一点", "十二点"}; private String[] mMinute = new String[]{ "零分", "一分", "二分", "三分", "四分", "五分", "六分", "七分", "八分", "九分", "十分", "十一分", "十二分", "十三分", "十四分", "十五分", "十六分", "十七分", "十八分", "十九分", "二十分", "二十一分", "二十二分", "二十三分", "二十四分", "二十五分", "二十六分", "二十七分", "二十八分", "二十九分", "三十分", "三十一分", "三十二分", "三十三分", "三十四分", "三十五分", "三十六分", "三十七分", "三十八分", "三十九分", "四十分", "四十一分", "四十二分", "四十三分", "四十四分", "四十五分", "四十六分", "四十七分", "四十八分", "四十九分", "五十分", "五十一分", "五十二分", "五十三分", "五十四分", "五十五分", "五十六分", "五十七分", "五十八分", "五十九分" }; private String[] mSeconds = new String[]{ "零秒", "一秒", "二秒", "三秒", "四秒", "五秒", "六秒", "七秒", "八秒", "九秒", "十秒", "十一秒", "十二秒", "十三秒", "十四秒", "十五秒", "十六秒", "十七秒", "十八秒", "十九秒", "二十秒", "二十一秒", "二十二秒", "二十三秒", "二十四秒", "二十五秒", "二十六秒", "二十七秒", "二十八秒", "二十九秒", "三十秒", "三十一秒", "三十二秒", "三十三秒", "三十四秒", "三十五秒", "三十六秒", "三十七秒", "三十八秒", "三十九秒", "四十秒", "四十一秒", "四十二秒", "四十三秒", "四十四秒", "四十五秒", "四十六秒", "四十七秒", "四十八秒", "四十九秒", "五十秒", "五十一秒", "五十二秒", "五十三秒", "五十四秒", "五十五秒", "五十六秒", "五十七秒", "五十八秒", "五十九秒" }; private int mWidth; private int mHeight; private float mCenterX; private float mCenterY; private Paint[] mPaints = new Paint[2]; private float mTextHeight; private Timer timer = new Timer(); private void init() { mPaints[0] = getPaint(Color.BLACK); mPaints[1] = getPaint(Color.GRAY); mPaints[1].setStyle(Paint.Style.FILL); Paint.FontMetrics fontMetrics = mPaints[1].getFontMetrics(); mTextHeight = Math.abs((fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent); }private Paint getPaint(int color) { Paint paint = new Paint(); paint.setDither(true); paint.setAntiAlias(true); paint.setTextSize(30f); paint.setColor(color); return paint; }public DYClockCompass(Context context) { this(context, null); }public DYClockCompass(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }public DYClockCompass(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); }@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = MeasureSpec.getSize(widthMeasureSpec); mHeight = MeasureSpec.getSize(heightMeasureSpec); mCenterX = mWidth / 2; mCenterY = mHeight / 2; }@Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); canvas.translate(mCenterX, mCenterY); //canvas.drawLine(0, 0, mWidth / 2, 0, mPaints[2]); drawHour(canvas); drawMinute(canvas); drawSeconds(canvas); setTimeAndAnimator(); drawCenterTime(canvas); }public Rect getBound() { Rect rect = new Rect(); mPaints[1].getTextBounds("一", 0, "一".length(), rect); return rect; }@SuppressLint("SimpleDateFormat") private String getTime(String format) { return new SimpleDateFormat(format).format(new Date(System.currentTimeMillis())); }private void drawHour(Canvas canvas) { float perAngle = 360f / 12f; int minuteIndex = Integer.valueOf(getTime("hh")) - 1; String[] preString = Arrays.copyOfRange(mHour, 0, minuteIndex); String[] sufString = Arrays.copyOfRange(mHour, minuteIndex, 12); String[] newHour = concat(sufString, preString); for (int i = 0; i < 12; i++) { canvas.save(); //设置当前画笔颜色 float curAngle = perAngle * i; setCurrentColor(curAngle); //镜像效果 canvas.scale(-1, 1, 0, 0); //旋转画布 canvas.rotate(curAngle, 0, 0); mPaints[1].setTextScaleX(-1); canvas.drawText(newHour[i], -180, 0 + mTextHeight, mPaints[1]); canvas.restore(); } }private void drawMinute(Canvas canvas) { float perAngle = 360f / 60f; int minuteIndex = Integer.valueOf(getTime("mm")); String[] preString = Arrays.copyOfRange(mMinute, 0, minuteIndex); String[] sufString = Arrays.copyOfRange(mMinute, minuteIndex, 60); String[] newMinute = concat(sufString, preString); for (int i = 0; i < 60; i++) { canvas.save(); //设置当前画笔颜色 float curAngle = perAngle * i; setCurrentColor(curAngle); //镜像效果 canvas.scale(-1, 1, 0, 0); //旋转画布 canvas.rotate(curAngle, 0, 0); mPaints[1].setTextScaleX(-1); canvas.drawText(newMinute[i], -getBound().width() * 6f - 120, 0 + mTextHeight, mPaints[1]); canvas.restore(); } }static String[] concat(String[] a, String[] b) { String[] c = new String[a.length + b.length]; System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(b, 0, c, a.length, b.length); return c; }private void drawSeconds(Canvas canvas) { float perAngle = 360f / 60f; int secondsIndex = Integer.valueOf(getTime("ss")); String[] preString = Arrays.copyOfRange(mSeconds, 0, secondsIndex); String[] sufString = Arrays.copyOfRange(mSeconds, secondsIndex, 60); String[] newSeconds = concat(sufString, preString); //Log.i("========", "newSeconds: " + Arrays.toString(newSeconds)); for (int i = 0; i < 60; i++) { canvas.save(); //镜像效果 canvas.scale(-1, 1, 0, 0); //设置当前画笔颜色 float curAngle = perAngle * i; setCurrentColor(curAngle); //旋转画布 canvas.rotate(curAngle + diff, 0, 0); mPaints[1].setTextScaleX(-1); canvas.drawText(newSeconds[i], -getBound().width() * 11f - 120, 0 + mTextHeight, mPaints[1]); canvas.restore(); } }ValueAnimator timeAnimator = null; private float diff; private void setTimeAndAnimator() { if (timeAnimator == null) { timeAnimator = ObjectAnimator.ofFloat(0f, -6f); timeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { diff = (float) animation.getAnimatedValue(); //invalidate(); } }); timeAnimator.setDuration(1000); timeAnimator.start(); timeAnimator.setInterpolator(new LinearInterpolator()); timeAnimator.setRepeatCount(-1); timeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationRepeat(Animator animation) { invalidate(); } }); } }private void drawCenterTime(Canvas canvas) { String time = getTime("HH:mm:ss"); mPaints[0].setColor(Color.WHITE); mPaints[0].setTextSize(70f); Rect bounds = new Rect(); mPaints[0].getTextBounds(time, 0, time.length(), bounds); Paint.FontMetrics fontMetrics = mPaints[0].getFontMetrics(); float y = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent; canvas.drawText(time, -bounds.width() / 2, y, mPaints[0]); }private void setCurrentColor(float curAngle) { if (curAngle == 0) mPaints[1].setColor(Color.WHITE); else mPaints[1].setColor(Color.GRAY); }}

最后,这个效果仅仅是我写来玩一玩的,偶然看到的一个时钟罗盘的软件,然后自己瞎写的,并没有处理分别率的问题,我的模拟器是 1920 * 1080 的,我是按这样的分辨率写的,在不同的分辨率可能会有不同的效果,还请自己修改参数。
【自定义|自定义 View 之抖音时钟罗盘仪效果】最后的最后,是这个动画的问题,这个没有完成的动画始终有点放不下,如果大佬有兴趣可以去进行修改一下动画的代码,达到那个视频的效果,可以多多交流一下。

    推荐阅读