本文为原创文章,转载请注明出处,原创不易,且转且珍惜
1. 前言
几年前做过一个类似MIUI时钟的效果,逻辑比较简单,虽然MIUI经过几年的系统迭代,时钟早已不是这个效果,但当时做需求时涉及到一些canvas绘制的技巧,想来还是有些意思,并且这些思路如果最近把代码翻了出来,回顾一下当时的想法和策略。
2.效果
俗话说的好,没有图你说个XX,先把效果图发出来看一下:
3. 用到的知识点
3.1. Canvas#saveLayer和Canvas#restore
Canvas 在一般的情况下可以看作是一张画布,所有的绘图操作如drawBitmap, drawCircle都发生在这张画布上,这张画板还定义了一些属性比如Matrix,颜色等等。但是如果需要实现一些相对复杂的绘图操作,比如多层动画,地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)。Canvas提供了图层(Layer)支持,缺省情况可以看作是只有一个图层Layer。如果需要按层次来绘图,Android的Canvas可以使用SaveLayerXXX, Restore 来创建一些中间层,对于这些Layer是按照“栈结构“来管理的:
创建一个新的Layer到“栈”中,可以使用saveLayer, savaLayerAlpha, 从“栈”中推出一个Layer,可以使用restore,restoreToCount。但Layer入栈时,后续的DrawXXX操作都发生在这个Layer上,而Layer退栈时,就会把本层绘制的图像“绘制”到上层或是Canvas上,在复制Layer到Canvas上时,可以指定Layer的透明度(Layer),这是在创建Layer时指定的:public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)本例Layers 介绍了图层的基本用法:Canvas可以看做是由两个图层(Layer)构成的。
3.2 Canvas转换
Canvas的转换主要有旋转、缩放、扭曲、平移、裁剪等,本文主要用到的是旋转(rotate)
4. 思路
4.1 总体思路
我们默认Canvas和系统坐标系是对应的,没有发生任何旋转缩放,因此,初始状态将每个元素绘制在View的12点钟方向,绘制每个元素时都要新建图层,待绘制完成后将图层旋转至当前时间所指示的方向即可。
4.2 当前时间角度的计算
一个圆的角度是360度,我们默认12点钟为0度,那么当前时针、分针、秒针所旋转的角度分别为:
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));
4.3 秒针(三角)的初始化
这里秒针的实现是使用的Canvas#drawPath,在drawPath之前,我们需要穿件一个Path使其成为一个封闭的三角形,Canvas提供了moveTo、lineTo、colse方法帮我们实现这个效果:
//初始化三角, 该三角形为底边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(); // 使这些点构成封闭的多边形
初始化完成后,秒针针头指向12点钟方向
4.4 当前时间的绘制
这里用到的是图层的创建和保存,以及Canvs#save方法,首先绘制秒针和中间圆环:
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); // 恢复图层
注意,调用restoreToCount或restore后,会将图层恢复至save或saveLayer之前的状态
秒针和中间圆环绘制完成后,绘制时针和分针,操作同上:
//画时针
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);
4.5 周边刻度的绘制
4.5.1 刻度绘制
圆环周边的刻度共有180个,我们需要新建一个图层,旋转180次,每次旋转2度即可:
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);
}
4.5.2 拖尾效果
刻度有一个从透明度255到透明度120的渐变拖尾效果,因此,我们需要逆时针绘制刻度,并且每次绘制时将透明度 减三,直到透明度到达120:
//画刻度
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);
5 动效
注意看效果图,秒针的运动比较圆润丝滑,而刻度的运动时从上一个跳到下一个,有一种秒针指引刻度运动的感觉,因此我们需要定义两个动画,一个秒针动画,使用float值,一个刻度动画,使用int值,这两个动画选择其中一个监听动画变化即可
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 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;
}
}
}