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);
相关推荐
柏箱3 分钟前
PHP基本语法总结
开发语言·前端·html·php
原野心存8 分钟前
java基础进阶——继承、多态、异常捕获(2)
java·java基础知识·java代码审计
进阶的架构师13 分钟前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
黄俊懿13 分钟前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
新缸中之脑13 分钟前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz85617 分钟前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
木子020422 分钟前
java高并发场景RabbitMQ的使用
java·开发语言
看到请催我学习23 分钟前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
夜雨翦春韭33 分钟前
【代码随想录Day29】贪心算法Part03
java·数据结构·算法·leetcode·贪心算法
blaizeer1 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css