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

博主声明:

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

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

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

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

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

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

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

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

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

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

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

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

     文本是水平的,但是效果中是有偏移角度的。于是呢,我就想到用 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、绘制刻度,罗盘指针固定位置,变动的只有刻度
     * <p>
     * 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 的,我是按这样的分辨率写的,在不同的分辨率可能会有不同的效果,还请自己修改参数。

    最后的最后,是这个动画的问题,这个没有完成的动画始终有点放不下,如果大佬有兴趣可以去进行修改一下动画的代码,达到那个视频的效果,可以多多交流一下。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值