Android Path和PathMeasure

本文继续介绍自定义控件的相关内容,Path在一些高级特效中使用相当广泛,如一些加载特效、刮刮卡和前段时间的比较火的撕衣应用。

Path一般结合xfermode或者贝塞尔曲线做一些很炫的交互效果,有关贝塞尔曲线的内容这里暂不涉及,本文主要介绍Path的一些简单基础用法,相关的类还包括PathMeasure,需要掌握PathMeasure的两个方法:getLength()、getSegment。

Path封装了由直线和曲线(二阶,三阶贝塞尔曲线)构成的几何路径。你能用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也可以用于剪裁画布和根据路径绘制文字。我们有时会用Path来描述一个图像的轮廓,所以也会称为轮廓线(轮廓线仅是Path的一种使用方法,两者并不等价)。

PathMeasure

PathMeasure是用来测量Path路径长度的类,可以通过getLength()获取Path的长度,但是该值的大小与forceClosed强相关,forceClosed可以通过如下方法传入:

public PathMeasure(Path path, boolean forceClosed)
public void setPath(Path path, boolean forceClosed)

forceClosed就是Path最终是否需要闭合,如果为true的话,则不管关联的Path是否是闭合的,都会被闭合,forceClosed对绑定的Path不会产生任何影响。

可以通过如下一个简单的代码测试一下影响:

path.moveTo(100,100);
path.lineTo(100,300);
path.lineTo(300,300);
path.lineTo(300,100);
pathMeasure=new PathMeasure(path,true);
float length=pathMeasure.getLength();
Log.d(TAG, "MyPathView: length="+length);

上述代码Path路径就是绘制一个不闭合的正方形,如果将PathMeasure的第二个参数forceClosed传入true,这是length为800,如果forceClosed为false,那么这时length为600。

一定要注意这里PathMeasure的forceClosed参数与Path类的close()方法的差异,PathMeasure的forceClosed参数并不会影响视觉上的绘制,就是说如果绘制Path不是闭合图像,这时候视觉上仍然是不闭合的。

但是Path的close()方法会影响图像,close()方法的意思是将当前点与初始点连接在一起,如果当前点与初始点不在一个点,这时候会将两个点连接在一起,而且在视觉上可以看到效果。如果连接了最后一个点和初始点仍然无法形成封闭图形,则close()方法什么也不做。

Direction

在使用Path绘制图形时有一个Direction类型的参数,该参数有两个值CW和CCW,其中CW是顺时针的绘制,CCW是逆时针绘制。

public void addCircle(float x, float y, float radius, Direction dir)
public void addOval(RectF oval, Direction dir)
public void addRect(RectF rect, Direction dir)
public void addRoundRect(RectF rect, float rx, float ry, Direction dir)
Direction在绘制时一般没有太大的差异,但是如果涉及到某些特效时,需要特别注意。

下面通过示例代码运行截图对比一下:

// 顺时针
path.addCircle(500, 400, 300, Path.Direction.CW);
canvas.drawPath(path, paint);
canvas.drawTextOnPath(str, path, 0, 0, paint);

path.reset();

// 逆时针
path.addCircle(500, 1100, 300, Path.Direction.CCW);
canvas.drawPath(path, paint);
canvas.drawTextOnPath(str, path, 0, 0, paint);

PathMeasure的getSegment方法

public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

该方法是一个boolean类型的方法,主要用于截取指定Path的片段到dst中,通过参数startD和stopD来控制截取的长度,最后一个参数startWithMoveTo表示起始点是否使用moveTo方法,通常为True,保证每次截取的Path片段都是正常的、完整的。

如果startWithMoveTo设置为false,通常是和dst一起使用,因为dst中保存的Path是被不断添加的,而不是每次被覆盖,设置为false,则新增的片段会从上一次Path终点开始计算,这样可以保存截取的Path片段数组连续起来。

在Android kitkat版本及之前的版本,该方法所获取到的path可能由于硬件加速问题导致无法被绘制显示出来的。要解决该问题的简单方法就是在所要获取的path上执行lineTo(0, 0) 或执行rLineTo(0, 0)。

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

	paint = new Paint();
	paint.setAntiAlias(true);
	paint.setColor(Color.RED);
	paint.setStyle(Paint.Style.STROKE);
	paint.setStrokeWidth(10);

	dstPath = new Path();
	path = new Path();
	path.addCircle(500, 500, 200, Path.Direction.CW);

	pathMeasure = new PathMeasure(path, false);
	length = pathMeasure.getLength();

	final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
	valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
		@Override
		public void onAnimationUpdate(ValueAnimator valueAnimator) {
			animatorValue = (float) valueAnimator.getAnimatedValue();
			invalidate();
		}
	});
	valueAnimator.setDuration(1300);
	valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
	valueAnimator.start();
}

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	dstPath.reset();
	dstPath.lineTo(0, 0);
	float stop = length * animatorValue;
	float start = (float) (stop - ((0.5 - Math.abs(animatorValue - 0.5)) * length));
	pathMeasure.getSegment(start, stop, dstPath, true);
	canvas.drawPath(dstPath, paint);
}

Path与xfermode示例

Path也经常与xfermode组合使用,如下是一个简单刮刮卡示例,如果有兴趣可以参看Bitmap学习笔记

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

	paint = new Paint();
	paint.setAntiAlias(true);
	paint.setStyle(Paint.Style.STROKE);
	paint.setStrokeCap(Paint.Cap.ROUND);
	paint.setStrokeJoin(Paint.Join.ROUND);
	paint.setStrokeWidth(30);
	//paint.setAlpha(0);
	//paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
	paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));

	path = new Path();
	bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);

	//双缓冲机制创建前景灰色图层
	foreground = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
	myCanvas = new Canvas(foreground);
	myCanvas.drawColor(Color.GRAY);
}

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	canvas.drawBitmap(bitmap, 0, 0, null);
	canvas.drawBitmap(foreground, 0, 0, null);

	myCanvas.drawPath(path, paint);
}

public boolean onTouchEvent(MotionEvent event) {
	switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			startX = (int) event.getX();
			startY = (int) event.getY();
			path.moveTo(startX, startY);
			break;
		case MotionEvent.ACTION_MOVE:
			int stopX = (int) event.getX();
			int stopY = (int) event.getY();
			path.lineTo(stopX, stopY);
			startX = stopX;
			startY = stopY;
			break;
	}
	invalidate();
	return true;
}

小结

本文介绍的内容不多,都是一些Path的简单基础的用法,包括如何获取绘制Path的长度,如何截取绘制Path的部分片段,通过Path和xfermode实现一个刮刮卡效果。将Path简单的用法使用熟练后,然后再结合其它相关类就可以实现一些很炫很酷的交互效果。

下一篇文章中会介绍贝塞尔曲线相关的内容,贝塞尔曲线实现的交互确实要比正余弦函数实现的交互要平滑许多,比如加入购物车或者加载的波纹效果都会使用贝塞尔曲线。

评论

您确定要删除吗?删除之后不可恢复