package org.avatarqing.miclock;

import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.BounceInterpolator;
import android.view.animation.LinearInterpolator;

import java.util.Calendar;

public class ClockView extends View {
	private static final String TAG = ClockView.class.getSimpleName();
	private static boolean DEBUG = true;

	private int colorBg = 0xff237ead;
	private int colorBgRing = 0x80ffffff;
	private int colorMPOuterRing = Color.WHITE;
	private int colorHPOuterRing = 0xccdddddd;
	private int colorMinutePointer = Color.WHITE;
	private int colorHourPointer = 0xccdddddd;
	private int colorTriangle = 0xffdddddd;
	private int colorScaleRing = 0xccdddddd;

	private Paint paintBgRing;
	private Paint paintProgressRing;
	private Paint paintTriangle;
	private Paint paintMinutePointer;
	private Paint paintHourPointer;
	private Paint paintMPOuterRing;
	private Paint paintMPInnerCircle;
	private Paint paintHPOuterRing;
	private Paint paintHPInnerCircle;
	private Paint paintNumber;
	private Paint paintScaleRing;

	private RectF boundScaleRing;
	private RectF boundTimeRing;

	private int centerX;
	private int centerY;

	private float radius;
	private float radiusScaleRing;
	private float radiusMPOuterRing = 20;
	private float radiusMPInnerRing = 10;
	private float radiusHPOuterRing = 18;
	private float radiusHPInnerRing = 8;

	private float strokeWidthRing = 30;
	private float strokeWidthScaleRing = 2;

	private float zDepthScaleRing = 180;
	private float zDepthDashRing = 100;
	private float zDepthHourPointer = 50;
	private float zDepthMinutePointer = 0;

	private float hpTopEdgeLength = 6;
	private float hpBottomEdgeLength = 10;
	private float hpPointerLength = 10;
	private float hpTopCYOffset = 6;

	private float mpTopEdgeLength = 5;
	private float mpBottomEdgeLength = 8;
	private float mpPointerLength = 20;
	private float mpTopCYOffset = 6;

	private float trianglePointerOffset = 6;
	private float triangleSideLength = 40;

	private float canvasRotateX = 0;
	private float canvasRotateY = 0;

	private float rotateHourPointer = 0;
	private float rotateMinutePointer = 0;
	private float rotateSecondPointer = 0;

	private float scaleTextSize = 13;
	private float scaleTextAngle = 5;
	private float[][] scaleTextDrawCoordinates;

	private float progressRingInitRotateDegree = 270;

	private float canvasMaxRotateDegree = 20;

	private float[] ringDashIntervals = new float[]{3f, 6f};

	private float[] sweepGradientColorPos = new float[]{0f, 300f / 360f, 330f / 360f, 1f};
	private int[] sweepGradientColors = new int[]{Color.TRANSPARENT, 0x80ffffff, 0xddffffff, Color.WHITE};

	private ValueAnimator steadyAnim;
	private ValueAnimator secondAnim;

	private Path pathTriangle;
	private Path pathMinutePointer;
	private Path pathHourPointer;

	private Matrix matrixCanvas = new Matrix();
	private Matrix matrixSweepGradient = new Matrix();

	private Shader sweepGradient;

	private Camera camera = new Camera();

	public ClockView(Context context) {
		super(context);
		init();
	}

	public ClockView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	@Override
	protected void onAttachedToWindow() {
		super.onAttachedToWindow();
		if (DEBUG) {
			Log.d(TAG, "onAttachedToWindow");
		}
//		startNewSecondAnim();

		// register broadcast receiver
		IntentFilter intentFilter = new IntentFilter();
		intentFilter.addAction(Intent.ACTION_TIME_TICK);
		intentFilter.addAction(Intent.ACTION_TIME_CHANGED);
		intentFilter.addAction(Intent.ACTION_SCREEN_ON);
		intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
		getContext().registerReceiver(receiver, intentFilter);
	}

	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		if (DEBUG) {
			Log.d(TAG, "onDetachedFromWindow");
		}
		getContext().unregisterReceiver(receiver);
		cancelSecondAnimIfNeed();
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		reset();
	}

	@Override
	protected void onDraw(Canvas canvas) {
		canvas.drawColor(colorBg);
		rotateCanvas(canvas);
		drawContent(canvas);
	}

	private void rotateCanvas(Canvas canvas) {
		matrixCanvas.reset();

		camera.save();
		camera.rotateX(canvasRotateX);
		camera.rotateY(canvasRotateY);
		camera.getMatrix(matrixCanvas);
		camera.restore();

		int matrixCenterX = centerX;
		int matrixCenterY = centerY;
		// This moves the center of the view into the upper left corner (0,0)
		// which is necessary because Matrix always uses 0,0, as it's transform point
		matrixCanvas.preTranslate(-matrixCenterX, -matrixCenterY);
		// This happens after the camera rotations are applied, moving the view
		// back to where it belongs, allowing us to rotate around the center or
		// any point we choose
		matrixCanvas.postTranslate(matrixCenterX, matrixCenterY);

		canvas.concat(matrixCanvas);
	}

	private void translateCanvas(Canvas canvas, float x, float y, float z) {
		matrixCanvas.reset();
		camera.save();
		camera.translate(x, y, z);
		camera.getMatrix(matrixCanvas);
		camera.restore();

		int matrixCenterX = centerX;
		int matrixCenterY = centerY;
		matrixCanvas.preTranslate(-matrixCenterX, -matrixCenterY);
		matrixCanvas.postTranslate(matrixCenterX, matrixCenterY);

		canvas.concat(matrixCanvas);
	}

	private void drawContent(Canvas canvas) {
		// Check rotate bound
		if (rotateSecondPointer >= 360f) {
			rotateSecondPointer %= 360f;
		}
		if (rotateMinutePointer >= 360f) {
			rotateMinutePointer %= 360f;
		}
		if (rotateHourPointer >= 360f) {
			rotateHourPointer %= 360f;
		}

		// Rotate ring sweep gradient
		matrixSweepGradient.setRotate(getProgressRingRotateDegree(), centerX, centerY);
		sweepGradient.setLocalMatrix(matrixSweepGradient);
		paintProgressRing.setShader(sweepGradient);

		// Draw scale ring
		canvas.save();
		translateCanvas(canvas, 0f, 0f, zDepthScaleRing);
		canvas.drawArc(boundScaleRing, scaleTextAngle, 90 - 2 * scaleTextAngle, false, paintScaleRing);
		canvas.drawArc(boundScaleRing, 90 + scaleTextAngle, 90 - 2 * scaleTextAngle, false, paintScaleRing);
		canvas.drawArc(boundScaleRing, 180 + scaleTextAngle, 90 - 2 * scaleTextAngle, false, paintScaleRing);
		canvas.drawArc(boundScaleRing, 270 + scaleTextAngle, 90 - 2 * scaleTextAngle, false, paintScaleRing);
		canvas.drawText("12", scaleTextDrawCoordinates[0][0], scaleTextDrawCoordinates[0][1], paintNumber);
		canvas.drawText("6", scaleTextDrawCoordinates[1][0], scaleTextDrawCoordinates[1][1], paintNumber);
		canvas.drawText("9", scaleTextDrawCoordinates[2][0], scaleTextDrawCoordinates[2][1], paintNumber);
		canvas.drawText("3", scaleTextDrawCoordinates[3][0], scaleTextDrawCoordinates[3][1], paintNumber);
		canvas.restore();

		// Draw dash ring and second pointer
		canvas.save();
		translateCanvas(canvas, 0f, 0f, zDepthDashRing);
		canvas.drawArc(boundTimeRing, 0, 359.5f, false, paintBgRing);
		canvas.drawArc(boundTimeRing, 0, 359.5f, false, paintProgressRing);
		canvas.rotate(rotateSecondPointer, centerX, centerY);
		canvas.drawPath(pathTriangle, paintTriangle);
		canvas.restore();

		// Draw hour pointer
		canvas.save();
		translateCanvas(canvas, 0f, 0f, zDepthHourPointer);
		canvas.rotate(rotateHourPointer, centerX, centerY);
		canvas.drawPath(pathHourPointer, paintHourPointer);
		canvas.drawCircle(centerX, centerY, radiusHPOuterRing, paintHPOuterRing);
		canvas.drawCircle(centerX, centerY, radiusHPInnerRing, paintHPInnerCircle);
		canvas.restore();

		// Draw minute pointer
		canvas.save();
		translateCanvas(canvas, 0f, 0f, zDepthMinutePointer);
		canvas.rotate(rotateMinutePointer, centerX, centerY);
		canvas.drawPath(pathMinutePointer, paintMinutePointer);
		canvas.drawCircle(centerX, centerY, radiusMPOuterRing, paintMPOuterRing);
		canvas.drawCircle(centerX, centerY, radiusMPInnerRing, paintMPInnerCircle);
		canvas.restore();
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		float x = event.getX();
		float y = event.getY();

		int action = event.getActionMasked();
		switch (action) {
			case MotionEvent.ACTION_DOWN: {
				cancelSteadyAnimIfNeed();
				rotateCanvasWhenMove(x, y);
				return true;
			}
			case MotionEvent.ACTION_MOVE: {
				rotateCanvasWhenMove(x, y);
				return true;
			}
			case MotionEvent.ACTION_UP: {
				cancelSteadyAnimIfNeed();
				startNewSteadyAnim();
				return true;
			}
		}
		return super.onTouchEvent(event);
	}

	private void rotateCanvasWhenMove(float x, float y) {
		float dx = x - centerX;
		float dy = y - centerY;

		float percentX = dx / (getWidth() / 2);
		float percentY = dy / (getHeight() / 2);

		if (percentX > 1f) {
			percentX = 1f;
		} else if (percentX < -1f) {
			percentX = -1f;
		}
		if (percentY > 1f) {
			percentY = 1f;
		} else if (percentY < -1f) {
			percentY = -1f;
		}

		canvasRotateY = canvasMaxRotateDegree * percentX;
		canvasRotateX = -(canvasMaxRotateDegree * percentY);
	}

	private void init() {
		initValues();
		startNewSecondAnim();
	}

	private void initValues() {
		// Colors
		colorBg = 0xff237ead;
		colorBgRing = 0x80ffffff;
		colorMPOuterRing = Color.WHITE;
		colorHPOuterRing = 0xccdddddd;
		colorMinutePointer = Color.WHITE;
		colorHourPointer = 0xccdddddd;
		colorTriangle = 0xffdddddd;
		colorScaleRing = 0xccdddddd;

		// Angle
		progressRingInitRotateDegree = 270;
		canvasMaxRotateDegree = 20;
		scaleTextAngle = 5;

		// Radius
		radiusMPOuterRing = dp2px(8);
		radiusMPInnerRing = dp2px(4);
		radiusHPOuterRing = dp2px(8);
		radiusHPInnerRing = dp2px(4);

		// Stroke width
		strokeWidthRing = dp2px(25);
		strokeWidthScaleRing = dp2px(2);

		// zDepth
		zDepthScaleRing = dp2px(130);
		zDepthDashRing = dp2px(100);
		zDepthHourPointer = dp2px(50);
		zDepthMinutePointer = 0;

		// Text properties on scale ring
		scaleTextSize = sp2px(20);

		// Hour pointer
		hpTopEdgeLength = dp2px(3);
		hpBottomEdgeLength = dp2px(5);
		hpPointerLength = radius * 3 / 5;
		hpTopCYOffset = dp2px(3);

		// Minute pointer
		mpTopEdgeLength = dp2px(2);
		mpBottomEdgeLength = dp2px(3);
		mpPointerLength = radius * 4 / 5;
		mpTopCYOffset = dp2px(3);

		// Second pointer
		trianglePointerOffset = dp2px(6);
		triangleSideLength = dp2px(20);

		// Ring dash intervals
		ringDashIntervals = new float[]{dp2px(1), dp2px(3)};

		// Sweep gradient
		sweepGradientColorPos = new float[]{0f, 300f / 360f, 330f / 360f, 1f};
		sweepGradientColors = new int[]{Color.TRANSPARENT, 0x80ffffff, 0xddffffff, Color.WHITE};
	}

	private void reset() {
		initBound();
		initBgRing();
		initProgressRing();
		initScaleRing();
		initTriangleSecondPointer();
		initMinutePointer();
		initHourPointer();
		updateTimePointer();
	}

	private void initBound() {
		radius = getWidth() * 3 / 7;
		radiusScaleRing = radius * 4 / 3;

		centerX = getWidth() / 2;
		centerY = getHeight() / 2;

		boundTimeRing = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
		boundScaleRing = new RectF(centerX - radiusScaleRing, centerY - radiusScaleRing, centerX + radiusScaleRing, centerY + radiusScaleRing);

		hpPointerLength = radius * 3 / 5;
		mpPointerLength = radius * 3 / 5;
	}

	private void initScaleRing() {
		// Scale ring paint
		paintScaleRing = new Paint();
		paintScaleRing.setAntiAlias(true);
		paintScaleRing.setStyle(Paint.Style.STROKE);
		paintScaleRing.setStrokeWidth(strokeWidthScaleRing);
		paintScaleRing.setColor(colorScaleRing);

		// Number text paint
		paintNumber = new Paint();
		paintNumber.setAntiAlias(true);
		paintNumber.setStyle(Paint.Style.FILL);
		paintNumber.setColor(colorScaleRing);
		paintNumber.setTextSize(scaleTextSize);

		// Parse text baseline
		float scaleTextWidthTwo = paintNumber.measureText("12");
		float scaleTextWidthOne = paintNumber.measureText("6");
		float scaleTextHeight = paintNumber.measureText("12");

		RectF topTextBound = new RectF();
		topTextBound.left = centerX - scaleTextWidthTwo / 2;
		topTextBound.top = centerY - radiusScaleRing - scaleTextHeight / 2;
		topTextBound.right = centerX + scaleTextWidthTwo / 2;
		topTextBound.bottom = centerY - radiusScaleRing + scaleTextHeight / 2;

		RectF bottomTextBound = new RectF();
		bottomTextBound.left = centerX - scaleTextWidthOne / 2;
		bottomTextBound.top = centerY + radiusScaleRing - scaleTextHeight / 2;
		bottomTextBound.right = centerX + scaleTextWidthOne / 2;
		bottomTextBound.bottom = centerY + radiusScaleRing + scaleTextHeight / 2;

		RectF leftTextBound = new RectF();
		leftTextBound.left = centerX - radiusScaleRing - scaleTextWidthOne / 2;
		leftTextBound.top = centerY - scaleTextHeight / 2;
		leftTextBound.right = centerX - radiusScaleRing + scaleTextWidthOne / 2;
		leftTextBound.bottom = centerY + scaleTextHeight / 2;

		RectF rightTextBound = new RectF();
		rightTextBound.left = centerX + radiusScaleRing - scaleTextWidthOne / 2;
		rightTextBound.top = leftTextBound.top;
		rightTextBound.right = centerX + radiusScaleRing + scaleTextWidthOne / 2;
		rightTextBound.bottom = leftTextBound.bottom;

		Paint.FontMetrics fm = paintNumber.getFontMetrics();

		scaleTextDrawCoordinates = new float[4][2];
		scaleTextDrawCoordinates[0][0] = topTextBound.left;
		scaleTextDrawCoordinates[0][1] = topTextBound.top + (topTextBound.bottom - topTextBound.top) / 2 - (fm.bottom - fm.top) / 2 - fm.top;
		scaleTextDrawCoordinates[1][0] = bottomTextBound.left;
		scaleTextDrawCoordinates[1][1] = bottomTextBound.top + (bottomTextBound.bottom - bottomTextBound.top) / 2 - (fm.bottom - fm.top) / 2 - fm
				.top;
		scaleTextDrawCoordinates[2][0] = leftTextBound.left;
		scaleTextDrawCoordinates[2][1] = leftTextBound.top + (leftTextBound.bottom - leftTextBound.top) / 2 - (fm.bottom - fm.top) / 2 - fm.top;
		scaleTextDrawCoordinates[3][0] = rightTextBound.left;
		scaleTextDrawCoordinates[3][1] = rightTextBound.top + (rightTextBound.bottom - rightTextBound.top) / 2 - (fm.bottom - fm.top) / 2 - fm.top;
	}

	private void initHourPointer() {
		// Center Ring
		paintHPOuterRing = new Paint();
		paintHPOuterRing.setStyle(Paint.Style.FILL);
		paintHPOuterRing.setAntiAlias(true);
		paintHPOuterRing.setColor(colorHPOuterRing);

		paintHPInnerCircle = new Paint(paintHPOuterRing);
		paintHPInnerCircle.setColor(colorBg);

		// Minute Pointer
		paintHourPointer = new Paint();
		paintHourPointer.setAntiAlias(true);
		paintHourPointer.setStyle(Paint.Style.FILL);
		paintHourPointer.setColor(colorHourPointer);

		float topX1 = centerX - hpTopEdgeLength / 2;
		float topY1 = centerY - hpPointerLength + strokeWidthRing / 2;
		float topX2 = centerX + hpTopEdgeLength / 2;
		float topY2 = topY1;
		float topCX1 = centerX;
		float topCY1 = topY1 - hpTopCYOffset;

		float bottomX1 = centerX - hpBottomEdgeLength / 2;
		float bottomY1 = centerY;
		float bottomX2 = centerX + hpBottomEdgeLength / 2;
		float bottomY2 = bottomY1;

		pathHourPointer = new Path();
		pathHourPointer.moveTo(bottomX1, bottomY1);
		pathHourPointer.lineTo(bottomX2, bottomY2);
		pathHourPointer.lineTo(topX2, topY2);
		pathHourPointer.quadTo(topCX1, topCY1, topX1, topY1);
		pathHourPointer.close();
	}

	private void initMinutePointer() {
		// Center Ring
		paintMPOuterRing = new Paint();
		paintMPOuterRing.setStyle(Paint.Style.FILL);
		paintMPOuterRing.setAntiAlias(true);
		paintMPOuterRing.setColor(colorMPOuterRing);

		paintMPInnerCircle = new Paint(paintMPOuterRing);
		paintMPInnerCircle.setColor(colorBg);

		// Minute Pointer
		paintMinutePointer = new Paint();
		paintMinutePointer.setStyle(Paint.Style.FILL);
		paintMinutePointer.setAntiAlias(true);
		paintMinutePointer.setColor(colorMinutePointer);

		float topX1 = centerX - mpTopEdgeLength / 2;
		float topY1 = centerY - mpPointerLength + strokeWidthRing / 2;
		float topX2 = centerX + mpTopEdgeLength / 2;
		float topY2 = topY1;
		float topCX1 = centerX;
		float topCY1 = topY1 - mpTopCYOffset;

		float bottomX1 = centerX - mpBottomEdgeLength / 2;
		float bottomY1 = centerY;
		float bottomX2 = centerX + mpBottomEdgeLength / 2;
		float bottomY2 = bottomY1;

		pathMinutePointer = new Path();
		pathMinutePointer.moveTo(bottomX1, bottomY1);
		pathMinutePointer.lineTo(bottomX2, bottomY2);
		pathMinutePointer.lineTo(topX2, topY2);
		pathMinutePointer.quadTo(topCX1, topCY1, topX1, topY1);
		pathMinutePointer.close();
	}

	private void initTriangleSecondPointer() {
		paintTriangle = new Paint();
		paintTriangle.setColor(colorTriangle);
		paintTriangle.setStyle(Paint.Style.FILL);
		paintTriangle.setAntiAlias(true);

		float height = (float) (1.0 * Math.sqrt(3f) / 2 * triangleSideLength);

		float point1x = centerX;
		float point1y = centerY - radius + strokeWidthRing / 2 + trianglePointerOffset;
		float point2x = point1x - triangleSideLength / 2;
		float point2y = point1y + height;
		float point3x = point1x + triangleSideLength / 2;
		float point3y = point1y + height;

		pathTriangle = new Path();
		pathTriangle.moveTo(point1x, point1y);
		pathTriangle.lineTo(point2x, point2y);
		pathTriangle.lineTo(point3x, point3y);
		pathTriangle.close();
	}

	private void initBgRing() {
		paintBgRing = new Paint();
		paintBgRing.setStyle(Paint.Style.STROKE);
		paintBgRing.setStrokeWidth(strokeWidthRing);
		paintBgRing.setAntiAlias(true);
		paintBgRing.setPathEffect(new DashPathEffect(ringDashIntervals, 0));
		paintBgRing.setColor(colorBgRing);
	}

	private void initProgressRing() {
		paintProgressRing = new Paint(paintBgRing);
		paintProgressRing.setColor(Color.WHITE);
		sweepGradient = new SweepGradient(centerX, centerY, sweepGradientColors, sweepGradientColorPos);
		paintProgressRing.setShader(sweepGradient);
	}

	private void startNewSecondAnim() {
		if (DEBUG) {
			Log.d(TAG, "startNewSecondAnim");
		}
		cancelSecondAnimIfNeed();
		updateTimePointer();

		final float startDegree = 0f;
		final float endDegree = 360f;
		secondAnim = ValueAnimator.ofFloat(startDegree, endDegree);
		// FIXME 不知为何某些机型动画实际执行时间是duration的一半
		// 在构造函数里启动动画就正常,在其他方法(如onAttachedToWindow或onSizeChanged)里调用本方法就不正常
		// 出问题的机型:小米4、小米1S
		secondAnim.setDuration(60 * 1000);
		secondAnim.setInterpolator(new LinearInterpolator());
		secondAnim.setRepeatMode(ValueAnimator.RESTART);
		secondAnim.setRepeatCount(ValueAnimator.INFINITE);
		secondAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

			private float lastDrawValue = 0;
			private float drawInterval = 0.1f;

			private float lastUpdatePointerValue = 0;
			private float updatePointerInterval = 360 / 60 * 5;

			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				float newValue = (float) animation.getAnimatedValue();

				// Check if it is the time to update pointer position
				float increasedPointerValue = newValue - lastUpdatePointerValue;
				if (increasedPointerValue < 0) {
					increasedPointerValue = endDegree + increasedPointerValue;
				}
				if (increasedPointerValue >= updatePointerInterval) {
					lastUpdatePointerValue = newValue;
					updateTimePointer();
				}

				// Check if it is the time to invalidate
				float increasedDrawValue = newValue - lastDrawValue;
				if (increasedDrawValue < 0) {
					increasedDrawValue = endDegree + increasedDrawValue;
				}
				if (increasedDrawValue >= drawInterval) {
					lastDrawValue = newValue;
					rotateSecondPointer += increasedDrawValue;
					invalidate();
//					if (DEBUG) {
//						Log.d(TAG, String.format("newValue:%s , currentPlayTime:%s", newValue, animation.getCurrentPlayTime()));
//					}
				}
			}
		});
		secondAnim.start();
	}

	private void cancelSecondAnimIfNeed() {
		if (secondAnim != null && (secondAnim.isStarted() || secondAnim.isRunning())) {
			secondAnim.cancel();
			if (DEBUG) {
				Log.d(TAG, "cancelSecondAnimIfNeed");
			}
		}
	}

	private void startNewSteadyAnim() {
		final String propertyNameRotateX = "canvasRotateX";
		final String propertyNameRotateY = "canvasRotateY";

		PropertyValuesHolder holderRotateX = PropertyValuesHolder.ofFloat(propertyNameRotateX, canvasRotateX, 0);
		PropertyValuesHolder holderRotateY = PropertyValuesHolder.ofFloat(propertyNameRotateY, canvasRotateY, 0);
		steadyAnim = ValueAnimator.ofPropertyValuesHolder(holderRotateX, holderRotateY);
		steadyAnim.setDuration(1000);
		steadyAnim.setInterpolator(new BounceInterpolator());
		steadyAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				canvasRotateX = (float) animation.getAnimatedValue(propertyNameRotateX);
				canvasRotateY = (float) animation.getAnimatedValue(propertyNameRotateY);
			}
		});
		steadyAnim.start();
	}

	private void cancelSteadyAnimIfNeed() {
		if (steadyAnim != null && (steadyAnim.isStarted() || steadyAnim.isRunning())) {
			steadyAnim.cancel();
		}
	}

	private void updateTimePointer() {
		int second = Calendar.getInstance().get(Calendar.SECOND);
		int minute = Calendar.getInstance().get(Calendar.MINUTE);
		int hour = Calendar.getInstance().get(Calendar.HOUR);
		float percentSecond = (float) (1.0 * second / 60);
		float percentMinute = (float) (1.0 * (minute * 60 + second) / (60 * 60));
		float percentHour = (float) (1.0 * (hour * 60 * 60 + minute * 60 + second) / (60 * 60 * 12));
		rotateSecondPointer = 360 * percentSecond;
		rotateMinutePointer = 360 * percentMinute;
		rotateHourPointer = 360 * percentHour;
	}

	private float getProgressRingRotateDegree() {
		float degree = (rotateSecondPointer + progressRingInitRotateDegree) % 360;
		return degree;
	}

	public float dp2px(float dpValue) {
		return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
	}

	public float sp2px(float spValue) {
		return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
	}

	private BroadcastReceiver receiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			if (intent == null) {
				return;
			}
			String action = intent.getAction();
			if (TextUtils.isEmpty(action)) {
				return;
			}
			if (DEBUG) {
				Log.d(TAG, String.format("action -> %s", action));
			}
			if (action.equals(Intent.ACTION_TIME_TICK)) {
				updateTimePointer();
			} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
				startNewSecondAnim();
			} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
				cancelSecondAnimIfNeed();
			}
		}
	};
}