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. 公共方法
返回值 | 方法名 | 释义 |
---|---|---|
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 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)的主要内容,如果未能解决你的问题,请参考以下文章
Android 高级UI解密 :Paint滤镜 与 颜色过滤(矩阵变换)
Android 高级UI解密 :Paint滤镜 与 颜色过滤(矩阵变换)
Android 高级UI解密 :结合Activity启动源码剖析View的诞生
Android 高级UI解密 :结合Activity启动源码剖析View的诞生