This article is the original article, reproduced please indicate the source, the original is not easy, and turn and cherish
1. Introduction
A few years ago had a similar effect of beautiful MIUI clock, logic is simple, although beautiful MIUI iteration, after several years of system clock is not the effect already, but at the time when making requirements involved some canvas rendering techniques, want some meaning, and these ideas if the latest code to turn over, review the idea and strategy.
Effect of 2.
As the saying goes, there is no graph you say XX, first send out the effect map to see:
#3
3.1. Canvas# saveLayer and Canvas# restore
Canvas can be regarded as a Canvas in general. All drawing operations, such as drawBitmap and drawCircle, take place on this Canvas. This Canvas also defines some properties, such as Matrix and color. However, if you need to achieve some relatively complex drawing operations, such as multi-layer animation, map (map can have multiple map layers superimposed, such as: political district layer, road layer, interest point layer). Canvas provides Layer support, which can be regarded as only one Layer by default. If you need to draw layers, the Android Canvas can use SaveLayerXXX, Restore to create intermediate layers that are managed according to the stack structure:
Create a new Layer on the stack by using saveLayer, savaLayerAlpha, and push aLayer from the stack by using Restore,restoreToCount. However, when Layer is stacked, subsequent DrawXXX operations will take place on this Layer, and when Layer is unstacked, the image drawn by this Layer will be “drawn” to the upper Layer or Canvas. When copying Layer to Canvas, the transparency of Layer can be specified. This is specified when creating Layer: public int saveLayerAlpha(RectF Bounds, int alpha, int saveFlags) Canvas can be regarded as composed of two layers.
3.2 Canvas transformations
Canvas conversions mainly include rotation, zooming, distortion, translation and clipping. This paper mainly uses rotate.
4. The train of thought
4.1 General Ideas
By default, Canvas is corresponding to the system coordinate system without any rotation and scaling. Therefore, in the initial state, each element is drawn in the 12 o ‘clock direction of View. When drawing each element, a new layer should be created, and after drawing, the layer should be rotated to the direction indicated by the current time.
4.2 Calculation of current time Angle
The Angle of a circle is 360 degrees. By default, 12 o ‘clock is 0 degrees. Then the current Angle of the hour hand, minute hand and second hand is:
mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360/12); mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360/60); mSecondStartAngle = Math.round((Calendar.getInstance().get(Calendar.SECOND) + Calendar.getInstance().get(Calendar.MILLISECOND) / Constants.SECOND) * (360 / 60));Copy the code
4.3 Initialization of second hand (triangle)
The second hand is implemented using Canvas#drawPath. Before drawPath, we need to draw a Path to make it a closed triangle. Canvas provides methods such as moveTo, lineTo, and Colse to achieve this effect:
MTriangle = new Path(); mTriangle = new Path(); mTriangle.moveTo(mGraduationPoint.x , mGraduationPoint.y + 70); // This point is the starting point of the polygon mgraduationPoint.x-20, mgraduationPoint.y + 97; mTriangle.lineTo(mGraduationPoint.x + 20, mGraduationPoint.y + 97); mTriangle.close(); // Make these points form closed polygonsCopy the code
After initialization, the second hand needle points at 12 o ‘clock
4.4 Drawing the current time
Here we use the layer creation and save, and the Canvs#save method. First draw the second hand and the middle ring:
int layerCount = canvas.saveLayer(0 , 0 , canvas.getWidth() , canvas.getHeight() , mDefaultPaint , Canvas.ALL_SAVE_FLAG); Log.d("zyl", "sanjiaolayerCount = " + layerCount); Rotate (mClockAngle + mSecondStartAngle, mCenterPoint. X, mCenterPoint. Y); rotate(mClockAngle + mSecondStartAngle, mCenterPoint. DrawPath (mTriangle, mPaint); DrawBitmap (mCircleBitmap, NULL, mDstCircleRect, mDefaultPaint); canvas.restoreToCount(layerCount); // Restore the layerCopy the code
Note that a call to restoreToCount or Restore restores the layer to the state before Save or saveLayer
After the second hand and the middle ring are drawn, the hour hand and minute hand are drawn. The operation is the same as above:
// Clockwise layerCount = Canvas.savelayer (0, 0, canvas.getwidth (), canvas.getheight (), mDefaultPaint, Canvas.all_save_flag); Log.d("zyl", "shizhenLayerCount = "+ layerCount); canvas.rotate(mHourAngle , mCenterPoint.x , mCenterPoint.y); canvas.drawBitmap(mHourBitmap , null , mDstHourRect , mDefaultPaint); canvas.restoreToCount(layerCount); // Draw minute hand layerCount = Canvas.savelayer (0, 0, canvas.getwidth (), canvas.getheight (), mDefaultPaint, Canvas.all_save_flag); Log.d("zyl", "fenzhenlayerCount = " + layerCount); canvas.rotate(mMinuteAngle , mCenterPoint.x , mCenterPoint.y); canvas.drawBitmap(mMinuteBitmap , null , mDstMinuteRect , mDefaultPaint); canvas.restoreToCount(layerCount);Copy the code
4.5 Drawing of peripheral scale
4.5.1 Scale drawing
There are 180 scales around the ring. We need to create a new layer and rotate it 180 times by 2 degrees each time:
Log.d("zyl", "fenzhenlayerCount = " + layerCount);
canvas.rotate(mSecondAngle + mSecondStartAngle , mCenterPoint.x, mCenterPoint.y);
for (int i = 0; i < GRADUATION_COUNT; i++) {
canvas.drawLine(mGraduationPoint.x, mGraduationPoint.y + 5, mGraduationPoint.x, mGraduationPoint.y + GRADUATION_LENGTH, mGraduationPaint);
canvas.rotate(-PER_GRADUATION_ANGLE, mCenterPoint.x, mCenterPoint.y);
}
Copy the code
4.5.2 Trailing effect
The scale has a trailing gradient from opacity 255 to opacity 120, so we need to draw the scale counterclockwise and decrease the opacity by three each time until the opacity reaches 120:
// Scale layerCount = Canvas.savelayer (0, 0, canvas.getwidth (), canvas.getheight (), mDefaultPaint, Canvas.all_save_flag); Log.d("zyl", "fenzhenlayerCount = " + layerCount); canvas.rotate(mSecondAngle + mSecondStartAngle , mCenterPoint.x, mCenterPoint.y); for (int i = 0; i < GRADUATION_COUNT; i++) { int alpha = 255 - i * 3; if (alpha > 120) { mGraduationPaint.setAlpha(alpha); } canvas.drawLine(mGraduationPoint.x, mGraduationPoint.y + 5, mGraduationPoint.x, mGraduationPoint.y + GRADUATION_LENGTH, mGraduationPaint); canvas.rotate(-PER_GRADUATION_ANGLE, mCenterPoint.x, mCenterPoint.y); } canvas.restoreToCount(layerCount);Copy the code
5 dynamic effect
Look at rendering, the movement of the second hand is fruity and silky, and scale of movement from a jump to the next, there is a second hand guide scale movement feeling, so we need to define two animation, a second hand animation, use float values, a calibration animation, use int values, choose one to monitor these two animation animation changes
Public void startAnimation() {// mClockAnimator = valueAnimator.offloat (0, GRADUATION_COUNT); mClockAnimator.setDuration(Constants.MINUTE); mClockAnimator.setInterpolator(new LinearInterpolator()); mClockAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mClockAngle = (float) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE; }}); mClockAnimator.setRepeatCount(ValueAnimator.INFINITE); // mSecondAnimator = ValueAnimator.ofint (0, GRADUATION_COUNT); mSecondAnimator.setDuration(Constants.MINUTE); mSecondAnimator.setInterpolator(new LinearInterpolator()); mSecondAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mSecondAngle = (int) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE; mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360/12); mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360/60); Log.d("zyl", "second = " + Calendar.getInstance().get(Calendar.SECOND)); Log.d("zyl", "mMinuteAngle = " + mMinuteAngle); invalidate(); }}); mSecondAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360/12); mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360/60); mSecondStartAngle = Math.round((Calendar.getInstance().get(Calendar.SECOND) + Calendar.getInstance().get(Calendar.MILLISECOND) / Constants.SECOND) * (360 / 60)); } @Override public void onAnimationEnd(Animator animator) { } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); mSecondAnimator.setRepeatCount(ValueAnimator.INFINITE); mSecondAnimator.start(); mClockAnimator.start(); }Copy the code
Full version code:
public class MIUIClock extends View {
private Paint mPaint;
private Context mContext;
private Paint mDefaultPaint;
private Paint mGraduationPaint;
private Rect mContentRect;
private Path mTriangle;
private Point mGraduationPoint;
private Point mCenterPoint;
private Rect mDstCircleRect; //时钟中心圆圈所在位置
private Rect mDstHourRect; //时针所在位置
private Rect mDstMinuteRect; //分针所在位置
private ValueAnimator mClockAnimator;
private ValueAnimator mSecondAnimator;
private float mSecondStartAngle; //圆环的起始角度
private float mClockAngle; //三角指针角度
private int mSecondAngle; //圆环角度
private float mHourAngle; //时针角度
private float mMinuteAngle; //分针角度
private static final int GRADUATION_LENGTH = 50; //圆环刻度长度
private static final int GRADUATION_COUNT = 180; //一圈圆环刻度的数量
private static final int ROUND_ANGLE = 360; //圆一周的角度
private static final int PER_GRADUATION_ANGLE = ROUND_ANGLE / GRADUATION_COUNT; //每个刻度的角度
private Bitmap mCircleBitmap; //时钟中心的圆圈
private Bitmap mHourBitmap; //时针
private Bitmap mMinuteBitmap; //分针
public MIUIClock(Context context) {
super(context);
init(context);
}
public MIUIClock(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mDefaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAlpha(120);
mGraduationPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mGraduationPaint.setColor(Color.WHITE);
mGraduationPaint.setStrokeWidth(4);
mGraduationPaint.setStrokeCap(Paint.Cap.ROUND);
mCircleBitmap = BitmapFactory.decodeResource(mContext.getResources() , R.mipmap.ic_circle);
mHourBitmap = BitmapFactory.decodeResource(mContext.getResources() , R.mipmap.ic_hour);
mMinuteBitmap = BitmapFactory.decodeResource(mContext.getResources() , R.mipmap.ic_minute);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mContentRect = new Rect(0 , 0 , w, h); // 本View内容区域
mGraduationPoint = new Point(w /2 , 0); // 圆圈刻度绘制的参照位置
mCenterPoint = new Point(w /2 , h /2); // 本View中心点位置
//初始化三角, 该三角形为底边40, 高27的等腰三角形
mTriangle = new Path();
mTriangle.moveTo(mGraduationPoint.x , mGraduationPoint.y + 70);// 此点为多边形的起点
mTriangle.lineTo(mGraduationPoint.x - 20, mGraduationPoint.y + 97);
mTriangle.lineTo(mGraduationPoint.x + 20, mGraduationPoint.y + 97);
mTriangle.close(); // 使这些点构成封闭的多边形
//初始化circle所在位置, 将圆圈置于View 中心
int circleWidth = mCircleBitmap.getWidth();
int circleHeight = mCircleBitmap.getHeight();
mDstCircleRect = new Rect(mCenterPoint.x - circleWidth /2 , mCenterPoint.y - circleHeight/2 ,
mCenterPoint.x + circleWidth /2 , mCenterPoint.y + circleHeight /2);
//初始化时针所在位置
int hourWidth = mHourBitmap.getWidth();
int hourHeight = mHourBitmap.getHeight();
mDstHourRect = new Rect(mCenterPoint.x - hourWidth / 2 , mCenterPoint.y - hourHeight - circleHeight / 2 - 5,
mCenterPoint.x + hourWidth / 2, mCenterPoint.y - circleHeight / 2 - 5);
//初始化分针所在位置
int minuteWidth = mMinuteBitmap.getWidth();
int minuteHeight = mMinuteBitmap.getHeight();
mDstMinuteRect = new Rect(mCenterPoint.x - minuteWidth / 2 , mCenterPoint.y - minuteHeight - circleHeight / 2 - 5,
mCenterPoint.x + minuteWidth / 2 , mCenterPoint.y - circleHeight / 2 - 5);
}
@Override
protected void onDraw(Canvas canvas) {
int layerCount = canvas.saveLayer(0 , 0 , canvas.getWidth() , canvas.getHeight() , mDefaultPaint , Canvas.ALL_SAVE_FLAG);
Log.d("zyl", "sanjiaolayerCount = " + layerCount);
canvas.rotate(mClockAngle + mSecondStartAngle , mCenterPoint.x , mCenterPoint.y);
//画三角
canvas.drawPath(mTriangle, mPaint);
//画中心的圆圈
canvas.drawBitmap(mCircleBitmap , null , mDstCircleRect , mDefaultPaint);
canvas.restoreToCount(layerCount);
//画时针
layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG); //新建图层
Log.d("zyl", "shizhenLayerCount = " + layerCount);
canvas.rotate(mHourAngle , mCenterPoint.x , mCenterPoint.y);
canvas.drawBitmap(mHourBitmap , null , mDstHourRect , mDefaultPaint);
canvas.restoreToCount(layerCount);
//画分针
layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG);
Log.d("zyl", "fenzhenlayerCount = " + layerCount);
canvas.rotate(mMinuteAngle , mCenterPoint.x , mCenterPoint.y);
canvas.drawBitmap(mMinuteBitmap , null , mDstMinuteRect , mDefaultPaint);
canvas.restoreToCount(layerCount);
//画刻度
layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG);
Log.d("zyl", "fenzhenlayerCount = " + layerCount);
canvas.rotate(mSecondAngle + mSecondStartAngle , mCenterPoint.x, mCenterPoint.y);
for (int i = 0; i < GRADUATION_COUNT; i++) {
int alpha = 255 - i * 3;
if (alpha > 120) {
mGraduationPaint.setAlpha(alpha);
}
canvas.drawLine(mGraduationPoint.x, mGraduationPoint.y + 5, mGraduationPoint.x, mGraduationPoint.y + GRADUATION_LENGTH, mGraduationPaint);
canvas.rotate(-PER_GRADUATION_ANGLE, mCenterPoint.x, mCenterPoint.y);
}
canvas.restoreToCount(layerCount);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec , heightMeasureSpec);
}
public void startAnimation() {
//三角刻度动画
mClockAnimator = ValueAnimator.ofFloat(0 , GRADUATION_COUNT);
mClockAnimator.setDuration(Constants.MINUTE);
mClockAnimator.setInterpolator(new LinearInterpolator());
mClockAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mClockAngle = (float) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE;
}
});
mClockAnimator.setRepeatCount(ValueAnimator.INFINITE);
//圆圈刻度动画
mSecondAnimator = ValueAnimator.ofInt(0 , GRADUATION_COUNT);
mSecondAnimator.setDuration(Constants.MINUTE);
mSecondAnimator.setInterpolator(new LinearInterpolator());
mSecondAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mSecondAngle = (int) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE;
mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
Log.d("zyl", "second = " + Calendar.getInstance().get(Calendar.SECOND));
Log.d("zyl", "mMinuteAngle = " + mMinuteAngle);
invalidate();
}
});
mSecondAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
mSecondStartAngle = Math.round((Calendar.getInstance().get(Calendar.SECOND) + Calendar.getInstance().get(Calendar.MILLISECOND) / Constants.SECOND) * (360 / 60));
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mSecondAnimator.setRepeatCount(ValueAnimator.INFINITE);
mSecondAnimator.start();
mClockAnimator.start();
}
public void cancelAnimation() {
if (mClockAnimator != null) {
mClockAnimator.removeAllUpdateListeners();
mClockAnimator.removeAllListeners();
mClockAnimator.cancel();
mClockAnimator = null;
}
if (mSecondAnimator != null) {
mSecondAnimator.removeAllUpdateListeners();
mSecondAnimator.removeAllListeners();
mSecondAnimator.cancel();
mSecondAnimator = null;
}
}
}
Copy the code