前面几篇文章已经按照顺序讲解了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. 公共方法

返回值 方法名 释义
void setPath(Path path, boolean forceClosed) 关联一个Path
boolean isClosed() 是否闭合
float getLength() 获取Path的长度
boolean nextContour() 跳转到下一个轮廓
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
boolean getPosTan(float distance, float[] pos, float[] tan) 获取指定长度的位置坐标及该点切线值tangle
boolean getMatrix(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;        }    }}

若有错误,虚心指教~

更多相关文章

  1. Android属性动画完全解析(下) Interpolator和ViewPropertyAnimat
  2. 一次重拾Android(安卓)Studio开发的经历
  3. Android从源码的角度彻底理解事件分发机制的解析(下)
  4. Android(安卓)apk安装过程及Java、JNI读取安装包内assets资源文
  5. Android(安卓)NDK开发(1)----- Java与C互相调用实例详解
  6. android 学习九 Fragments 介绍(android3.0及4.0与之前版本区别的
  7. Android基础知识巩固系列 Android之四大组件——Service(服务)
  8. [置顶] 解决android某些应用开发某些类无法解析/找到的问题--使
  9. Android面试真题,了解一下?

随机推荐

  1. Linux下卸载MySQL数据库
  2. mysql 触发器用法实例详解
  3. Mysql如何巧妙的绕过未知字段名详解
  4. SQL计算timestamp的差值的方法
  5. MySql 5.6.36 64位绿色版安装图文教程
  6. MySQL 5.6.36 Windows x64位版本的安装教
  7. MySQL基础教程第一篇 mysql5.7.18安装和
  8. 浅谈MySQL event 计划任务
  9. mysql中的跨库关联查询方法
  10. MYSQL实现连续签到功能断签一天从头开始(