Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android高仿小米時鐘(使用Camera和Matrix實現3D效果)

android高仿小米時鐘(使用Camera和Matrix實現3D效果)

編輯:關於Android編程

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

這裡寫圖片描述

一個這樣的效果,在繪制的時候最好選擇一個方向一步一步的繪制,這裡我選擇由外到內、由深到淺的方向來繪制,代碼步驟如下:

1、首先老一套~新建attrs.xml文件,編寫自定義屬性如時鐘背景色、亮色(用於分針、秒針、漸變終止色)、暗色(圓弧、刻度線、時針、漸變起始色),新建MiClockView繼承View,重寫構造方法,獲取自定義屬性值,初始化Paint、Path以及畫圓、弧需要的RectF等東東,重寫onMeasure計算寬高,這裡不再啰嗦~剛開始學自定義View的同學建議從我的前幾篇博客看起

2、由於onSizeChanged方法在構造方法、onMeasure之後,又在onDraw之前,此時已經完成全局變量初始化,也得到了控件的寬高,所以可以在這個方法中確定一些與寬高有關的數值,比如這個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旋轉時鐘時造成四周超出view大小
  mDefaultPadding = 0.12f * mRadius;//根據比例確定默認padding大小
  //為了適配控件大小match_parent、wrap_content、精確數值以及padding屬性
  mPaddingLeft = mDefaultPadding + w / 2 - mRadius + getPaddingLeft();
  mPaddingTop = mDefaultPadding + h / 2 - mRadius + getPaddingTop();
  mPaddingRight = mPaddingLeft;
  mPaddingBottom = mPaddingTop;
  mScaleLength = 0.12f * mRadius;//根據比例確定刻度線長度
  mScaleArcPaint.setStrokeWidth(mScaleLength);//刻度盤的弧寬
  mScaleLinePaint.setStrokeWidth(0.012f * mRadius);//刻度線的寬度
  //梯度掃描漸變,以(w/2,h/2)為中心點,兩種起止顏色梯度漸變
  //float數組表示,[0,0.75)為起始顏色所占比例,[0.75,1}為起止顏色漸變所占比例
  mSweepGradient = new SweepGradient(w / 2, h / 2,
      new int[]{mDarkColor, mLightColor}, new float[]{0.75f, 1});
}

3、准備工作做的差不多了,那就開始繪制,根據方向我先確定最外層的小時時間文本的位置及其旁邊的四個弧:

這裡寫圖片描述

注意兩位數字的寬度和一位數的寬度是不一樣的,在計算的時候一定要注意

 

  String timeText = "12";
  mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
  int textLargeWidth = mTextRect.width();//兩位數字的寬
  mCanvas.drawText("12", getWidth() / 2 - textLargeWidth / 2, mPaddingTop + mTextRect.height(), mTextPaint);
  timeText = "3";
  mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
  int textSmallWidth = mTextRect.width();//一位數字的寬
  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,然後再繪制時調用

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、再往裡是刻度盤,畫這個刻度盤的思路是現在底層畫一個mScaleLength寬度的圓,並設置SweepGradient漸變,上面再畫一圈背景色的刻度線。獲得SweepGradient的Matrix對象,通過不斷旋轉mGradientMatrix的角度實現刻度盤的旋轉效果:

/**
 * 畫一圈梯度渲染的亮暗色漸變圓弧,重繪時不斷旋轉,上面蓋一圈背景色的刻度線
 */
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默認會在三點鐘方向開始顏色的漸變,為了吻合
  //鐘表十二點鐘順時針旋轉的方向,把秒針旋轉的角度減去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,即秒針旋轉的角度,需要根據當前時間動態獲取:

/**
 * 獲取當前 時分秒 所對應的角度
 * 為了不讓秒針走得像老式掛鐘一樣僵硬,需要精確到毫秒
 */
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點鐘的三角形,通過不斷旋轉畫布實現秒針的旋轉:

/**
 * 畫秒針,根據不斷變化的秒針角度旋轉畫布
 */
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、看實現圖,時針在分針之下並且比分針顏色淺,那我就先畫時針,仍然是Path,並且針頭為圓弧狀,那麼就用二階貝賽爾曲線,路徑為moveTo( A),lineTo(B),quadTo(C,D),lineTo(E),close.

這裡寫圖片描述

/**
 * 畫時針,根據不斷變化的時針角度旋轉畫布
 * 針頭為圓弧狀,使用二階貝塞爾曲線
 */
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、然後是分針,按照時針的思路:

這裡寫圖片描述

/**
 * 畫分針,根據不斷變化的分針角度旋轉畫布
 */
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();

這一套組合拳可以減少不少三角函數、角度弧度相關的計算。

10、辣麼接下來就是如何實現觸摸使鐘表3D旋轉

借助Camera類和Matrix類,在構造方法中:

Matrix mCameraMatrix = new Matrix();
Camera mCamera = new Camera();
/**
 * 設置3D時鐘效果,觸摸矩陣的相關設置、照相機的旋轉大小
 * 應用在繪制圖形之前,否則無效
 *
 * @param rotateX 繞X軸旋轉的大小
 * @param rotateY 繞Y軸旋轉的大小
 */
private void setCameraRotate(float rotateX, float rotateY) {
  mCameraMatrix.reset();
  mCamera.save();
  mCamera.rotateX(mCameraRotateX);//繞x軸旋轉角度
  mCamera.rotateY(mCameraRotateY);//繞y軸旋轉角度
  mCamera.getMatrix(mCameraMatrix);//相關屬性設置到matrix中
  mCamera.restore();
  //camera在view左上角那個點,故旋轉默認是以左上角為中心旋轉
  //故在動作之前pre將matrix向左移動getWidth()/2長度,向上移動getHeight()/2長度
  mCameraMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
  //在動作之後post再回到原位
  mCameraMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
  mCanvas.concat(mCameraMatrix);//matrix與canvas相關聯
}

這段代碼除了camera的旋轉、平移、縮放之類的操作之外,剩下的代碼一般是固定的

全局變量mCameraRotateX和mCameraRotateY應該與此時手指觸摸坐標相關聯動態獲取:

@Override
public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      getCameraRotate(event);
      break;
    case MotionEvent.ACTION_MOVE:
      //根據手指坐標計算camera應該旋轉的大小
      getCameraRotate(event);
      break;
  }
  return true;
}

Camera的坐標系和View的坐標系是不一樣的

View坐標系是二維的,原點在屏幕左上角,右為x軸正方向,下為y軸正方向;而Camera坐標系是三維的,原點在屏幕左上角,右為x軸正方向,上為y軸正方向,屏幕向裡為z軸正方向

/**
 * 獲取camera旋轉的大小
 * 注意view坐標與camera坐標方向的轉換
 */
private void getCameraRotate(MotionEvent event) {
  float rotateX = -(event.getY() - getHeight() / 2);
  float rotateY = (event.getX() - getWidth() / 2);
  //求出此時旋轉的大小與半徑之比
  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;
  }
  //最終旋轉的大小按比例勻稱改變
  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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved