浅谈Android PathMeasure详解和应用
PathMeasure,顾名思义,就是一个用来测量Path的类,主要有以下方法:
构造方法
无参构造方法:
PathMeasure()
创建一个空的PathMeasure,用这个构造函数可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。
有参构造方法
PathMeasure(Path path, boolean forceClosed)
该构造函数是创建一个 PathMeasure 并关联一个 Path, 其实和创建一个空的 PathMeasure 后调用 setPath 进行关联效果是一样的,同样,被关联的 Path 也必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。 该方法有两个参数,第一个参数自然就是被关联的 Path 了,第二个参数是用来确保 Path 闭合,如果设置为 true, 则不论之前Path是否闭合,都会自动闭合该 Path(如果Path可以闭合的话)。
这里需要说明以下forceClosed:
1)不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path的状态,即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。
2)forceClosed 的设置状态可能会影响测量结果,如果 Path 没有闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取得到的是该 Path 闭合时的状态。
setPath
setPath(Path path, boolean forceClosed)方法就是关联一个Path,需要预先创建好。
isClosed
isClosed方法用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为true。
getLength
getLength()方法用于获取Path的长度。
public class PathMeasureView extends View { private static final String TAG = "lwj"; private int mViewHeight; private int mViewWidth; private Paint paint; public PathMeasureView(Context context) { super(context); init(context); } private void init(Context context) { paint = new Paint(); paint.setColor(Color.RED); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(mViewWidth/2, mViewHeight/2); Path path = new Path(); path.lineTo(0, 300); path.lineTo(300, 300); path.lineTo(300, 0); PathMeasure measure = new PathMeasure(path, false); PathMeasure measure2 = new PathMeasure(path, true); Log.i(TAG, "length:"+measure.getLength());//900 Log.i(TAG,"length:"+ measure2.getLength());//1200 canvas.drawPath(path, paint); } //该方法在当前View尺寸变化时被调用 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mViewHeight = h; mViewWidth = w; } }
nextContour
我们知道 Path 可以由多条曲线构成,但不论是 getLength , getgetSegment 或者是其它方法,都只会在其中第一条线段上运行,而这个 nextContour 就是用于跳转到下一条曲线到方法,如果跳转成功,则返回 true, 如果跳转失败,则返回 false。 注意:使用多路径的效果需要关闭硬件加速。
setLayerType(View.LAYER_TYPE_SOFTWARE, null); Path path = new Path(); path.addRect(-200, -200, 200, 200, Path.Direction.CW); path.addRect(-100, -100, 100, 100, Path.Direction.CW); PathMeasure measure = new PathMeasure(path, false); float length = measure.getLength(); //获取下一个路径,有可能没有多个路径了,返回false boolean nextContour = measure.nextContour(); float length2 = measure.getLength(); Log.i("damon", "length1:"+length); Log.i("damon", "length2:"+length2); canvas.drawPath(path, paint);
getSegment
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo):用于获取Path的一个片段。
解析:
1)返回值(boolean):判断截取是否成功,true 表示截取成功,结果存入dst中,false 截取失败,不会改变dst中内容。
2)startD:开始截取位置距离 Path 起点的长度 取值范围: 0 <= startD < stopD <= Path总长度;
3)stopD:结束截取位置距离 Path 起点的长度 取值范围: 0 <= startD < stopD <= Path总长度;
4)dst:截取的 Path 将会添加到 dst 中 注意: 是添加,而不是替换;
5)startWithMoveTo:起始点是否使用 moveTo,用于保证截取的 Path 第一个点位置不变。
注意:
• 如果 startD、stopD 的数值不在取值范围 [0, getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst 内容。
• 如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)
• 可以用以下规则来判断 startWithMoveTo 的取值:
true:保证截取得到的 Path 片段不会发生形变;
false:保证存储截取片段的 Path(dst) 的连续性。
Path path = new Path(); //多路径的效果需要关闭硬件加速!! path.addRect(-200, -200, 200, 200, Path.Direction.CW); PathMeasure measure = new PathMeasure(path, false); float length = measure.getLength(); Log.i("damon", "length1:"+length); canvas.drawPath(path, paint); Path dst = new Path(); dst.lineTo(-300, -300); //startWithMoveTo:false,代表该起始点是否位上一个的结束点(是否保持连续性)。 measure.getSegment(200, 600, dst , false); paint.setColor(Color.RED); canvas.drawPath(dst, paint);
getMatrix
getMatrix(float distance, Matrix matrix, int flags):获取路径上某一长度的位置以及该位置的正切值的矩阵。
返回值(boolean):判断获取是否成功,true表示成功,数据会存入matrix中,false 失败,matrix内容不会改变;
distance:距离 Path 起点的长度,取值范围: 0 <= distance <= getLength;
matrix:根据 falgs 封装好的matrix 会根据 flags 的设置而存入不同的内容;
flags:规定哪些内容会存入到matrix中,可选择:
POSITION_MATRIX_FLAG(位置)
ANGENT_MATRIX_FLAG(正切)
getPosTan
getPosTan(float distance, float[] pos, float[] tan):获取指定长度的位置坐标及该点切线值tangle。
返回值(boolean):判断获取是否成功,true表示成功,数据会存入 pos 和 tan 中, false 表示失败,pos 和 tan 不会改变;
distance:距离 Path 起点的长度,取值范围: 0 <= distance <= getLength;
pos:该点的坐标值,坐标值: (x==[0], y==[1]);
tan:该点的正切值,正切值: (x==[0], y==[1])。
通过 三角函数tan 得值计算出图片旋转的角度,tan 是 tangent 的缩写, 其中tan0是邻边边长,tan1是对边边长,而Math中 atan2 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度,所以上面又将弧度转为了角度。
Path path = new Path(); path.addCircle(0, 0, 300, Path.Direction.CW); PathMeasure measure = new PathMeasure(path, false); float[] pos = new float[2]; float[] tan = new float[2];//tan=y/x measure.getPosTan(measure.getLength()/4, pos , tan ); Log.i("damon", "position:x-"+pos[0]+", y-"+pos[1]); Log.i("damon", "tan:x-"+tan[0]+", y-"+tan[1]); canvas.drawPath(path, paint);
应用
绘制一个放大镜,然后慢慢沿着放大镜的路径慢慢撤退消失,变成圆形搜索的loading,接着loading完成之后,沿着路径绘制出放大镜。 如效果图所示:
这样一个自定义View,需要用到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; } } }
以上就是关于PathMeasure的详解和应用,需要读者去多点动手才能理解其中的精髓的地方。