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. 公共方法
返回值 | 方法名 | 释义 |
---|---|---|
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])
;
作用:用于获取路径上某点的坐标以及该位置的正切值,即切线的坐标。相当于是getPos
、getTan
两个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动画效果
- 在自定义View构造方法中调用Paint的
setStyle
、setStrokeWidth
方法初始化画笔基本属性。 - 创建Path路径对象,绘制一个空心圆;创建PathMeasure对象,调用
setPath
方法关联Path,并调用getLength
获取路径长度,创建Dst对象,后续会使用。 - 创建动画ValueAnimator,调用
ofFloat(0, 1)
方法,此处的(0, 1)
范围代表百分比例,即绘制圆的比例从0到100%。再设置线性插值器和循环播放,重点在于实现动画的监听事件中获取变化的比例值赋值给成员变量,调用invalidate();
刷新。 - 以上都是在构造方法中实现,准备就绪后,接下来在
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博客中截取的内容,可见:
- 首先调用PathMeasure的
getLength
方法获取Path路径的全长度length; - 接下来就是重点应用Dash风格效果:创建DashPathEffect,设置实线、虚线的长度都为length,而第三个参数则是起始偏移量偏移量;
- 最后将此效果设置到Paint画笔中,canvas绘制即可;
- 后续我们再自己创建动画,将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
方法。首先来看第一个效果实现步骤:
- 在构造方法中创建并设置Paint画笔基本属性;创建Path路径添设置圆的轨迹;创建PathMeasure对象关联Path;创建
getPosTan
方法中需要的Pos、T an数组,留以后用; - 在构造方法中创建接着创建动画,线性插值器,偏移量[0,1]变化,都是一些常规设置。
- 在
onDraw
方法中调用PathMeasure的getPosTan
方法,注意回顾此方法要求的三个参数信息,分别是距离 Path 起点的长度(取值范围[0, getLength]
)、坐标值数组、切点数组,因此此处我们传入的参数分别是:动画偏移量百分比*length、两个新创建的数组。调用此方法后,后序绘制时可以利用Pos数组,即沿着圆轨迹移动的坐标值来绘制移动的小圆圈! - 先使用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属性动画完全解析(下) Interpolator和ViewPropertyAnimat
- 一次重拾Android(安卓)Studio开发的经历
- Android从源码的角度彻底理解事件分发机制的解析(下)
- Android(安卓)apk安装过程及Java、JNI读取安装包内assets资源文
- Android(安卓)NDK开发(1)----- Java与C互相调用实例详解
- android 学习九 Fragments 介绍(android3.0及4.0与之前版本区别的
- Android基础知识巩固系列 Android之四大组件——Service(服务)
- [置顶] 解决android某些应用开发某些类无法解析/找到的问题--使
- Android面试真题,了解一下?