android高仿小米時鐘(使用Camera和Matrix實現(xiàn)3D效果)
繼續(xù)練習自定義View。。畢竟熟才能生巧。一直覺得小米的時鐘很精美,那這次就搞它~這次除了練習自定義View,還涉及到使用Camera和Matrix實現(xiàn)3D效果。

一個這樣的效果,在繪制的時候最好選擇一個方向一步一步的繪制,這里我選擇由外到內(nèi)、由深到淺的方向來繪制,代碼步驟如下:
1、首先老一套~新建attrs.xml文件,編寫自定義屬性如時鐘背景色、亮色(用于分針、秒針、漸變終止色)、暗色(圓弧、刻度線、時針、漸變起始色),新建MiClockView繼承View,重寫構(gòu)造方法,獲取自定義屬性值,初始化Paint、Path以及畫圓、弧需要的RectF等東東,重寫onMeasure計算寬高,這里不再啰嗦~剛開始學自定義View的同學建議從我的前幾篇博客看起
2、由于onSizeChanged方法在構(gòu)造方法、onMeasure之后,又在onDraw之前,此時已經(jīng)完成全局變量初始化,也得到了控件的寬高,所以可以在這個方法中確定一些與寬高有關(guān)的數(shù)值,比如這個View的半徑啊、padding值等,方便繪制的時候計算大小和位置:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//寬和高分別去掉padding值,取min的一半即表盤的半徑
mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(),
h - getPaddingTop() - getPaddingBottom()) / 2;
//加一個默認的padding值,為了防止用camera旋轉(zhuǎn)時鐘時造成四周超出view大小
mDefaultPadding = 0.12f * mRadius;//根據(jù)比例確定默認padding大小
//為了適配控件大小match_parent、wrap_content、精確數(shù)值以及padding屬性
mPaddingLeft = mDefaultPadding + w / 2 - mRadius + getPaddingLeft();
mPaddingTop = mDefaultPadding + h / 2 - mRadius + getPaddingTop();
mPaddingRight = mPaddingLeft;
mPaddingBottom = mPaddingTop;
mScaleLength = 0.12f * mRadius;//根據(jù)比例確定刻度線長度
mScaleArcPaint.setStrokeWidth(mScaleLength);//刻度盤的弧寬
mScaleLinePaint.setStrokeWidth(0.012f * mRadius);//刻度線的寬度
//梯度掃描漸變,以(w/2,h/2)為中心點,兩種起止顏色梯度漸變
//float數(shù)組表示,[0,0.75)為起始顏色所占比例,[0.75,1}為起止顏色漸變所占比例
mSweepGradient = new SweepGradient(w / 2, h / 2,
new int[]{mDarkColor, mLightColor}, new float[]{0.75f, 1});
}
3、準備工作做的差不多了,那就開始繪制,根據(jù)方向我先確定最外層的小時時間文本的位置及其旁邊的四個?。?/p>

注意兩位數(shù)字的寬度和一位數(shù)的寬度是不一樣的,在計算的時候一定要注意
String timeText = "12";
mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
int textLargeWidth = mTextRect.width();//兩位數(shù)字的寬
mCanvas.drawText("12", getWidth() / 2 - textLargeWidth / 2, mPaddingTop + mTextRect.height(), mTextPaint);
timeText = "3";
mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
int textSmallWidth = mTextRect.width();//一位數(shù)字的寬
mCanvas.drawText("3", getWidth() - mPaddingRight - mTextRect.height() / 2 - textSmallWidth / 2,
getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
mCanvas.drawText("6", getWidth() / 2 - textSmallWidth / 2, getHeight() - mPaddingBottom, mTextPaint);
mCanvas.drawText("9", mPaddingLeft + mTextRect.height() / 2 - textSmallWidth / 2,
getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
我計算文本的寬高一般采用的方法是,new一個Rect,然后再繪制時調(diào)用
mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
將這個文本的范圍賦值給這個mTextRect,此時mTextRect.width()就是這段文本的寬,mTextRect.height()就是這段文本的高。

畫文本旁邊的四個弧:
mCircleRectF.set(mPaddingLeft + mTextRect.height() / 2 + mCircleStrokeWidth / 2,
mPaddingTop + mTextRect.height() / 2 + mCircleStrokeWidth / 2,
getWidth() - mPaddingRight - mTextRect.height() / 2 + mCircleStrokeWidth / 2,
getHeight() - mPaddingBottom - mTextRect.height() / 2 + mCircleStrokeWidth / 2);
for (int i = 0; i < 4; i++) {
mCanvas.drawArc(mCircleRectF, 5 + 90 * i, 80, false, mCirclePaint);
}
計算圓弧外接矩形的范圍別忘了加上圓弧線寬的一半
4、再往里是刻度盤,畫這個刻度盤的思路是現(xiàn)在底層畫一個mScaleLength寬度的圓,并設置SweepGradient漸變,上面再畫一圈背景色的刻度線。獲得SweepGradient的Matrix對象,通過不斷旋轉(zhuǎn)mGradientMatrix的角度實現(xiàn)刻度盤的旋轉(zhuǎn)效果:
/**
* 畫一圈梯度渲染的亮暗色漸變圓弧,重繪時不斷旋轉(zhuǎn),上面蓋一圈背景色的刻度線
*/
private void drawScaleLine() {
mScaleArcRectF.set(mPaddingLeft + 1.5f * mScaleLength + mTextRect.height() / 2,
mPaddingTop + 1.5f * mScaleLength + mTextRect.height() / 2,
getWidth() - mPaddingRight - mTextRect.height() / 2 - 1.5f * mScaleLength,
getHeight() - mPaddingBottom - mTextRect.height() / 2 - 1.5f * mScaleLength);
//matrix默認會在三點鐘方向開始顏色的漸變,為了吻合
//鐘表十二點鐘順時針旋轉(zhuǎn)的方向,把秒針旋轉(zhuǎn)的角度減去90度
mGradientMatrix.setRotate(mSecondDegree - 90, getWidth() / 2, getHeight() / 2);
mSweepGradient.setLocalMatrix(mGradientMatrix);
mScaleArcPaint.setShader(mSweepGradient);
mCanvas.drawArc(mScaleArcRectF, 0, 360, false, mScaleArcPaint);
//畫背景色刻度線
mCanvas.save();
for (int i = 0; i < 200; i++) {
mCanvas.drawLine(getWidth() / 2, mPaddingTop + mScaleLength + mTextRect.height() / 2,
getWidth() / 2, mPaddingTop + 2 * mScaleLength + mTextRect.height() / 2, mScaleLinePaint);
mCanvas.rotate(1.8f, getWidth() / 2, getHeight() / 2);
}
mCanvas.restore();
}
這里有一個全局變量mSecondDegree,即秒針旋轉(zhuǎn)的角度,需要根據(jù)當前時間動態(tài)獲?。?/p>
/**
* 獲取當前 時分秒 所對應的角度
* 為了不讓秒針走得像老式掛鐘一樣僵硬,需要精確到毫秒
*/
private void getTimeDegree() {
Calendar calendar = Calendar.getInstance();
float milliSecond = calendar.get(Calendar.MILLISECOND);
float second = calendar.get(Calendar.SECOND) + milliSecond / 1000;
float minute = calendar.get(Calendar.MINUTE) + second / 60;
float hour = calendar.get(Calendar.HOUR) + minute / 60;
mSecondDegree = second / 60 * 360;
mMinuteDegree = minute / 60 * 360;
mHourDegree = hour / 12 * 360;
}
5、然后就是畫秒針,用Path繪制一個指向12點鐘的三角形,通過不斷旋轉(zhuǎn)畫布實現(xiàn)秒針的旋轉(zhuǎn):
/**
* 畫秒針,根據(jù)不斷變化的秒針角度旋轉(zhuǎn)畫布
*/
private void drawSecondHand() {
mCanvas.save();
mCanvas.rotate(mSecondDegree, getWidth() / 2, getHeight() / 2);
mSecondHandPath.reset();
float offset = mPaddingTop + mTextRect.height() / 2;
mSecondHandPath.moveTo(getWidth() / 2, offset + 0.27f * mRadius);
mSecondHandPath.lineTo(getWidth() / 2 - 0.05f * mRadius, offset + 0.35f * mRadius);
mSecondHandPath.lineTo(getWidth() / 2 + 0.05f * mRadius, offset + 0.35f * mRadius);
mSecondHandPath.close();
mSecondHandPaint.setColor(mLightColor);
mCanvas.drawPath(mSecondHandPath, mSecondHandPaint);
mCanvas.restore();
}

6、看實現(xiàn)圖,時針在分針之下并且比分針顏色淺,那我就先畫時針,仍然是Path,并且針頭為圓弧狀,那么就用二階貝賽爾曲線,路徑為moveTo( A),lineTo(B),quadTo(C,D),lineTo(E),close.

/**
* 畫時針,根據(jù)不斷變化的時針角度旋轉(zhuǎn)畫布
* 針頭為圓弧狀,使用二階貝塞爾曲線
*/
private void drawHourHand() {
mCanvas.save();
mCanvas.rotate(mHourDegree, getWidth() / 2, getHeight() / 2);
mHourHandPath.reset();
float offset = mPaddingTop + mTextRect.height() / 2;
mHourHandPath.moveTo(getWidth() / 2 - 0.02f * mRadius, getHeight() / 2);
mHourHandPath.lineTo(getWidth() / 2 - 0.01f * mRadius, offset + 0.5f * mRadius);
mHourHandPath.quadTo(getWidth() / 2, offset + 0.48f * mRadius,
getWidth() / 2 + 0.01f * mRadius, offset + 0.5f * mRadius);
mHourHandPath.lineTo(getWidth() / 2 + 0.02f * mRadius, getHeight() / 2);
mHourHandPath.close();
mCanvas.drawPath(mHourHandPath, mHourHandPaint);
mCanvas.restore();
}
7、然后是分針,按照時針的思路:

/**
* 畫分針,根據(jù)不斷變化的分針角度旋轉(zhuǎn)畫布
*/
private void drawMinuteHand() {
mCanvas.save();
mCanvas.rotate(mMinuteDegree, getWidth() / 2, getHeight() / 2);
mMinuteHandPath.reset();
float offset = mPaddingTop + mTextRect.height() / 2;
mMinuteHandPath.moveTo(getWidth() / 2 - 0.01f * mRadius, getHeight() / 2);
mMinuteHandPath.lineTo(getWidth() / 2 - 0.008f * mRadius, offset + 0.38f * mRadius);
mMinuteHandPath.quadTo(getWidth() / 2, offset + 0.36f * mRadius,
getWidth() / 2 + 0.008f * mRadius, offset + 0.38f * mRadius);
mMinuteHandPath.lineTo(getWidth() / 2 + 0.01f * mRadius, getHeight() / 2);
mMinuteHandPath.close();
mCanvas.drawPath(mMinuteHandPath, mMinuteHandPaint);
mCanvas.restore();
}
8、最后由于path是close的,所以干脆畫兩個圓蓋在上面:

/**
* 畫指針的連接圓圈,蓋住指針path在圓心的連接線
*/
private void drawCoverCircle() {
mCanvas.drawCircle(getWidth() / 2, getHeight() / 2, 0.05f * mRadius, mSecondHandPaint);
mSecondHandPaint.setColor(mBackgroundColor);
mCanvas.drawCircle(getWidth() / 2, getHeight() / 2, 0.025f * mRadius, mSecondHandPaint);
}
9、終于畫完了,onDraw部分就是這樣
@Override
protected void onDraw(Canvas canvas) {
mCanvas = canvas;
getTimeDegree();
drawTimeText();
drawScaleLine();
drawSecondHand();
drawHourHand();
drawMinuteHand();
drawCoverCircle();
invalidate();
}
繪制的時候,尤其是像這樣圓形view,靈活運用
canvas.save(); canvas.rotate(mDegree, mCenterX, mCenterY); <!-- draw something --> canvas.restore();
這一套組合拳可以減少不少三角函數(shù)、角度弧度相關(guān)的計算。
10、辣么接下來就是如何實現(xiàn)觸摸使鐘表3D旋轉(zhuǎn)
借助Camera類和Matrix類,在構(gòu)造方法中:
Matrix mCameraMatrix = new Matrix(); Camera mCamera = new Camera();
/**
* 設置3D時鐘效果,觸摸矩陣的相關(guān)設置、照相機的旋轉(zhuǎn)大小
* 應用在繪制圖形之前,否則無效
*
* @param rotateX 繞X軸旋轉(zhuǎn)的大小
* @param rotateY 繞Y軸旋轉(zhuǎn)的大小
*/
private void setCameraRotate(float rotateX, float rotateY) {
mCameraMatrix.reset();
mCamera.save();
mCamera.rotateX(mCameraRotateX);//繞x軸旋轉(zhuǎn)角度
mCamera.rotateY(mCameraRotateY);//繞y軸旋轉(zhuǎn)角度
mCamera.getMatrix(mCameraMatrix);//相關(guān)屬性設置到matrix中
mCamera.restore();
//camera在view左上角那個點,故旋轉(zhuǎn)默認是以左上角為中心旋轉(zhuǎn)
//故在動作之前pre將matrix向左移動getWidth()/2長度,向上移動getHeight()/2長度
mCameraMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
//在動作之后post再回到原位
mCameraMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
mCanvas.concat(mCameraMatrix);//matrix與canvas相關(guān)聯(lián)
}
這段代碼除了camera的旋轉(zhuǎn)、平移、縮放之類的操作之外,剩下的代碼一般是固定的
全局變量mCameraRotateX和mCameraRotateY應該與此時手指觸摸坐標相關(guān)聯(lián)動態(tài)獲?。?/p>
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getCameraRotate(event);
break;
case MotionEvent.ACTION_MOVE:
//根據(jù)手指坐標計算camera應該旋轉(zhuǎn)的大小
getCameraRotate(event);
break;
}
return true;
}
Camera的坐標系和View的坐標系是不一樣的
View坐標系是二維的,原點在屏幕左上角,右為x軸正方向,下為y軸正方向;而Camera坐標系是三維的,原點在屏幕左上角,右為x軸正方向,上為y軸正方向,屏幕向里為z軸正方向
/**
* 獲取camera旋轉(zhuǎn)的大小
* 注意view坐標與camera坐標方向的轉(zhuǎn)換
*/
private void getCameraRotate(MotionEvent event) {
float rotateX = -(event.getY() - getHeight() / 2);
float rotateY = (event.getX() - getWidth() / 2);
//求出此時旋轉(zhuǎn)的大小與半徑之比
float percentX = rotateX / mRadius;
float percentY = rotateY / mRadius;
if (percentX > 1) {
percentX = 1;
} else if (percentX < -1) {
percentX = -1;
}
if (percentY > 1) {
percentY = 1;
} else if (percentY < -1) {
percentY = -1;
}
//最終旋轉(zhuǎn)的大小按比例勻稱改變
mCameraRotateX = percentX * mMaxCameraRotate;
mCameraRotateY = percentY * mMaxCameraRotate;
}
11、最后在onTouchEvent中松開手指時加一個復原并晃動的動畫
case MotionEvent.ACTION_UP:
//松開手指,時鐘復原并伴隨晃動動畫
ValueAnimator animX = getShakeAnim(mCameraRotateX, 0);
animX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCameraRotateX = (float) valueAnimator.getAnimatedValue();
}
});
ValueAnimator animY = getShakeAnim(mCameraRotateY, 0);
animY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCameraRotateY = (float) valueAnimator.getAnimatedValue();
}
});
break;
/**
* 使用OvershootInterpolator完成時鐘晃動動畫
*/
private ValueAnimator getShakeAnim(float start, float end) {
ValueAnimator anim = ValueAnimator.ofFloat(start, end);
anim.setInterpolator(new OvershootInterpolator(10));
anim.setDuration(500);
anim.start();
return anim;
}
終于寫完了,這個MiClockView適配也做的差不多了,時間也是同步的手機時間,一般可以拿來就用了~
demo下載地址:http://xiazai.jb51.net/201701/yuanma/MiClockView_jb51.rar
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
android Gallery組件實現(xiàn)的iPhone圖片滑動效果實例
這篇文章主要介紹了android Gallery組件實現(xiàn)的iPhone圖片滑動效果實例,即相冊內(nèi)的圖片實現(xiàn)可左右滑動的效果,需要的朋友可以參考下2014-07-07
Android 獲取屏幕高度,標題高度,狀態(tài)欄高度(實例代碼)
getWindow().findViewById(Window.ID_ANDROID_CONTENT)這個方法獲取到的view就是程序不包括標題欄的部分,然后就可以知道標題欄的高度了2013-11-11
Kotlin擴展函數(shù)及實現(xiàn)機制的深入探索
擴展函數(shù)與擴展屬性的神奇之處在于,可以在不修改原來類的條件下,使用函數(shù)和屬性,表現(xiàn)得就像是屬于這個類的一樣。下面這篇文章主要給大家介紹了關(guān)于Kotlin擴展函數(shù)及實現(xiàn)機制的相關(guān)資料,需要的朋友可以參考下2018-06-06
詳解Android WebView監(jiān)聽console錯誤信息
這篇文章主要介紹了Android WebView監(jiān)聽console錯誤信息,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
Android 修改系統(tǒng)關(guān)機動畫的實現(xiàn)
這篇文章主要介紹了Android 修改系統(tǒng)關(guān)機動畫的實現(xiàn)的相關(guān)資料,需要的朋友可以參考下2016-10-10
Android實現(xiàn)TCP斷點上傳 后臺C#服務接收
這篇文章主要為大家詳細介紹了Android實現(xiàn)TCP斷點上傳,后臺C#服務實現(xiàn)接收,感興趣的小伙伴們可以參考一下2016-08-08
Android開發(fā)之5.0activity跳轉(zhuǎn)時共享元素的使用方法
下面小編就為大家分享一篇Android開發(fā)之5.0activity跳轉(zhuǎn)時共享元素的使用方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01
android Web跳轉(zhuǎn)到app指定頁面并傳遞參數(shù)實例
這篇文章主要介紹了android Web跳轉(zhuǎn)到app指定頁面并傳遞參數(shù)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
DCloud的native.js調(diào)用系統(tǒng)分享實例Android版代碼
本文為大家分享了DCloud的native.js如何調(diào)用系統(tǒng)分享功能Android版的實例代碼,直接拿來就用2018-09-09

