1.前言
在学习完paint和canvas之后,我们再来学习一下path,这样就可以在自定义控件的绘制这块领域为所欲为了!
本篇文章将首先介绍path的一些基本使用,之后按照国际惯例刷一波贝塞尔曲线,接着介绍pathMeasure的基础,最后针对其重点方法各举一例。
概括起来容易,篇幅还是很长滴,请大家在安心享用!
2.Path基本使用
2.1 Path.FillType
FillType是path中两个对象相交时展示的类型,一共只有四种。
1.WINDING 模式:取Path所有所在的区域(默认的模式)
2.EVEN_ODD 模式:取Path所在不相交的区域
3.INVERSE_WINDING 模式:取path所有未占的区域
4.INVERSE_EVEN_ODD 模式:取path所有未占和相交的区域
代码很简单,只需要调用path.setFillType()
即可,我们以WINDING模式为例
private void drawWindingType(Canvas canvas) {
mPath = new Path();
mPath.offset(100,100);
mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPath.addCircle(300, 300, 100, Path.Direction.CW);
mPath.setFillType(Path.FillType.WINDING);
mPaint.setColor(Color.RED);
canvas.drawPath(mPath,mPaint);
}
下面是效果图,从左到右,从上到下依次是四种类型,大家可以对照着解释和图片进行理解。
2.2 Path.Op
说完了path内部元素的交集,现在来介绍path与path之间的交集,Op想必大家都不会感到陌生,在之前的文章中已经多次出现。
1.DIFFERENCE:减去Path2后Path1区域剩下的部分
2.INTERSECT:保留Path2 和 Path1 共同的部分
3.UNION:保留Path1 和 Path 2
4.XOR:保留Path1 和 Path2 还有共同的部分
5.REVERSE_DIFFERENCE:减去Path1后Path2区域剩下的部分
下面以Path.Op.DIFFERENCE为例展示代码,其他几种情况都是类似的。在最后之所以又画了2个circle是为了方便展示。
private void drawDifferenceOp(Canvas canvas) {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
Path path1 = new Path();
path1.addCircle(150, 150, 100, Path.Direction.CW);
Path path2 = new Path();
path2.addCircle(200, 200, 100, Path.Direction.CW);
path1.op(path2, Path.Op.DIFFERENCE);
canvas.drawPath(path1, mPaint);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
canvas.drawCircle(150, 150, 100,mPaint);
canvas.drawCircle(200, 200, 100,mPaint);
}
效果图依然是从左到右,从上到下的顺序
2.3 Path其他用法
Path其他的API都十分顾名思义,使用起来也非常简单,不熟悉的同学自己下点功夫撸一遍。
这里再补充介绍下rLineTo()
,rMoveTo()
这样以r开头的方法,他们都是基于前一个点的相对位置,而普通的LineTo()
、MoveTo()
使用的都是绝对位置。再之后的例子中我们会使用到。
此外addCircle()
中的第三个参数需要说明下,Path.Direction.CCW是clockwise的意思,也就是是顺时针方向,而Path.Direction.CW就是逆时针方向。
3.贝塞尔曲线
3.1 Path绘制曲线
贝塞尔曲线是日常生活中我们用来绘制曲线的基本操作。在Android中,只需要调用path.quadTo(x1,y1,x2,y2)
即可进行绘制,其中(x1,y2)是控制点,(x2,y2)是结束点。
贝塞尔曲线有不同的阶数。比如二阶贝塞尔曲线需要3个坐标点,三阶需要4个,以此类推。大致的原理图如下
不同的阶数实现的曲线效果也是不同的,如果之前没有接触过,可以去Github上看看官方的效果图
下面我们就一起感受贝塞尔曲线到底有多牛逼吧。
3.2 水波纹效果
还记得之前我们使用xfermode实现的水波纹效果吗?该实现方式需要提前准备一张波浪的图片,而使用贝塞尔曲线就可以完全用paint画出来。
(终于学会弄gif图片了,所以先秀一下效果图)
首先是在构造方法中对paint和path进行初始化
private void init() {
mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWavePaint.setColor(Color.BLUE);
mWavePaint.setStyle(Paint.Style.FILL);
mWavePath = new Path();
startAnim();
}
接着重写onDraw()
方法。这里重点解释下for循环中的波浪绘制方法,这里用的是rQuadTo
方法,也就是相对位置,所以控制点是1/2波浪长度的1/2,而结束点是1/2波长。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//必须reset,否则会绘制重复导致奇怪的现象,并且GPU爆炸
mWavePath.reset();
//移动到屏幕底端
//dx用来水平移动,dy用来垂直移动
mWavePath.moveTo(-mItemWaveLength+dx, getHeight()+dy);
dy-=1;
int halfWaveLength = mItemWaveLength / 2;
//画曲线,左右各多出一个波浪,方便移动
for (int i = -mItemWaveLength; i < getWidth() + mItemWaveLength; i += mItemWaveLength) {
mWavePath.rQuadTo(halfWaveLength / 2, -50, halfWaveLength, 0);
mWavePath.rQuadTo(halfWaveLength / 2, 50, halfWaveLength, 0);
}
mWavePath.lineTo(getWidth(),getHeight());
mWavePath.lineTo(0,getHeight());
mWavePath.close();
canvas.drawPath(mWavePath, mWavePaint);
}
最后需要一个动画来让波浪动起来,我们选择让dx在0~波长之间递增,接着将动画类型设置为无限循环,最后在动画监听中通过postInvalidate()
进行重绘。
public void startAnim(){
ValueAnimator animator=ValueAnimator.ofInt(0,mItemWaveLength);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx= (int) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
大功告成,其实这就是写一些自定义控件的基本套路。
3.3 仿QQ拖动气泡
上面的水波纹比较简单,我们再来一个国标级的,其实也是大家耳熟能详的效果——仿QQ拖动气泡。如果你不知道这是什么,请打开QQ看看聊天界面右边的小红点。
首先我们要思考气泡有多少种状态,如下
/**
* 气泡默认状态--静止
*/
private final int BUBBLE_STATE_DEFAUL = 0;
/**
* 气泡相连
*/
private final int BUBBLE_STATE_CONNECT = 1;
/**
* 气泡分离
*/
private final int BUBBLE_STATE_APART = 2;
/**
* 气泡消失
*/
private final int BUBBLE_STATE_DISMISS = 3;
接着想想需要多少成员变量呢?还真不少,我把他们罗列出来
/**
* 气泡半径
*/
private float mBubbleRadius;
/**
* 气泡颜色
*/
private int mBubbleColor;
/**
* 气泡消息文字
*/
private String mTextStr;
/**
* 气泡消息文字颜色
*/
private int mTextColor;
/**
* 气泡消息文字大小
*/
private float mTextSize;
/**
* 不动气泡的半径
*/
private float mBubStillRadius;
/**
* 可动气泡的半径
*/
private float mBubMoveableRadius;
/**
* 不动气泡的圆心
*/
private PointF mBubStillCenter;
/**
* 可动气泡的圆心
*/
private PointF mBubMoveableCenter;
/**
* 气泡的画笔
*/
private Paint mBubblePaint;
/**
* 贝塞尔曲线path
*/
private Path mBezierPath;
/**
* 文字绘制的画笔
*/
private Paint mTextPaint;
/**
* 文字绘制的区域
*/
private Rect mTextRect;
/**
* 爆炸效果绘制的画笔
*/
private Paint mBurstPaint;
/**
* 爆炸效果绘制的区域
*/
private Rect mBurstRect;
/**
* 气泡状态标志
*/
private int mBubbleState = BUBBLE_STATE_DEFAUL;
/**
* 两气泡圆心距离
*/
private float mDist;
/**
* 气泡相连状态最大圆心距离
*/
private float mMaxDist;
/**
* 手指触摸偏移量
*/
private final float MOVE_OFFSET;
/**
* 气泡爆炸的bitmap数组
*/
private Bitmap[] mBurstBitmapsArray;
/**
* 是否在执行气泡爆炸动画
*/
private boolean mIsBurstAnimStart = false;
/**
* 当前气泡爆炸图片index
*/
private int mCurDrawableIndex;
/**
* 气泡爆炸的图片id数组
*/
private int[] mBurstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2
, R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};
国际惯例,我们需要在构造方法中初始化一些属性,这里用到了自定义的属性,注意array.recycle()
要记得调用啊。
public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView, defStyleAttr, 0);
mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, 12);
mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
mTextStr = array.getString(R.styleable.DragBubbleView_bubble_text);
mTextSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize, 12);
mTextColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE);
array.recycle();
mBubStillRadius = mBubbleRadius;
mBubMoveableRadius = mBubStillRadius;
mMaxDist = 8 * mBubbleRadius;
MOVE_OFFSET = mMaxDist / 8;//用来增大触摸范围
//抗锯齿
mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint.setColor(mBubbleColor);
mBubblePaint.setStyle(Paint.Style.FILL);
mBezierPath = new Path();
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextRect = new Rect();
mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBurstPaint.setFilterBitmap(true);
mBurstRect = new Rect();
mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];
for (int i = 0; i < mBurstDrawablesArray.length; i++) {
//将气泡爆炸的drawable转为bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
mBurstBitmapsArray[i] = bitmap;
}
}
那么如何确定气泡的位置呢?由于onSizeChanged()
会在view生成时调用,并会传入w和h两个位置参数,所以我们可以在这个方法中确定气泡圆心的位置
private void initView(int w, int h) {
//设置两气泡圆心初始坐标
if (mBubStillCenter == null) {
mBubStillCenter = new PointF(w / 2, h / 2);
} else {
mBubStillCenter.set(w / 2, h / 2);
}
if (mBubMoveableCenter == null) {
mBubMoveableCenter = new PointF(w / 2, h / 2);
} else {
mBubMoveableCenter.set(w / 2, h / 2);
}
mBubbleState = BUBBLE_STATE_DEFAUL;
}
下面进入绘制方法onDraw()
,由于气泡有不同的状态,所以要根据状态进行绘制。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1、画静止状态
// 2、画相连状态
// 3、画分离状态
// 4、画消失状态---爆炸动画
// 1、画拖拽的气泡 和 文字
if (mBubbleState != BUBBLE_STATE_DISMISS) {
canvas.drawCircle(mBubMoveableCenter.x, mBubMoveableCenter.y,
mBubMoveableRadius, mBubblePaint);
mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
canvas.drawText(mTextStr, mBubMoveableCenter.x - mTextRect.width() / 2,
mBubMoveableCenter.y + mTextRect.height() / 2, mTextPaint);
}
// 2、画相连的气泡状态
if (mBubbleState == BUBBLE_STATE_CONNECT) {
// 1、画静止气泡
canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y,
mBubStillRadius, mBubblePaint);
// 2、画相连曲线
// 计算控制点坐标,两个圆心的中点
int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);
int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);
float cosTheta = (mBubMoveableCenter.x - mBubStillCenter.x) / mDist;
float sinTheta = (mBubMoveableCenter.y - mBubStillCenter.y) / mDist;
float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;
float iBubStillStartY = mBubStillCenter.y + mBubStillRadius * cosTheta;
float iBubMoveableEndX = mBubMoveableCenter.x - mBubMoveableRadius * sinTheta;
float iBubMoveableEndY = mBubMoveableCenter.y + mBubMoveableRadius * cosTheta;
float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;
float iBubMoveableStartY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta;
float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;
float iBubStillEndY = mBubStillCenter.y - mBubStillRadius * cosTheta;
mBezierPath.reset();
// 画上半弧
mBezierPath.moveTo(iBubStillStartX, iBubStillStartY);
mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);
// 画上半弧
mBezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);
mBezierPath.quadTo(iAnchorX, iAnchorY, iBubStillEndX, iBubStillEndY);
mBezierPath.close();
canvas.drawPath(mBezierPath, mBubblePaint);
}
// 3、画消失状态---爆炸动画
if (mIsBurstAnimStart) {
mBurstRect.set((int) (mBubMoveableCenter.x - mBubMoveableRadius),
(int) (mBubMoveableCenter.y - mBubMoveableRadius),
(int) (mBubMoveableCenter.x + mBubMoveableRadius),
(int) (mBubMoveableCenter.y + mBubMoveableRadius));
canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null,
mBurstRect, mBubblePaint);
}
}
上面的代码,讲三个值得一提的地方
1.之前在paint基础中讲解了文字基线的内容,这里就用另一种方式,先根据文字长度获取文字范围,接着用画笔直接在这个范围内绘制文字。
2.中间一大串位置点的计算就涉及到数学中三角函数的知识了,如下图所示
3.爆炸效果的绘制是先计算爆炸范围,接着结合属性动画绘制不同的bitmap,关于动画我们最后在讲。
绘制方法是根据状态进行不同的绘制,而这个状态则是由用户通过触摸事件来决定的
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (mBubbleState != BUBBLE_STATE_DISMISS) {
mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x,
event.getY() - mBubStillCenter.y);
if (mDist < mBubbleRadius + MOVE_OFFSET) {
// 加上MOVE_OFFSET是为了方便拖拽
mBubbleState = BUBBLE_STATE_CONNECT;
} else {
mBubbleState = BUBBLE_STATE_DEFAUL;
}
}
}
break;
case MotionEvent.ACTION_MOVE: {
if (mBubbleState != BUBBLE_STATE_DEFAUL) {
mBubMoveableCenter.x = event.getX();
mBubMoveableCenter.y = event.getY();
mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x,
event.getY() - mBubStillCenter.y);
if (mBubbleState == BUBBLE_STATE_CONNECT) {
// 减去MOVE_OFFSET是为了让不动气泡半径到一个较小值时就直接消失
// 或者说是进入分离状态
if (mDist < mMaxDist - MOVE_OFFSET) {
mBubStillRadius = mBubbleRadius - mDist / 8;
} else {
mBubbleState = BUBBLE_STATE_APART;
}
}
invalidate();
}
}
break;
case MotionEvent.ACTION_UP: {
if (mBubbleState == BUBBLE_STATE_CONNECT) {
startBubbleRestAnim();
} else if (mBubbleState == BUBBLE_STATE_APART) {
if (mDist < 2 * mBubbleRadius) {
startBubbleRestAnim();
} else {
startBubbleBurstAnim();
}
}
}
break;
}
return true;
}
其中Math.hypot用来计算两点之间的距离,而MOVE_OFFSET存在的意义是方便用户进行拖动。
最后就是爆炸动画和重置效果了,我们先来看简单点的爆炸动画,这和之前水波纹的动画有类似之处,其核心思想就是在属性动画中改变某个值的状态,属性动画本身并没有产生什么动画效果。
private void startBubbleBurstAnim() {
//气泡改为消失状态
mBubbleState = BUBBLE_STATE_DISMISS;
mIsBurstAnimStart = true;
//做一个int型属性动画,从0~mBurstDrawablesArray.length结束
ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);
anim.setInterpolator(new LinearInterpolator());
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//设置当前绘制的爆炸图片index
mCurDrawableIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//修改动画执行标志
mIsBurstAnimStart = false;
}
});
anim.start();
}
接着是重置动画,这里让一个PointF在两个圆心之间变化,点睛之笔是使用了OvershootInterpolator插值器从而实现来回抖动的效果。
private void startBubbleRestAnim() {
ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
new PointF(mBubMoveableCenter.x, mBubMoveableCenter.y),
new PointF(mBubStillCenter.x, mBubStillCenter.y));
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator(5f));
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBubMoveableCenter = (PointF) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBubbleState = BUBBLE_STATE_DEFAUL;
}
});
anim.start();
}
到此为止,仿QQ气泡的基本的功能就算实现了。虽然代码量挺大,但这仅仅是个开始,我们只是单纯的实现了一个小球,如果想要将它放在listview中,它是无法拖动出当前item的范围的。完整的实现方式内容实在是太多啦,这里给出一个思路:最开始使用一个静态的控件,点击后隐藏,并通过windowmanager添加一个气泡到相应的位置,这时候气泡就能满屏幕拖动了。不过还要注意Touch事件的监听以及动画的播放。
关于完整的实现方式,附上一篇个人觉得不错的博客仿QQ拖动删除未读消息个数气泡之二,有时间的都去读读,写的真是妙啊,妙啊。
4.PathMeasure
顾名思义,PathMeasure是一个用来测量Path的类,所以说她是Path的童养媳(测量两个字真是太污了)
很多朋友都没接触过PathMeasure,所以我们从基础开始讲起
4.1 PathMeasure基本方法
构造方法
方法名 | 释义 |
---|---|
PathMeasure() | 创建一个空的PathMeasure |
PathMeasure(Path path, boolean forceClosed) | 创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)。 |
公共方法
返回值 | 方法名 | 释义 |
---|---|---|
void | setPath(Path path, boolean forceClosed) | 关联一个Path |
boolean | isClosed() | 是否闭合 |
float | getLength() | 获取Path的长度 |
boolean | nextContour() | 关联一个Path |
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) | 获取指定长度的位置坐标及该点Matrix(矩阵) |
有些地方看不明白没关系,我们用几个例子来进一步说明。
先看看初始化方法,这里准备了两个画笔,分别用来绘制坐标轴和路径
private void init() {
mDefaultPaint = new Paint();
mDefaultPaint.setColor(Color.RED);
mDefaultPaint.setStrokeWidth(5);
mDefaultPaint.setStyle(Paint.Style.STROKE);
mPaint = new Paint();
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.STROKE);
}
在onSizeChanged()
中获取控件的宽高,以便之后绘制坐标轴
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
}
onDraw()
方法首先平移了坐标轴,接着重新绘制x,y轴(为了更好的展示效果),如果对平移和坐标轴还不太了解,可以看之前介绍canvas的文章
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(mViewWidth / 2, mViewHeight / 2);
canvas.drawLine(-canvas.getWidth(), 0, canvas.getWidth(), 0, mPaint);
canvas.drawLine(0, -canvas.getHeight(), 0, canvas.getWidth(), mPaint);
// testForceClosed(canvas);
// testGetSegment(canvas);
// testGetSegmentMoveTo(canvas);
// testNextContour(canvas);
}
下面我们依次介绍被注释掉的四个方法
private void testForceClosed(Canvas canvas) {
Path path = new Path();
path.lineTo(0, 200);
path.lineTo(200, 200);
path.lineTo(200, 0);
PathMeasure measure1 = new PathMeasure(path, false);
PathMeasure measure2 = new PathMeasure(path, true);
Log.e(TAG, "forceClosed=false length = " + measure1.getLength());
Log.e(TAG, "forceClosed=true length = " + measure2.getLength());
canvas.drawPath(path, mDefaultPaint);
}
在testForceClosed
中,path由三条直线组成,赋值给PathMeasure 后,measure1的ForceClosed为true,measure2为false,打印出的长度分别为600,800。是不是很好理解ForceClosed这个参数的意思了?
testGetSegment
中,我们画了一个矩形,并截取其中200-600长度的部分
private void testGetSegment(Canvas canvas) {
Path path = new Path();
// 创建Path并添加了一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();
// 将 Path 与 PathMeasure 关联
PathMeasure measure = new PathMeasure(path, false);
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200,600,dst,false);
canvas.drawPath(path,mPaint);
// 绘制 dst
canvas.drawPath(dst, mDefaultPaint);
}
注意此处getSegment
的startWithMoveTo参数为false,效果图如下
testGetSegmentMoveTo()
只是将startWithMoveTo参数改为true,其结果如下
一般情况下,我们都会将该参数设置为true,后面的进阶中会有更好的例子。
最后看testNextContour()
,这里添加了两条path,通过op合并后赋值给pathmeasure,之后通过nextContour()
分别获取他们的长度,结果为1600.0和800.0
private void testNextContour(Canvas canvas) {
Path path = new Path();
Path path1 = new Path();
Path path2 = new Path();
// 添加小矩形
path1.addRect(-100, -100, 100, 100, Path.Direction.CW);
// 添加大矩形
//path.addRect(-200, 200, 200, 600, Path.Direction.CW);
path2.addRect(-200, -200, 200, 200, Path.Direction.CW);
path.op(path1,path2, Path.Op.XOR);
canvas.drawPath(path,mDefaultPaint);
PathMeasure measure = new PathMeasure(path, false);
float len1 = measure.getLength();
// 跳转到下一条路径
measure.nextContour();
float len2 = measure.getLength();
Log.d(TAG,"len1 = "+len1);
Log.d(TAG,"len2 = "+len2);
}
看到这里,你应该对PathMeasure有了最基本的认识,接下来就可以进入到PathMeasure的高级使用了。
4.2 PathMeasure高级用法
4.2.1 getPosTan与getMatrix
getPosTan
参数 | 作用 | 备注 |
---|---|---|
返回值(boolean) | 判断获取是否成功 | true表示成功,数据会存入 pos 和 tan 中,false 表示失败,pos 和 tan 不会改变 |
distance | 距离 Path 起点的长度 | 取值范围: 0 <= distance <= getLength |
pos | 该点的坐标值 | 坐标值: (x==[0], y==[1]) |
tan | tan 该点的正切值 | 坐标值: (x==[0], y==[1]) |
getMatrix
参数 | 作用 | 备注 |
---|---|---|
返回值(boolean) | 判断获取是否成功 | true表示成功,数据会存入matrix中,false 失败,matrix内容不会改变 |
distance | 距离 Path 起点的长度 | 取值范围: 0 <= distance <= getLength |
matrix | 根据 falgs 封装好的matrix | 会根据 flags 的设置而存入不同的内容 |
flags | 规定哪些内容会存入到matrix中 | 可选择POSITION_MATRIX_FLAG(位置) ANGENT_MATRIX_FLAG(正切) |
getPosTan()
是计算出path上某一点的坐标与tan值,因此需要传入pos、tan两个长度为2的数组用于值的存放。此外,我们还在初始化方法中添加了一张箭头的图片,该箭头朝左,也就是X轴正轴的方向。
private void init(Context context) {
pos = new float[2];
tan = new float[2];
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
mMatrix = new Matrix();
...省略画笔的初始化
}
其他的平移与坐标轴绘制还是老样子就不展示出来了,剩下的重点都在onDraw()
方法中
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...省略平移绘制
Path path = new Path(); // 创建 Path
path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
if (currentValue >= 1) {
currentValue = 0;
}
// 方案一
// 获取当前位置的坐标以及趋势
measure.getPosTan(measure.getLength() * currentValue, pos, tan);
// 重置Matrix
mMatrix.reset();
// 计算图片旋转角度
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
// 旋转图片(本来方向朝左,即x轴正轴方向)
mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
// 将图片绘制中心调整到与当前点重合(本来是在中心点的)
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);
// // 方案二
// // 获取当前位置的坐标以及趋势的矩阵
// measure.getMatrix(measure.getLength() * currentValue, mMatrix,
// PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
//
// // 将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
// mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
//
canvas.drawPath(path, mDeafultPaint);
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);
invalidate();
}
先看方案一,首先我们获取了圆上某个点的坐标与tan值,其中tan0是邻边边长,tan1是对边边长,而Math中 atan2 方法是根据正切是数值计算出该角度的大小,得到的单位是弧度。接着根据tan值计算出角度degree,然后以箭头图的中心为圆心旋转degree度,再将箭头图平移到圆的边上,最后绘制出来。这就是getPosTan
的用法,比较麻烦需要我们自己去计算,相对的自由度也比较高。
至于方案二,直接将长度、矩阵和要测量的pos、tan两个FLAG传入getMatrix()
方法中,可以直接获取到对应的matrix。相比方案一显得更加简单,但自由度比较小。
两种方案都可以实现上图的效果。
4.2.1 getSegment
参数 | 作用 | 备注 |
---|---|---|
返回值(boolean) | 判断获取是否成功 | true 表示截取成功,结果存入dst中,false 截取失败,不会改变dst中内容 |
startD | 开始截取位置距离 Path 起点的长度 | 取值范围: 0 <= startD < stopD <= Path总长度 |
stopD | 结束截取位置距离 Path 起点的长度 | 取值范围: 0 <= startD < stopD <= Path总长度 |
dst | t截取的 Path 将会添加到 dst 中 | 注意: 是添加,而不是替换 |
startWithMoveTo | 起始点是否使用 moveTo | 用于保证截取的 Path 第一个点位置不变 |
按照国际惯例,先看初始化方法,这里直接搞了个属性动画
...省略画笔的初始化
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(2000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.start();
重点在onDraw()
中通过mAnimatorValue动态改变stop和start的值。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDst.reset();
// 硬件加速的BUG
mDst.lineTo(0,0);
float stop = mLength * mAnimatorValue;
float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength));
mPathMeasure.getSegment(start, stop, mDst, true);
canvas.drawPath(mDst, mPaint);
}
注意,安卓4.4或者之前的版本,在开启硬件加速的情况下,更改 dst 的内容后会出现问题,要关闭硬件加速或者给 dst 添加一个操作比如dst.rLineTo(0, 0)
5.总结
艾玛这么多东西真是累死我了。。。。