博主聲明:
轉載請在開頭附加本文鏈接及作者信息,並標記爲轉載。本文由博主 威威喵 原創,請多支持與指教。
偶然間看到了一個時鐘羅盤的動畫效果,那個是桌面版的,用來當屏保效果還不錯。於是呢,在抖音視頻上搜了一下,果然找到這種時鐘的效果視頻,當然還有設置的教程。至於什麼效果,插一段抖音視頻的動態圖:
就是這個樣子的,由於它這個視頻格式是 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 的,我是按這樣的分辨率寫的,在不同的分辨率可能會有不同的效果,還請自己修改參數。
最後的最後,是這個動畫的問題,這個沒有完成的動畫始終有點放不下,如果大佬有興趣可以去進行修改一下動畫的代碼,達到那個視頻的效果,可以多多交流一下。