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

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running

偶然间看到了一个时钟罗盘的动画效果,那个是桌面版的,用来当屏保效果还不错。于是呢,在抖音视频上搜了一下,果然找到这种时钟的效果视频,当然还有设置的教程。至于什么效果,插一段抖音视频的动态图:

自定义 View 之抖音时钟罗盘仪效果_第1张图片
image

就是这个样子的,由于它这个视频格式是 mp4 的,也无法上传,就录了一点点效果,也可以看了。

首先呢,看到这个效果,感觉还是可以的,正好博主这几天都在搞自定义 View 这一块,恰好也有这个兴致可以玩一玩。之前还没做过类似于时钟的效果,刚好可以尝试一下。

于是呢,我就开始盯着这个动画看了好一会儿,把里面的一些信息给记录了下来。首先呢,它是以罗盘的形式在转动的,可以观察它的罗盘指针,那个高亮文本的信息指出的就是当前的系统时间,而且它是始终固定在那里的。

罗盘呢,是一个联动效果的仪器,从最外圈带动内圈转动,起到更新时间的效果。但这些都是我们的视觉效果,其实不就是绘制一个一个圆,计算好它们的半径,然后圆上面都是文字嘛。

经过了上面的初步分析,然后我就开始起手写代码了。我刚开始也是照着视频中的效果还原的,不过很可惜,这个视频中的信息量太大了,由于我们的手机屏幕比较小,不太适合视频中的那么多信息,于是我就把其中的月份、星期等给去除了,我们剩下的就是这样的效果:

自定义 View 之抖音时钟罗盘仪效果_第2张图片
image

细心的小伙伴可能一眼就发现,你这个效果明显和视频里面的有差距,视频里面有旋转动画,这个没有啊。这个确实,我个人能力有限,在代码中也添加了旋转动画效果,可能计算动画时,会有一个 bug,目前呢,还没有得到改善,还望大佬们指点指点。

不过呢,实现这个效果,才是我们的首要目的,动画什么的只是锦上添花。接下来,我们来看看实现的步骤和要点吧。

首先呢,我们从最里面的 12 个时辰开始,这里需要获取一下系统的时间,然后取匹配我们的对应的字符,因为系统的默认格式是:01~12 这样的,显然我们需要中文的格式,但这部分也比较简单。

接着我们需要把文字绘制成一圈的形式,重点开始。如何绘制一圈的文字,我在这也卡了挺久的,我的做法是这样的,首先把画布的中心点平移到屏幕的中心,这个好说。然后 12 个时辰绘制一圈,就是 360°/12 吧,这个也好说。但是呢,这里我们不能直接进行绘制,那会出现这个效果:

自定义 View 之抖音时钟罗盘仪效果_第3张图片
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 之抖音时钟罗盘仪效果)