Android 三角形绕环运动

一、前言

对于开发Path运动的游戏和地图而言,往往要处理物体跟随线条移动,但是线条的不规则导致需要物体频繁旋转角度,但这种旋转其实是一个很难的问题,因为一个多边形有多个顶点,必须要时应该舍弃一些顶点,比如打车软件,经常看到车头跑出了路线图或者原地旋转,主要原因是之一是车体和路不成比例、且顶点难以获取造成。今天本篇从圆环运动来解析一下这类问题。

二、问题点

注意:左上角以及三角形边框是DEBUG视图

当设计给你一个矩形汽车或者三角形,你很容易找到中心点,但是现实情况可能没这么幸运,很多图形都是不成比例的,另外中心点找到了,车头或者三角形头部没有贴着线怎么办,这个问题我们暂不处理,后续PathMeasure的案例中会重点处理一下。

目前的难点

  • 【1】 中心点落在圆环上
  • 【2】运行过程中自动调整 "箭头" 方向

难点:最大的难点不是虚线动画,而是图中的三角形的 "自旋转" + "整体旋转",因为三角形物件不是正方形,因此自旋转中心点位置很难处理,并且导致中心点很难做 "骑线" 运动。

解决思路:

这个看似简单的问题,利用了很多初中高中数学知识。对于宽高不一致图片或者图形,我们最简单的办法就是将其填充为宽高一致的,然后通过像素矩阵旋转,再通过像素矩阵平移就能完成。当然,我相信,如果数学基础很好的话,还有更好的方法。

三、方案

技术上我们通过下面方式实现

  • 1: Path的线形处理

  • 2:原图转换,原图如果不是正方形,必须转换填充,否则很难和中心点对齐,当然,旋转Canvas也是可以做到的,这里我们主要是通过matrix,学会图片变幻

  • 3:图片旋转中心点查计算

  • 4:切线夹角推理,这里主要考察数学中的圆形的切线问题,(degree + 90)是切线和X轴正方向推理得到的

  • 5:Matrix pre/post关系,变换图像分为2个阶段,pre是预处理阶段,post是绘制阶段,理论上让图片先在固定坐标体系下变换,再移动到指定的位置上展示,更让人能容易理解,否则在Post时坐标系一直是动的,可能产生其他问题

全部代码

ini 复制代码
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);
    private float offsetDegree = 5;  //补偿角度

    public CirclePathArrowView(Context context) {
        this(context, null);
    }

    public CirclePathArrowView(Context context,  AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CirclePathArrowView(Context context,  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());
    }

    private RectF rectF = new RectF();
    private Path path = new Path();
    private PaintFlagsDrawFilter paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    private Matrix matrix = new Matrix();

    private Bitmap bmp;
    Canvas canvasBitmap;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

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

        float radius = Math.min(getWidth(), getHeight()) / 3F - mPathPaint.getStrokeWidth();

        mPathPaint.setPathEffect(new DashPathEffect(new float[]{40, 20}, phase));

        path.reset();
        path.addCircle(centerX, centerY, radius, Path.Direction.CCW);
        canvas.drawPath(path, mPathPaint);

        canvas.setDrawFilter(paintFlagsDrawFilter);
        if (arrowBitmap != null && !arrowBitmap.isRecycled()) {
            double radians = Math.toRadians(degree);

            float bmpCenterX = arrowBitmap.getWidth() / 2F;
            float bmpCenterY = arrowBitmap.getHeight() / 2F;
            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】原图变幻问题
            if(bmp == null) {
                 bmp = Bitmap.createBitmap((int) arrowRadius * 2, (int) arrowRadius * 2, Bitmap.Config.ARGB_8888);
                 canvasBitmap = new Canvas(bmp);
            }
            bmp.eraseColor(Color.TRANSPARENT);

            //矩阵变幻以图片本身左上角为坐标原点,而不是Canvas坐标,因此使用Matrix

            matrix.reset();
            //预处理,移动原图坐标系,让原图中心点对齐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 + offsetDegree, pX, pY);
            canvasBitmap.drawBitmap(arrowBitmap, matrix, mPathPaint);

            if (IS_DEBUG) {
                canvas.drawBitmap(arrowBitmap, matrix, mPathPaint);
            }

            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_arrow_right);
        }
        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;
        }
    }
}

四、总结

处理这类问题一般是规则化图形,再加一定的偏移,后续使用PathMeasure时做运动时,其本质是构建一个大圆环来处理和不规则图形的运动,但是理解本篇可以方便理解后续实现原理。

arduino 复制代码
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
相关推荐
yinuo26 分钟前
前端跨页面通讯终极指南⑥:SharedWorker 用法全解析
前端
CoderYanger4 小时前
C.滑动窗口-求子数组个数-越长越合法——2799. 统计完全子数组的数目
java·c语言·开发语言·数据结构·算法·leetcode·职场和发展
C++业余爱好者4 小时前
Java 提供了8种基本数据类型及封装类型介绍
java·开发语言·python
想用offer打牌4 小时前
RocketMQ如何防止消息丢失?
java·后端·架构·开源·rocketmq
皮卡龙4 小时前
Java常用的JSON
java·开发语言·spring boot·json
PineappleCoder5 小时前
还在重复下载资源?HTTP 缓存让二次访问 “零请求”,用户体验翻倍
前端·性能优化
拉不动的猪5 小时前
webpack编译中为什么不建议load替换ast中节点删除consolg.log
前端·javascript·webpack
李姆斯5 小时前
Agent时代下,ToB前端的UI和交互会往哪走?
前端·agent·交互设计
利刃大大5 小时前
【JavaSE】十三、枚举类Enum && Lambda表达式 && 列表排序常见写法
java·开发语言·枚举·lambda·排序
float_六七5 小时前
Java反射:万能遥控器拆解编程
java·开发语言