自定義 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 的,我是按這樣的分辨率寫的,在不同的分辨率可能會有不同的效果,還請自己修改參數。

    最後的最後,是這個動畫的問題,這個沒有完成的動畫始終有點放不下,如果大佬有興趣可以去進行修改一下動畫的代碼,達到那個視頻的效果,可以多多交流一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章