一、实现三角形环绕圆圈运动
【1】 中心点落在圆环上
【2】运行过程中自动调整箭头方向
二、代码实现
/**
* 难点1: Path的线形处理
* 难点2:原图转换,原图如果不是正方形,必须转换填充,否则很难和中心点对齐,当然,旋转Canvas也是可以做到的,这里我们主要是通过matrix,学会图片变幻
* 难点3:图片旋转中心点查询
* 难点4:切线夹角推理,这里主要考察数学中的圆形的切线问题,(degree + 90)是切线和X轴正方向推理得到的
* 难点5:Matrix pre/post关系,变换图像分为2个阶段,pre是预处理阶段,post是绘制阶段,理论上让图片先在固定坐标体系下变换,再移动到指定的位置上展示,更让人能容易理解,否则在Post时坐标系一直是动的,可能产生其他问题
*/
public class CirclePathArrowView extends View implements ValueAnimator.AnimatorUpdateListener {
private static final boolean IS_DEBUG = true;
private final Paint mPathPaint;
private Bitmap arrowBitmap = null;
private ValueAnimator animator;
private float degree = 0;
private float phase = 0;
private float speed = dp2px(1);
public CirclePathArrowView(Context context) {
this(context, null);
}
public CirclePathArrowView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CirclePathArrowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPathPaint.setAntiAlias(true);
mPathPaint.setFilterBitmap(false);
mPathPaint.setStyle(Paint.Style.STROKE);
mPathPaint.setStrokeWidth(dp2px(1));
mPathPaint.setColor(0xaaffffff);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
if (heightMode == MeasureSpec.UNSPECIFIED) {
height = (int) dp2px(120);
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(getMeasuredHeight(), getMeasuredWidth());
} else {
height = MeasureSpec.getSize(heightMeasureSpec);
}
setMeasuredDimension(getMeasuredWidth(), height);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float centerX = getWidth() / 2;
float centerY = getHeight() / 2;
float radius = Math.min(getWidth(), getHeight()) / 3 - mPathPaint.getStrokeWidth();
mPathPaint.setPathEffect(new DashPathEffect(new float[]{40, 20}, phase));
Path path = new Path();
path.addCircle(centerX, centerY, radius, Path.Direction.CCW);
canvas.drawPath(path, mPathPaint);
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG));
if (arrowBitmap != null && !arrowBitmap.isRecycled()) {
double radians = Math.toRadians(degree);
float bmpCenterX = arrowBitmap.getWidth() / 2;
float bmpCenterY = arrowBitmap.getHeight() / 2;
float arrowRadius = Math.max(bmpCenterX, bmpCenterY);
float x = (float) (radius * Math.cos(radians) + centerX);
float y = (float) (radius * Math.sin(radians) + centerY);
//这里变幻图像,主要解决2个问题:【1】原图不是正方形,【2】原图变幻问题
Bitmap bmp = Bitmap.createBitmap((int) arrowRadius * 2, (int) arrowRadius * 2, Bitmap.Config.ARGB_4444);
Canvas canvasBitmap = new Canvas(bmp);
//矩阵变幻以图片本身左上角为坐标原点,而不是Canvas坐标,因此使用Matrix
Matrix matrix = new Matrix();
//预处理,移动原图坐标系,让原图中心点对齐bmp中心点,计算x,y方向的偏移量
float dx = arrowRadius * 2 - bmpCenterX * 2;
float dy = arrowRadius * 2 - bmpCenterY * 2;
matrix.preTranslate(dx, dy);
//预处理,在新坐标系中,找到坐标原点到旋转中心的偏移量
float pX = arrowRadius - dx; //px,py 也是偏移量,不是绝对坐标
float pY = arrowRadius - dx;
matrix.preRotate(degree + 90, pX, pY);
canvasBitmap.drawBitmap(arrowBitmap, matrix, mPathPaint);
if (IS_DEBUG) {
canvas.drawBitmap(arrowBitmap, matrix, mPathPaint);
}
RectF rectF = new RectF();
rectF.left = x - arrowRadius;
rectF.right = x + arrowRadius;
rectF.top = y - arrowRadius;
rectF.bottom = y + arrowRadius;
int color = mPathPaint.getColor();
mPathPaint.setColor(Color.MAGENTA);
//将新图会知道矩形区域
canvas.drawBitmap(bmp, null, rectF, null);
canvas.drawRect(rectF, mPathPaint);
mPathPaint.setColor(color);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (arrowBitmap == null || arrowBitmap.isRecycled()) {
arrowBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_cloud_ft_arrow);
}
if (animator != null) {
animator.cancel();
}
animator = ValueAnimator.ofFloat(0, 360).setDuration(5000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatMode(ValueAnimator.RESTART);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(this);
animator.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (arrowBitmap != null) {
arrowBitmap.recycle();
arrowBitmap = null;
}
if (animator != null) {
animator.cancel();
}
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
degree = (float) animation.getAnimatedValue();
invalidate();
phase += speed;
if (phase > Integer.MAX_VALUE) {
phase = phase % speed;
}
}
}