不多说,直接上代码:
MainActivity:
package com.example.archermind.xiaomi;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
工具类DensityUtils:
package com.example.archermind.xiaomi;
import android.content.Context;
import android.util.TypedValue;
public class DensityUtils {
private DensityUtils(){
throw new UnsupportedOperationException("cannot be instantiated");
}
public static int dp2px(Context context, float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics());
}
public static int sp2px(Context context, float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, context.getResources().getDisplayMetrics());
}
public static float px2dp(Context context, float pxVal) {
final float scale = context.getResources().getDisplayMetrics().density;
return (pxVal / scale);
}
public static float px2sp(Context context, float pxVal) {
return pxVal / context.getResources().getDisplayMetrics().scaledDensity;
}
}
package com.example.archermind.xiaomi;
import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.Date;
public class MiClockView extends View {
private int mBackgroundColor;
private int mLightColor;
private int mDarkColor;
private int mTextSize;
private final int mStrokeWidth = 2;
private Paint mHourHandPaint;
private Paint mMinuteHandPaint;
private Paint mSecondHandPaint;
private Paint mScaleLinePaint;
private Paint mTextPaint;
private Paint mCirclePaint;
private Rect mTextRect;
private RectF mCircleRectF;
private RectF mScaleArcF;
private Path mHourHandPath;
private Path mMinuteHandPath;
private Path mSecondHandPath;
private Matrix mGradientMatrix;
private Matrix mCameraMatrix;
private Camera mCamera;
private int mRadius;
private float mDefaultPadding;
private float mPaddingLeft;
private float mPaddingTop;
private float mPaddingRight;
private float mPaddingBottom;
private float mScaleLength;
private Paint mScaleArcPaint;
private float mMaxCanvasTranslate;
private SweepGradient mSweepGradient;
private Canvas mCanvas;
private float mCanvasTranslateX;
private float mCanvasTranslateY;
private float mMaxCameraRotate = 10;
private float mCameraRotateX;
private float mCameraRotateY;
private float mSecondDegree;
private float mMinuteDegree;
private float mHourDegree;
private float mCircleStrokeWidth = 2;
private ValueAnimator mShakeAnim;
public MiClockView(Context context) {
this(context, null);
}
public MiClockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MiClockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MiClockView, defStyleAttr, 0);
mBackgroundColor = ta.getColor(R.styleable.MiClockView_backgroundColor, Color.parseColor("#237EAD"));
setBackgroundColor(mBackgroundColor);
mLightColor = ta.getColor(R.styleable.MiClockView_lightColor, Color.parseColor("#ffffff"));
mDarkColor = ta.getColor(R.styleable.MiClockView_darkColor, Color.parseColor("#80ffffff"));
mTextSize = (int) ta.getDimension(R.styleable.MiClockView_textSize, DensityUtils.sp2px(context, 14));
ta.recycle();
mHourHandPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHourHandPaint.setStyle(Paint.Style.FILL);
mHourHandPaint.setColor(mDarkColor);
mMinuteHandPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMinuteHandPaint.setColor(mLightColor);
mSecondHandPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mSecondHandPaint.setStyle(Paint.Style.FILL);
mSecondHandPaint.setColor(mLightColor);
mScaleLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mScaleLinePaint.setStyle(Paint.Style.STROKE);
mScaleLinePaint.setColor(mBackgroundColor);
mScaleArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mScaleArcPaint.setStyle(Paint.Style.STROKE);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(mDarkColor);
mTextPaint.setTextSize(mTextSize);
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(mStrokeWidth);
mCirclePaint.setColor(mDarkColor);
mTextRect = new Rect();
mCircleRectF = new RectF();
mScaleArcF = new RectF();
mHourHandPath = new Path();
mMinuteHandPath = new Path();
mSecondHandPath = new Path();
mGradientMatrix = new Matrix();
mCameraMatrix = new Matrix();
mCamera = new Camera();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureDimension(widthMeasureSpec), measureDimension(heightMeasureSpec));
}
private int measureDimension(int measureSpec) {
int result;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
if (mode == MeasureSpec.EXACTLY) {
result = size;
} else {
result = 800;
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
}
return result;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom()) / 2;
mDefaultPadding = 0.12f * mRadius;
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);
mMaxCanvasTranslate = 0.02f * mRadius;
mSweepGradient = new SweepGradient(w / 2, h / 2, new int[]{mLightColor, mDarkColor}, new float[]{0.75f, 1});
}
@Override
protected void onDraw(Canvas canvas) {
mCanvas = canvas;
setCameraRotate();
getTimeDegree();
drawTimeText();
drawScaleLine();
drawSecondHand();
drawHourHand();
drawMinuteHand();
invalidate();
}
private void drawMinuteHand() {
mCanvas.save();
mCanvas.translate(mCanvasTranslateX * 2f, mCanvasTranslateY * 2f);
mCanvas.rotate(mMinuteDegree, getWidth() / 2, getHeight() / 2);
mMinuteHandPath.reset();
float offset = mPaddingTop + mTextRect.height() / 2;
mMinuteHandPath.moveTo(getWidth() / 2 - 0.01f * mRadius, getHeight() / 2 - 0.03f * mRadius);
mMinuteHandPath.lineTo(getWidth() / 2 - 0.008f * mRadius, offset + 0.365f * mRadius);
mMinuteHandPath.quadTo(getWidth() / 2, offset + 0.345f * mRadius,
getWidth() / 2 + 0.008f * mRadius, offset + 0.365f * mRadius);
mMinuteHandPath.lineTo(getWidth() / 2 + 0.01f * mRadius, getHeight() / 2 - 0.03f * mRadius);
mMinuteHandPath.close();
mMinuteHandPaint.setStyle(Paint.Style.FILL);
mCanvas.drawPath(mMinuteHandPath, mMinuteHandPaint);
mCircleRectF.set(getWidth() / 2 - 0.03f * mRadius, getHeight() / 2 - 0.03f * mRadius,
getWidth() / 2 + 0.03f * mRadius, getHeight() / 2 + 0.03f * mRadius);
mMinuteHandPaint.setStyle(Paint.Style.STROKE);
mMinuteHandPaint.setStrokeWidth(0.02f * mRadius);
mCanvas.drawArc(mCircleRectF, 0, 360, false, mMinuteHandPaint);
mCanvas.restore();
}
private void drawHourHand() {
mCanvas.save();
mCanvas.translate(mCanvasTranslateX * 1.2f, mCanvasTranslateY * 1.2f);
mCanvas.rotate(mHourDegree, getWidth() / 2, getHeight() / 2);
mHourHandPath.reset();
float offset = mPaddingTop + mTextRect.height() / 2;
mHourHandPath.moveTo(getWidth() / 2 - 0.018f * mRadius, getHeight() / 2 - 0.03f * mRadius);
mHourHandPath.lineTo(getWidth() / 2 - 0.009f * mRadius, offset + 0.48f * mRadius);
mHourHandPath.quadTo(getWidth() / 2, offset + 0.46f * mRadius,
getWidth() / 2 + 0.009f * mRadius, offset + 0.48f * mRadius);
mHourHandPath.lineTo(getWidth() / 2 + 0.018f * mRadius, getHeight() / 2 - 0.03f * mRadius);
mHourHandPath.close();
mHourHandPaint.setStyle(Paint.Style.FILL);
mCanvas.drawPath(mHourHandPath, mHourHandPaint);
mCircleRectF.set(getWidth() / 2 - 0.03f * mRadius, getHeight() / 2 - 0.03f * mRadius,
getWidth() / 2 + 0.03f * mRadius, getHeight() / 2 + 0.03f * mRadius);
mHourHandPaint.setStyle(Paint.Style.STROKE);
mHourHandPaint.setStrokeWidth(0.01f * mRadius);
mCanvas.drawArc(mCircleRectF, 0, 360, false, mHourHandPaint);
mCanvas.restore();
}
private void drawSecondHand() {
mCanvas.save();
mCanvas.translate(mCanvasTranslateX, mCanvasTranslateY);
mCanvas.rotate(mSecondDegree, getWidth() / 2, getHeight() / 2);
mSecondHandPath.reset();
float offset = mPaddingTop + mTextRect.height() / 2;
mSecondHandPath.moveTo(getWidth() / 2, offset + 0.26f * mRadius);
mSecondHandPath.lineTo(getWidth() / 2 - 0.05f * mRadius, offset + 0.34f * mRadius);
mSecondHandPath.lineTo(getWidth() / 2 + 0.05f * mRadius, offset + 0.34f * mRadius);
mSecondHandPath.close();
mSecondHandPaint.setColor(mLightColor);
mCanvas.drawPath(mSecondHandPath, mSecondHandPaint);
mCanvas.restore();
}
private void drawScaleLine() {
mCanvas.save();
mCanvas.translate(mCanvasTranslateX, mCanvasTranslateY);
mScaleArcF.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);
mGradientMatrix.setRotate(mSecondDegree - 90, getWidth() / 2, getHeight() / 2);
mSweepGradient.setLocalMatrix(mGradientMatrix);
mScaleArcPaint.setShader(mSweepGradient);
mCanvas.drawArc(mScaleArcF, 0, 360, false, mScaleArcPaint);
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();
}
private void drawTimeText() {
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);
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);
}
}
private void getTimeDegree() {
Date date = new Date();
long time = date.getTime();
float milliSecond = time % 1000;
float second = date.getSeconds() + milliSecond / 1000;
float minute = date.getMinutes() + second / 60;
float hour = date.getHours() + minute / 60;
mSecondDegree = second / 60 * 360;
mMinuteDegree = minute / 60 * 360;
mHourDegree = hour / 12 * 360;
}
protected void getCameraRotate(MotionEvent event) {
float rotateX = -(event.getY() - getHeight() / 2);
float rotateY = event.getX() - getWidth() / 2;
float[] percentArr = getPercent(rotateX, rotateY);
mCameraRotateX = percentArr[0] * mMaxCameraRotate;
mCameraRotateY = percentArr[1] * mMaxCameraRotate;
}
private float[] getPercent(float rotateX, float rotateY) {
float[] percentArr = new float[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;
}
percentArr[0] = percentX;
percentArr[1] = percentY;
return percentArr;
}
private void setCameraRotate() {
mCameraMatrix.reset();
mCamera.save();
mCamera.rotateX(mCameraRotateX);
mCamera.rotateY(mCameraRotateY);
mCamera.getMatrix(mCameraMatrix);
mCamera.restore();
mCameraMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
mCameraMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
mCanvas.concat(mCameraMatrix);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mShakeAnim != null && mShakeAnim.isRunning()) {
mShakeAnim.cancel();
}
getCameraRotate(event);
getCanvasTranslate(event);
break;
case MotionEvent.ACTION_MOVE:
getCameraRotate(event);
getCanvasTranslate(event);
break;
case MotionEvent.ACTION_UP:
startShakeAnim();
break;
}
return true;
}
private void getCanvasTranslate(MotionEvent event) {
float translateX = (event.getX() - getWidth() / 2);
float translateY = (event.getY() - getHeight() / 2);
//求出此时位移的大小与半径之比
float[] percentArr = getPercent(translateX, translateY);
//最终位移的大小按比例匀称改变
mCanvasTranslateX = percentArr[0] * mMaxCanvasTranslate;
mCanvasTranslateY = percentArr[1] * mMaxCanvasTranslate;
}
private void startShakeAnim() {
final String cameraRotateXName = "cameraRotateX";
final String cameraRotateYName = "cameraRotateY";
final String canvasTranslateXName = "canvasTranslateX";
final String canvasTranslateYName = "canvasTranslateY";
PropertyValuesHolder cameraRotateXHolder =
PropertyValuesHolder.ofFloat(cameraRotateXName, mCameraRotateX, 0);
PropertyValuesHolder cameraRotateYHolder =
PropertyValuesHolder.ofFloat(cameraRotateYName, mCameraRotateY, 0);
PropertyValuesHolder canvasTranslateXHolder =
PropertyValuesHolder.ofFloat(canvasTranslateXName, mCanvasTranslateX, 0);
PropertyValuesHolder canvasTranslateYHolder =
PropertyValuesHolder.ofFloat(canvasTranslateYName, mCanvasTranslateY, 0);
mShakeAnim = ValueAnimator.ofPropertyValuesHolder(cameraRotateXHolder,
cameraRotateYHolder, canvasTranslateXHolder, canvasTranslateYHolder);
mShakeAnim.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
//http://inloop.github.io/interpolator/
float f = 0.571429f;
return (float) (Math.pow(2, -2 * input) * Math.sin((input - f / 4) * (2 * Math.PI) / f) + 1);
}
});
mShakeAnim.setDuration(1000);
mShakeAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCameraRotateX = (float) animation.getAnimatedValue(cameraRotateXName);
mCameraRotateY = (float) animation.getAnimatedValue(cameraRotateYName);
mCanvasTranslateX = (float) animation.getAnimatedValue(canvasTranslateXName);
mCanvasTranslateY = (float) animation.getAnimatedValue(canvasTranslateYName);
}
});
mShakeAnim.start();
}
}
还有自定义的属性资源文件attrs:
END