Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Posted 鸽一门

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)相关的知识,希望对你有一定的参考价值。

前面几篇文章已经按照顺序讲解了Paint画笔、Canvas画布、Path相关内容了,也许没有面面俱到,但特地强调了其重点内容。有关Path的内容只讲解了贝塞尔曲线绘制,日后再做补充。此篇文章将介绍另外一个重点内容:PathMeasure。

PathMeasure类明显是用来辅助Path类的,其API方法很少,但是有两个王牌,即截取片段getSegment方法和获取指定长度的位置坐标及该点切线值tanglegetPosTan方法。前者容易了解,截取部分曲线或图形片段处理,而后者的获取指定点切线值,这个充满数学魅力的API,

(此系列文章知识点相对独立,可分开阅读,不过笔者建议按照顺序阅读,理解更加深入清晰)

Android 高级UI解密 (四) :花式玩转贝塞尔曲线(波浪、轨迹变换动画
Android 高级UI解密 (三) :Canvas裁剪 与 二维、三维Camera几何变换(图层Layer原理)
Android 高级UI解密 (二) :Paint滤镜 与 颜色过滤(矩阵变换)
Android 高级UI解密 (一) :Paint图形文字绘制 与 高级渲染

此篇涉及到的知识点如下:

  • PathMeasure基础API介绍
  • PathMeasure实践Loading效果和切线
  • 新思路实现轨迹变换动画

一. PathMeasure基础API介绍

顾名思义,PathMeasure是一个用来测量Path的类,它的方法比较少,以下先来介绍API基本使用。

1. 构造方法

方法名释义
PathMeasure()创建一个空的PathMeasure
PathMeasure(Path path, boolean forceClosed)创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)。

(1)无参构造函数

PathMeasure()

用这个构造函数可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的。如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。


(2)有参构造函数

PathMeasure (Path path, boolean forceClosed)
  • Path path:被关联的 Path ;
  • boolean forceClosed:用来确保 Path 闭合,如果设置为 true, 则不论之前Path是否闭合,都会自动闭合该 Path(如果Path可以闭合的话);

用这个构造函数是创建一个 PathMeasure 并关联一个 Path, 其实和创建一个空的 PathMeasure 后调用 setPath 进行关联效果是一样的。同样,被关联的 Path 也必须是已经创建好的。如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。

注意forceClosed 参数:

  • 不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path的状态,即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。
  • forceClosed 的状态设置可能会影响测量结果。如果 Path 未闭合,例如绘制的是未闭合的矩形,但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,即测量了矩形的四条边而不是三条,获取到到是该 Path 闭合时的状态。


2. 公共方法

返回值方法名释义
voidsetPath(Path path, boolean forceClosed)关联一个Path
booleanisClosed()是否闭合
floatgetLength()获取Path的长度
booleannextContour()跳转到下一个轮廓
booleangetSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)截取片段
booleangetPosTan(float distance, float[] pos, float[] tan)获取指定长度的位置坐标及该点切线值tangle
booleangetMatrix(float distance, Matrix matrix, int flags)设置距离为0 <= distance <= getLength(),然后计算相应的矩阵

(1)setPath方法

void setPath(Path path, boolean forceClosed)

作用:此方法是 PathMeasure 与 Path 关联的重要方法,效果和构造函数中两个参数的作用是一样的。


(2)isClosed方法

boolean isClosed()

作用:此方法用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为true。


(3)getLength方法

float getLength()

作用:此方法用于获取 Path 路径的总长度。


(4)nextContour方法

boolean nextContour()

作用: Path 可以由多条曲线构成,但不论是 getLength 方法, 还是getgetSegment 或者其它方法,都只会在其中第一条线段上运行。此 nextContour方法 就是用于跳转到下一条曲线到方法。如果跳转成功,则返回 true, 如果跳转失败,则返回 false。


(5)getSegment方法

boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
  • 返回值boolean:判断截取是否成功(true 表示截取成功,结果存入dst中,false 截取失败,不会改变dst中内容);
  • float startD:开始截取位置距离 Path 起点的长度(取值范围: 0 <= startD < stopD <= Path总长度);
  • float stopD:结束截取位置距离 Path 起点的长度(取值范围: 0 <= startD < stopD <= Path总长度);
  • Path dst:截取的 Path 将会添加到 dst 中(注意: 是添加,而不是替换);
  • boolean startWithMoveTo:起始点是否使用 moveTo,用于保证截取的 Path 第一个点位置不变(true表示保证截取得到的 Path 片段不会发生形变,false表示保证存储截取片段的 Path(dst) 的连续性);

作用:用于获取Path路径的一个片段。(如果 startD、stopD 的数值不在取值范围 [0, getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst 内容)。

注意:如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)


(6)getPosTan方法

boolean getPosTan(float distance, float[] pos, float[] tan)
  • 返回值(boolean):判断获取是否成功(true表示成功,数据会存入 pos 和 tan 中,false 表示失败,pos 和 tan 不会改变);
  • float distance:距离 Path 起点的长度 取值范围: 0 <= distance <= getLength
  • float[] pos:该点的坐标值,坐标值: (x==[0], y==[1])
  • float[] tan:该点的正切值,正切值: (x==[0], y==[1])

作用:用于获取路径上某点的坐标以及该位置的正切值,即切线的坐标。相当于是getPosgetTan两个API的集合。

//用于获取路径上某点的切线角度
(math.atan2(tan[1], tan[0])*180.0 / math.PI)

上面代码是常用的一个公式,用于获取路径上某点的切线角度通过 tan 得值计算出图片旋转的角度,tan 是 tangent 的缩写,即中学中常见的正切, 其中tan0是邻边边长,tan1是对边边长,而Math中 atan2 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度,所以上面又将弧度转为了角度


(7)getMatrix方法

boolean getMatrix(float distance, Matrix matrix, int flags) 
  • 返回值(boolean):判断获取是否成功(true表示成功,数据会存入matrix中,false 失败,matrix内容不会改变);
  • float distance:距离 Path 起点的长度(取值范围: 0 <= distance <= getLength);
  • Matrix matrix:根据 falgs 封装好的matrix,会根据 flags 的设置而存入不同的内容;
  • int flags:规定哪些内容会存入到matrix中(可选择POSITION_MATRIX_FLAG位置 、ANGENT_MATRIX_FLAG正切 );

作用:用于得到路径上某一长度的位置以及该位置的正切值的矩阵。





二. PathMeasure实践

1. 实现Loading动画效果

  1. 在自定义View构造方法中调用Paint的setStylesetStrokeWidth方法初始化画笔基本属性。
  2. 创建Path路径对象,绘制一个空心圆;创建PathMeasure对象,调用setPath方法关联Path,并调用getLength获取路径长度,创建Dst对象,后续会使用。
  3. 创建动画ValueAnimator,调用ofFloat(0, 1)方法,此处的(0, 1)范围代表百分比例,即绘制圆的比例从0到100%。再设置线性插值器和循环播放,重点在于实现动画的监听事件中获取变化的比例值赋值给成员变量,调用invalidate();刷新。
  4. 以上都是在构造方法中实现,准备就绪后,接下来在onDraw方法中进行绘制,绘制圆的起点当然是0,终点则是随着动画渐变成圆,为mLength * mAnimValue;,即圆比例值*绘制路径总长度。有了这两个float值后,可使用PathMeasure的getSegment(start, stop, mDst, true)方法获取到对应路径,接下来再调用熟悉的canvas 绘制drawPath(mDst, mPaint)即可。

注意,在onDraw方法中一开始除了需要重置mDst外,还需要调用Dst.lineTo(0, 0)方法,这是android硬件加速的一个小bug,若不调用则getSegment(start, stop, mDst, true)方法可能不起作用。

public class PathTracingView extends View 
    private Path mDst;
    private Path mPath;
    private Paint mPaint;
    private float mLength;
    private float mAnimValue;

    private PathMeasure mPathMeasure;
    ......

    public PathTracingView(Context context, AttributeSet attrs) 
        super(context, attrs);
        //设置Paint画笔基本属性
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPath = new Path();
        mDst = new Path();

        mPath.addCircle(400, 400, 100, Path.Direction.CW);
        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, true);

        mLength = mPathMeasure.getLength();

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setDuration(1000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) 
                mAnimValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            
        );
        animator.start();
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        mDst.reset();
        mDst.lineTo(0, 0);

        float stop = mLength * mAnimValue;
        float start = 0//float start = (float) (stop - ((0.5 - Math.abs(mAnimValue - 0.5)) * mLength));

        mPathMeasure.getSegment(0, stop, mDst, true);
        //mPathMeasure.getSegment(start, stop, mDst, true);
        canvas.drawPath(mDst, mPaint);
    

此部分的实现重点在于对PathMeasure的运用,首先获取动画实时变化的圆比例,调用getSegment方法获取圆的指定路径,canvas将其绘制出来。效果如下:

在见识到PathMeasure的精彩之处后,发现上面这个Loading绘制太普通了,怎么着也要来点特效~只需要改变两行代码就可以实现Windows的开机Loading效果图。

效果如上,比起第一个要酷炫不少吧~只需要将onDraw方法中将float start = 0;改成

//修改成Windows的Loading效果
float start = (float) (stop - ((0.5 - Math.abs(mAnimValue - 0.5)) * mLength));
mPathMeasure.getSegment(start, stop, mDst, true);

可以发现stop的值没有修改,仍旧是从[0, 圆周长长度] 之间的变化,可是start值看似有些复杂,决定于stop、mAnimValue的值。先来分析动画效果,可把它分成上半圆、下半圆效果来看。这意味着:

  • 当mAnimValue小于0.5时,即绘制不到半圆时,start还是0,绘制下半圆效果跟第一个相同。
  • 当mAnimValue大于0.5时,即可以绘制整圆时,经过运算的start越趋近于stop,因此其效果出现的是上半圆。

因此可见各种绚丽的动画效果,对坐标进行简单的数学计算就可以实现。



2. 实现轨迹动画的新思路

关于轨迹动画的实现,通常是使用VectorDrawable或者Path来实现,但一位Android大神Romain Guy提出了一种新的实现思路:Path Tracing Trick,此小节结合新的思路来实现轨迹动画效果。

如上图所示这几种不同的线条效果,通过设置画笔Paint属性即可完成。重点查看第三种Dash风格,实质是由实线、虚线组合而成,在代码设置Dash风格时需要传入两个参数:实线长度和虚线长度。

那么举一反三,如果要实现一个布景的绘制动画,通过设置画笔Paint的Dash风格,将实线和虚线的长度都设置为布景的长度,那么布景初始时的显示是一条实线或一条虚线,通过最后一个参数偏移量的设置,令全部都是虚线(即空白)的图形不断的被虚线所填充,从而可以实现轨迹动画的效果。

Romain Guy提出的如上思路的确令人耳目一新,以Paint画笔特有的Dash实、虚线风格(即DashPathEffect),再借助动画的偏移量位移,从而可以实现轨迹偏移的动画效果,接下来学习实现这个抽象的思路。

上图中代码演示是Romain Guy博客中截取的内容,可见:

  1. 首先调用PathMeasure的getLength方法获取Path路径的全长度length;
  2. 接下来就是重点应用Dash风格效果:创建DashPathEffect,设置实线、虚线的长度都为length,而第三个参数则是起始偏移量偏移量;
  3. 最后将此效果设置到Paint画笔中,canvas绘制即可;
  4. 后续我们再自己创建动画,将DashPathEffect第三个参数偏移量改成动画指定的偏移量,即可完成实线、虚线交错(路径轨迹)的动画效果。

完整代码如下,配上注释并不难理解:

public class PathPaintView extends View 

    private Path mPath;
    private Paint mPaint;
    private float mLength;
    private float mAnimValue;
    private PathEffect mEffect;

    private PathMeasure mPathMeasure;

    public PathPaintView(Context context) 
        super(context);
    

    public PathPaintView(Context context, AttributeSet attrs) 
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPath = new Path();

        //绘制三角形
        mPath.moveTo(100, 100);
        mPath.lineTo(100, 500);
        mPath.lineTo(400, 300);
        mPath.close();

        //设置PathMeasure
        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, true);

        //获取轨迹路径全长度
        mLength = mPathMeasure.getLength();

        //设置动画,线性插值器数值从百分比[0,1]变化
        ValueAnimator animator = ValueAnimator.ofFloat(1, 0);
        animator.setDuration(2000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) 
                //获取动画偏移量
                mAnimValue = (float) valueAnimator.getAnimatedValue();
                //创建Paint画笔的DashPathEffect效果,三个参数分别为:实线、虚线长度、起始偏移量(通过变化的百分比乘以路径长度)
                mEffect = new DashPathEffect(new float[]mLength, mLength, mLength * mAnimValue);
                mPaint.setPathEffect(mEffect);
                //刷新UI
                invalidate();
            
        );
        animator.start();
    

    public PathPaintView(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    

绘制出的路径效果如下,可见这就是实线在不断替代虚线的过程,即虚线到实线的一个变化效果,这也就对应了以上代码中对动画值的变化设置是[1,0],如果设置成[0,1],则是实线到虚线的变化效果。由此可见,借助Paint的Dash实虚线变化效果,再结合 PathMeasure的辅助方法获取路径长度计算偏移量,即可以新的思路完成路径轨迹的效果动画。



3. getPosTan绘制切线实践

在介绍PathMeasure的基本方法中介绍过了getPosTan重点方法,通过一个简单的切线绘制demo来深入了解学习。

这里先给出效果,如上,以绘制的圆形作为辅助更容易理解切线的概念,将以上效果实现分成两个部分:小圆圈沿着圆的轨迹移动,切线沿着圆的轨迹移动,这些实现都要依赖getPosTan方法。首先来看第一个效果实现步骤:

  1. 在构造方法中创建并设置Paint画笔基本属性;创建Path路径添设置圆的轨迹;创建PathMeasure对象关联Path;创建getPosTan方法中需要的Pos、T an数组,留以后用;
  2. 在构造方法中创建接着创建动画,线性插值器,偏移量[0,1]变化,都是一些常规设置。
  3. onDraw方法中调用PathMeasure的getPosTan方法,注意回顾此方法要求的三个参数信息,分别是距离 Path 起点的长度(取值范围[0, getLength])、坐标值数组、切点数组,因此此处我们传入的参数分别是:动画偏移量百分比*length、两个新创建的数组。调用此方法后,后序绘制时可以利用Pos数组,即沿着圆轨迹移动的坐标值来绘制移动的小圆圈!
  4. 先使用canvas的drawPath绘制出大圆,接着调用drawCircle绘制沿着圆轨迹移动的小圆圈,而此方法传入的圆心坐标就是Pos数组!

绘制效果如上,接下来就是重头戏,绘制移动小圆圈相对于大圆的切线,此处需要用到讲解该API时的公式:

//用于获取路径上某点的切线角度
(math.atan2(tan[1], tan[0])*180.0 / math.PI)

通过以上公式可以获取到沿着圆轨迹移动的小圆圈的切线角度,有此角度后便可绘制不断变化的切线,此处有个小技巧,不需要多次重复绘制变化的切线,既然已经知晓变化的角度,直接调用canvas的rotate方法变化圆的形状即可,因为圆即使改变了角度也无任何变化,而其切线则会产生变化。

完整代码如下:

public class PathPosTanView extends View  implements View.OnClickListener

    private Path mPath;
    private float[] mPos;
    private float[] mTan;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private ValueAnimator mAnimator;
    private float mCurrentValue;

    public PathPosTanView(Context context) 
        super(context);
    

    public PathPosTanView(Context context, AttributeSet attrs) 
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPath.addCircle(0, 0, 200, Path.Direction.CW);

        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, false);

        mPos = new float[2];
        mTan = new float[2];

        setOnClickListener(this);

        mAnimator = ValueAnimator.ofFloat(0, 1);
        mAnimator.setDuration(3000);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) 
                mCurrentValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            
        );
    

    public PathPosTanView(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
    

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

        mPathMeasure.getPosTan(mCurrentValue * mPathMeasure.getLength(), mPos, mTan);
        float degree = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);

        canvas.save();
        canvas.translate(400, 400);
        canvas.drawPath(mPath, mPaint);
        canvas.drawCircle(mPos[0], mPos[1], 10, mPaint);
        canvas.rotate(degree);
        //相对坐标
        canvas.drawLine(0, -200, 300, -200, mPaint);
        canvas.restore();
    

    @Override
    public void onClick(View view) 
        mAnimator.start();
    




三. 综合实例 —— 搜索View

最后留一个常见的自定义View供读者自己奇思妙想去实现,除了用VectorDrawable实现,阅读过此篇文章可以轻松使用PathMeasure实现哟~

(此自定义控件本不打算贴源码,留给读者自行实现,但思量过后还是贴上,实现的具体步骤暂不分析,建议读者思索尝试过后再看源码)

public class SearchView extends View 

    // 画笔
    private Paint mPaint;

    // View 宽高
    private int mViewWidth;
    private int mViewHeight;

    // 这个视图拥有的状态
    public static enum State 
        NONE,
        STARTING,
        SEARCHING,
        ENDING
    

    // 当前的状态(非常重要)
    private State mCurrentState = State.NONE;

    // 放大镜与外部圆环
    private Path path_srarch;
    private Path path_circle;

    // 测量Path 并截取部分的工具
    private PathMeasure mMeasure;

    // 默认的动效周期 2s
    private int defaultDuration = 2000;

    // 控制各个过程的动画
    private ValueAnimator mStartingAnimator;
    private ValueAnimator mSearchingAnimator;
    private ValueAnimator mEndingAnimator;

    // 动画数值(用于控制动画状态,因为同一时间内只允许有一种状态出现,具体数值处理取决于当前状态)
    private float mAnimatorValue = 0;

    // 动效过程监听器
    private ValueAnimator.AnimatorUpdateListener mUpdateListener;
    private Animator.AnimatorListener mAnimatorListener;

    // 用于控制动画状态转换
    private Handler mAnimatorHandler;

    // 判断是否已经搜索结束
    private boolean isOver = false;

    private int count = 0;

    public SearchView(Context context) 
        super(context);

        initPaint();

        initPath();

        initListener();

        initHandler();

        initAnimator();

        // 进入开始动画
        mCurrentState = State.STARTING;
        mStartingAnimator.start();

    

    private void initPaint() 
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(15);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setAntiAlias(true);
    

    private void initPath() 
        path_srarch = new Path();
        path_circle = new Path();

        mMeasure = new PathMeasure();

        // 注意,不要到360度,否则内部会自动优化,测量不能取到需要的数值
        RectF oval1 = new RectF(-50, -50, 50, 50);          // 放大镜圆环
        path_srarch.addArc(oval1, 45, 359.9f);

        RectF oval2 = new RectF(-100, -100, 100, 100);      // 外部圆环
        path_circle.addArc(oval2, 45, -359.9f);

        float[] pos = new float[2];

        mMeasure.setPath(path_circle, false);               // 放大镜把手的位置
        mMeasure.getPosTan(0, pos, null);

        path_srarch.lineTo(pos[0], pos[1]);                 // 放大镜把手

        Log.i("TAG", "pos=" + pos[0] + ":" + pos[1]);
    

    private void initListener() 
        mUpdateListener = new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            
        ;

        mAnimatorListener = new Animator.AnimatorListener() 
            @Override
            public void onAnimationStart(Animator animation) 

            @Override
            public void onAnimationEnd(Animator animation) 
                // getHandle发消息通知动画状态更新
                mAnimatorHandler.sendEmptyMessage(0);
            

            @Override
            public void onAnimationCancel(Animator animation) 

            @Override
            public void onAnimationRepeat(Animator animation) 
        ;
    

    private void initHandler() 
        mAnimatorHandler = new Handler() 
            @Override
            public void handleMessage(Message msg) 
                super.handleMessage(msg);
                switch (mCurrentState) 
                    case STARTING:
                        // 从开始动画转换好搜索动画
                        isOver = false;
                        mCurrentState = State.SEARCHING;
                        mStartingAnimator.removeAllListeners();
                        mSearchingAnimator.start();
                        break;
                    case SEARCHING:
                        if (!isOver)   // 如果搜索未结束 则继续执行搜索动画
                            mSearchingAnimator.start();
                            Log.e("Update", "RESTART");

                            count++;
                            if (count>2)       // count大于2则进入结束状态
                                isOver = true;
                            
                         else         // 如果搜索已经结束 则进入结束动画
                            mCurrentState = State.ENDING;
                            mEndingAnimator.start();
                        
                        break;
                    case ENDING:
                        // 从结束动画转变为无状态
                        mCurrentState = State.NONE;
                        break;
                
            
        ;
    

    private void initAnimator() 
        mStartingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration);
        mSearchingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration);
        mEndingAnimator = ValueAnimator.ofFloat(1, 0).setDuration(defaultDuration);

        mStartingAnimator.addUpdateListener(mUpdateListener);
        mSearchingAnimator.addUpdateListener(mUpdateListener);
        mEndingAnimator.addUpdateListener(mUpdateListener);

        mStartingAnimator.addListener(mAnimatorListener);
        mSearchingAnimator.addListener(mAnimatorListener);
        mEndingAnimator.addListener(mAnimatorListener);
    


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
    

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

        drawSearch(canvas);
    

    private void drawSearch(Canvas canvas) 

        mPaint.setColor(Color.WHITE);


        canvas.translate(mViewWidth / 2, mViewHeight / 2);

        canvas.drawColor(Color.parseColor("#0082D7"));

        switch (mCurrentState) 
            case NONE:
                canvas.drawPath(path_srarch, mPaint);
                break;
            case STARTING:
                mMeasure.setPath(path_srarch, false);
                Path dst = new Path();
                mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst, true);
                canvas.drawPath(dst, mPaint);
                break;
            case SEARCHING:
                mMeasure.setPath(path_circle, false);
                Path dst2 = new Path();
                float stop = mMeasure.getLength() * mAnimatorValue;
                float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * 200f));
//                float start = stop-50;
                mMeasure.getSegment(start, stop, dst2, true);
                canvas.drawPath(dst2, mPaint);
                break;
            case ENDING:
                mMeasure.setPath(path_srarch, false);
                Path dst3 = new Path();
                mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst3, true);
                canvas.drawPath(dst3, mPaint);
                break;
        
    

若有错误,虚心指教~

以上是关于Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)的主要内容,如果未能解决你的问题,请参考以下文章

Android 高级UI解密 :Paint滤镜 与 颜色过滤(矩阵变换)

Android 高级UI解密 :Paint滤镜 与 颜色过滤(矩阵变换)

Android 高级UI解密 :结合Activity启动源码剖析View的诞生

Android 高级UI解密 :结合Activity启动源码剖析View的诞生

Android 高级UI解密 :结合Activity启动源码剖析View的诞生

Android 高级UI解密 :Canvas裁剪 与 二维三维Camera几何变换(图层Layer原理)