package gastudio.clock.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import java.util.Calendar;
import gastudio.clock.R;
import gastudio.clock.utils.Constants;
import gastudio.clock.utils.UiUtils;
public class ClockViewByPath extends View {
private static final int DEFAULT_CLOCK_ANIMATION_DURATION = Constants.MINUTE;
private static final int FULL_ANGLE = 360;
private static final int SECOND_PER_MINUTE = 60;
private static final int DEFAULT_TOTAL_CLOCK_SCALE_LINE_NUM = 120;
private static final int DEFAULT_CLOCK_VIEW_WIDTH = 260;
private static final int ANGLE_PER_SECOND = FULL_ANGLE / SECOND_PER_MINUTE;
private static final int ANGLE_PER_SCALE = FULL_ANGLE / DEFAULT_TOTAL_CLOCK_SCALE_LINE_NUM;
private static final int DEFAULT_CLOCK_SCALE_LINE_COLOR = Color.WHITE;
private static final int DEFAULT_CLOCK_SCALE_LINE_WIDTH = 2;
private static final int DEFAULT_CLOCK_SCALE_LINE_HEIGHT = 14;
private static final int ADJUST_CLOCK_SCALE_LINE_START_X = 1;
private static final int DEFAULT_DIGITAL_TIME_TEXT_COLOR = Color.WHITE;
private static final int DEFAULT_DIGITAL_TIME_TEXT_SIZE = 60;
private static final String DEFAULT_DEFAULT_DIGITAL_TIME_TEXT = "00:00";
private static final int DEFAULT_CLOCK_POINT_COLOR = Color.RED;
private static final int DEFAULT_CLOCK_POINT_RADIUS = 6;
private static final float[] CLOCK_SCALE_LINE_BASE_LEN_ARRAY = new float[]{
1F, 1.1F, 1.21F, 1.32F, 1.452F,
1.551F, 1.6827F, 1.75F, 1.6827F, 1.551F,
1.452F, 1.32F, 1.21F, 1.1F, 1F};
// This is max in CLOCK_SCALE_LINE_BASE_LEN_ARRAY
private static final float RATIO_OF_MAX_HEIGTH_TO_NORMAL_HEIGHT = 1.75F;
private int mClockScaleLineWidth;
private int mClockScaleLineHeight;
private int mClockScaleLineMaxHeight;
private int mClockScaleLineColor;
private int mAdjustClockScaleLineStartX;
private int mClockViewCenterX;
private int mClockViewCenterY;
private int mClockMaskRadius;
private RectF mClockViewRectF;
private Path mClockMaskPath;
private float mClockMaskAdjustAngle;
private int mClockPointRadius;
private int mClockPointColor;
private int mClockPointCenterX;
private int mClockPointCenterY;
private int mDigitalTimeTextStartX;
private int mDigitalTimeTextStartY;
private int mDigitalTimeTextSize;
private int mDigitalTimeTextColor;
private Rect mDigitalTimeTextRect;
private String mLastDigitalTimeStr;
private Paint mPaint;
private Xfermode mXfermode;
private ValueAnimator mClockAnimator;
private float mNowClockAngle;
private float mInitClockAngle;
private long mLastTimeMillis;
private Calendar mCalendar;
public ClockViewByPath(Context context) {
this(context, null);
}
public ClockViewByPath(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ClockViewByPath(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ClockViewByPath(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ClockView);
mDigitalTimeTextSize = a.getDimensionPixelSize(R.styleable.ClockView_timeTextSize,
UiUtils.dipToPx(context, DEFAULT_DIGITAL_TIME_TEXT_SIZE));
mDigitalTimeTextColor = a.getColor(R.styleable.ClockView_timeTextSize,
DEFAULT_DIGITAL_TIME_TEXT_COLOR);
mClockPointRadius = a.getDimensionPixelSize(R.styleable.ClockView_pointRadius,
UiUtils.dipToPx(context, DEFAULT_CLOCK_POINT_RADIUS));
mClockPointColor = a.getColor(R.styleable.ClockView_pointColor,
DEFAULT_CLOCK_POINT_COLOR);
mClockScaleLineWidth = a.getDimensionPixelSize(R.styleable.ClockView_timeScaleWidth,
UiUtils.dipToPx(context, DEFAULT_CLOCK_SCALE_LINE_WIDTH));
mClockScaleLineHeight = a.getDimensionPixelSize(R.styleable.ClockView_timeScaleHeight,
UiUtils.dipToPx(context, DEFAULT_CLOCK_SCALE_LINE_HEIGHT));
mClockScaleLineMaxHeight = (int) (RATIO_OF_MAX_HEIGTH_TO_NORMAL_HEIGHT * mClockScaleLineHeight);
mClockScaleLineColor = a.getDimensionPixelSize(R.styleable.ClockView_timeScaleColor,
UiUtils.dipToPx(context, DEFAULT_CLOCK_SCALE_LINE_COLOR));
a.recycle();
init();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int len = w > h ? h : w;
mClockViewRectF.set(0, 0, len, len);
mClockViewRectF.offset((w - len) / 2, (h - len) / 2);
mClockViewCenterX = (int) mClockViewRectF.centerX();
mClockViewCenterY = (int) mClockViewRectF.centerY();
mDigitalTimeTextStartX = mClockViewCenterX - mDigitalTimeTextRect.left - mDigitalTimeTextRect.width() / 2;
mDigitalTimeTextStartY = mClockViewCenterY - mDigitalTimeTextRect.top - mDigitalTimeTextRect.height() / 2;
mClockPointCenterX = mClockViewCenterX;
mClockPointCenterY = (int) (mClockViewRectF.top + mAdjustClockScaleLineStartX
+ mClockScaleLineMaxHeight + mClockPointRadius * 2);
mClockMaskRadius = (int) (mClockViewRectF.width() / 2 - mClockScaleLineMaxHeight);
generateMaskPath();
mClockMaskAdjustAngle = (float) (CLOCK_SCALE_LINE_BASE_LEN_ARRAY.length + 1) / 2 * ANGLE_PER_SCALE;
}
private void generateMaskPath() {
Log.i("zyl log 0223","generateMaskPath");
Log.i("zyl log 0223","mClockViewCenterX = " + mClockViewCenterX);//260
Log.i("zyl log 0223","mClockViewCenterY = " + mClockViewCenterY);//260
Log.i("zyl log 0223","mClockViewCenterY - mClockMaskRadius - mClockScaleLineHeight = " + (mClockViewCenterY - mClockMaskRadius - mClockScaleLineHeight));
Point point = new Point(mClockViewCenterX, mClockViewCenterY - mClockMaskRadius - mClockScaleLineHeight);//21
mClockMaskPath.moveTo(point.x, point.y);
// Generate contour of the special clock scale lines
int arrayLen = CLOCK_SCALE_LINE_BASE_LEN_ARRAY.length;//15
for (int index = 0; index < arrayLen; index++) {
calculateNextPoint(point, CLOCK_SCALE_LINE_BASE_LEN_ARRAY[index],
(float) Math.toRadians(ANGLE_PER_SCALE * (index + 1)));
mClockMaskPath.lineTo(point.x, point.y);
}
// Generate contour of the normal clock scale lines
int insertLen = mClockScaleLineMaxHeight - mClockScaleLineHeight;
RectF cycleRectF = new RectF(mClockViewRectF);
cycleRectF.inset(insertLen, insertLen);//03.10 ???
//If dx is positive, then the sides are moved inwards, making the rectangle narrower
mClockMaskPath.arcTo(cycleRectF, arrayLen * ANGLE_PER_SCALE - 90,//-45(对于圆心找-45度方位,与矩形交点即为划线起始点)
(DEFAULT_TOTAL_CLOCK_SCALE_LINE_NUM - arrayLen) * ANGLE_PER_SCALE);//315,弧线划过的角度(相对于圆心)
}
private void calculateNextPoint(Point point, float scale, float angle) {
Log.i("zyl log 0228","mClockMaskRadius = " + mClockMaskRadius);//211
Log.i("zyl log 0228","mClockScaleLineHeight = " + mClockScaleLineHeight);//28
float originLen = mClockMaskRadius + mClockScaleLineHeight;
float adjustLen = mClockMaskRadius + mClockScaleLineHeight * scale;
Log.i("zyl log 0228","mClockViewCenterX + (int) (adjustLen * Math.sin(angle) = " + (mClockViewCenterX + (int) (adjustLen * Math.sin(angle) ) ) ) ;
Log.i("zyl log 0228","mClockViewCenterY - mClockMaskRadius - mClockScaleLineHeight\n" +
" + (int) (-adjustLen * Math.cos(angle) + originLen) = " + (mClockViewCenterY - mClockMaskRadius - mClockScaleLineHeight
+ (int) (-adjustLen * Math.cos(angle) + originLen)));
point.set(mClockViewCenterX + (int) (adjustLen * Math.sin(angle)),
mClockViewCenterY - mClockMaskRadius - mClockScaleLineHeight
+ (int) (-adjustLen * Math.cos(angle) + originLen));
}
private void init() {
mClockViewRectF = new RectF();
mDigitalTimeTextRect = new Rect();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(mDigitalTimeTextSize);
mPaint.getTextBounds(DEFAULT_DEFAULT_DIGITAL_TIME_TEXT, 0,
DEFAULT_DEFAULT_DIGITAL_TIME_TEXT.length(), mDigitalTimeTextRect);
mPaint.setStrokeWidth(mClockScaleLineWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);//取下层绘制非交集部分(大小正方形中套圆)
mAdjustClockScaleLineStartX = UiUtils.dipToPx(getContext(), ADJUST_CLOCK_SCALE_LINE_START_X);
mClockMaskPath = new Path();
mCalendar = Calendar.getInstance();
mLastDigitalTimeStr = String.format("%02d:%02d", mCalendar.get(Calendar.HOUR),
mCalendar.get(Calendar.MINUTE));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode != MeasureSpec.EXACTLY) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
UiUtils.dipToPx(getContext(), DEFAULT_CLOCK_VIEW_WIDTH), MeasureSpec.EXACTLY);
}
if (heightSpecMode != MeasureSpec.EXACTLY) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
UiUtils.dipToPx(getContext(), DEFAULT_CLOCK_VIEW_WIDTH), MeasureSpec.EXACTLY);
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
//03.08:
// 1.Canvas是一个很虚幻的概念,相当于一个透明图层(用过PS的同学应该都知道),每次Canvas画图时(即调用Draw系列函数),
// 都会产生一个透明图层,然后在这个图层上画图,画完之后覆盖在屏幕上显示
// 2.在画源图像时,会把之前画布上所有的内容都做为目标图像
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Save layer
//mPaint.setColor(Color.RED);
//canvas.drawRect(mClockViewRectF.left+10,mClockViewRectF.top+10,mClockViewRectF.right+10,mClockViewRectF.bottom+10,mPaint);
int layerOne = canvas.saveLayer(mClockViewRectF, mPaint, Canvas.ALL_SAVE_FLAG);
//03.08saveLayer会创建一个全新透明的bitmap,大小与指定保存的区域一致,其后的绘图操作都放在这个bitmap上进行。
// 在绘制结束后,会直接盖在上一层的Bitmap上显示
// Draw clock scale lines
mPaint.setColor(mClockScaleLineColor);
float clockScaleLineStartY = mAdjustClockScaleLineStartX + mClockViewRectF.top;
float clockScaleLineEndY = clockScaleLineStartY + mClockScaleLineMaxHeight;
for (int i = 0; i < 120; i++) {//120
Log.i(“zyl log 0309”,”clockScaleLineStartY = ” + clockScaleLineStartY);
Log.i(“zyl log 0309”,”clockScaleLineEndY = ” + clockScaleLineEndY);
canvas.drawLine(mClockViewCenterX, clockScaleLineStartY,
mClockViewCenterX, clockScaleLineEndY, mPaint);
canvas.rotate(ANGLE_PER_SCALE, mClockViewCenterX, mClockViewCenterY);//3度;其实是画布所在的坐标系的旋转
}
//2017.03.03 canvas.rotate方法会将坐标轴旋转!!!!
mPaint.setXfermode(mXfermode);
canvas.rotate(mNowClockAngle - mClockMaskAdjustAngle, mClockViewCenterX, mClockViewCenterY);
// Generate a mask by path
int layerTwo = canvas.saveLayer(mClockViewRectF, mPaint, Canvas.ALL_SAVE_FLAG);
Log.i(“zyl log 02281”,”mClockViewRectF = ” + mClockViewRectF);//(0,0,520,520)
Xfermode zXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC); //add by zyl 2017.03.09
//mPaint.setXfermode(null);
mPaint.setXfermode(zXfermode);
//为什么需要重置一下呢?
//1.因为在调用saveLayer时,会生成了一个全新的bitmap,这个bitmap的大小就是我们指定的保存区域的大小,
// 新生成的bitmap是全透明的,注意是透明的在调用saveLayer后所有的绘图操作都是在这个bitmap上进行的
//2.在layerOne时,Paint的Xfermode为取下层绘制非交集部分,那么在drawOval和drawPath之后效果跟新建一张透明图层效果是一样的
canvas.drawOval(mClockViewRectF, mPaint);//外层大圆
//mPaint.setXfermode(mXfermode);//取下层绘制非交集部分
zXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);//03.09 ??? 为什么缺省一块?
mPaint.setXfermode(zXfermode);
//mPaint.setColor(mClockPointColor);
canvas.drawPath(mClockMaskPath, mPaint);//mClockMaskPath==眼球
//03.09:关于mClockMaskPath路径的计算???
canvas.restoreToCount(layerTwo);
mPaint.setXfermode(null);
// Draw clock point
mPaint.setColor(mClockPointColor);
canvas.rotate(mClockMaskAdjustAngle, mClockViewCenterX, mClockViewCenterY);
canvas.drawCircle(mClockPointCenterX, mClockPointCenterY, mClockPointRadius, mPaint);
canvas.restoreToCount(layerOne);
updateTimeText(canvas);
}
private void updateTimeText(Canvas canvas) {
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - mLastTimeMillis >= Constants.MINUTE) {
mLastTimeMillis = currentTimeMillis;
mCalendar.setTimeInMillis(currentTimeMillis);
mLastDigitalTimeStr = String.format("%02d:%02d",
mCalendar.get(Calendar.HOUR), mCalendar.get(Calendar.MINUTE));
}
mPaint.setColor(mDigitalTimeTextColor);
canvas.drawText(mLastDigitalTimeStr, mDigitalTimeTextStartX, mDigitalTimeTextStartY, mPaint);
}
public void performAnimation() {
cancelAnimation();
mClockAnimator = ValueAnimator.ofFloat(0, FULL_ANGLE);
mClockAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mNowClockAngle = (float) animation.getAnimatedValue();
mNowClockAngle += mInitClockAngle;
invalidate();
}
});
mClockAnimator.setDuration(DEFAULT_CLOCK_ANIMATION_DURATION);
mClockAnimator.setInterpolator(new LinearInterpolator());
mClockAnimator.setRepeatCount(Animation.INFINITE);
mClockAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
long currentTimeMillis = System.currentTimeMillis();
mCalendar.setTimeInMillis(currentTimeMillis);
mLastDigitalTimeStr = String.format("%02d:%02d", mCalendar.get(Calendar.HOUR),
mCalendar.get(Calendar.MINUTE));
mInitClockAngle = (mCalendar.get(Calendar.SECOND)
+ (float) mCalendar.get(Calendar.MILLISECOND) / Constants.SECOND) * ANGLE_PER_SECOND;
mLastTimeMillis = currentTimeMillis - mCalendar.get(Calendar.SECOND) * Constants.SECOND
- mCalendar.get(Calendar.MILLISECOND);
}
});
mClockAnimator.start();
}
public void cancelAnimation() {
if (mClockAnimator != null) {
mClockAnimator.removeAllUpdateListeners();
mClockAnimator.removeAllListeners();
mClockAnimator.cancel();
mClockAnimator = null;
}
}
}