一.Measure测量:
决定view大小需要两个值:widthMeasureSpec(宽详细测量值),heightMeasureSpec(高详细测量值)。 MeasureSec由两个元素组成:size(大小),mode(模式)。size,mode,MeasureSpc这三者的关系封装在MeasureSpec里。
- MeasureSpec
//MeasureSpec包含了父布局传递给子布局的布局要求;
//MeasureSpec由size 和 model组成,其中mode有三者可能的值;
//UNSPECIFIED:父布局未对子布局添加任何约束,子布局可以是任意大小;
//EXACTLY:父布局确定了子布局的大小,不管子布局本身想要多大的大小,它的边界都将限制在父布局给的大小里;
//AT_MOST:子布局的大小由它自己决定,但父布局会限定一个最大值。
public static class MeasureSpec {
//MeasureSpec 利用30位int的前两位代表mode,后30位代表大小
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//00向左移30位,即00 0000000...(后面30个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01向左移30位,即01 0000000...(后面30个0)
public static final int EXACTLY = 1 << MODE_SHIFT;
//11向左移30位,即11 00000000...(后面30个0)
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 通过位运算获取mode
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
*通过为运算获取size
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
复制代码
- 在自定义View是需要通过重写OnMeasure()来确定view的大小;
OnMeasure()源码
/**
* 测量view的宽高值;
* @param widthMeasureSpec 当前view的宽详细测量值(由父布局指定);
* @param heightMeasureSpec 当前view的高详细测量值(由父布局指定);
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}
/**
* 记录测量的宽高大小值,这个方法只能在onMeasure()中调用
* @param measuredWidth 测量的宽度值;
* @param measuredHeight 测量的高度值;
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
/**
*获取建议的宽度的最小值;
*/
protected int getSuggestedMinimumWidth() {
// mBackground.getMinimumWidth()指的是mBackgroud所对应的Drawable的最小宽度值
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
/**
* 获取view默认的宽高值;
* @param size view默认的宽度或者高度;
* @param measureSpec 由父布局添加的宽高约束;
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
复制代码
- 父布局指定子布局宽高详细值(MeasureSpec)的过程
/**
* 对于ViewGroup而言,在执行测量过程时,需要测量所有childView的宽高;
* @param widthMeasureSpec 当前viewGroup的宽度详细测量值;
* @param heightMeasureSpec 当前viewGroup的高度详细测量值;
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec){
final int size = mChildrenCount;
final View[] children = mChildren;
//测量所有的child view的宽高
for (int i = 0; i < size; ++i) {
final View child = children[i];
//如果child view位置可见,则进行测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
*测量child view的宽高值
* @param child 需要测量的child view
* @param parentWidthMeasureSpec 父布局的宽度详细值;
* @param parentHeightMeasureSpec 父布局的高度详细值;
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取child view的布局参数
final LayoutParams lp = child.getLayoutParams();
//获取child view的宽度详细值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//获取child view的高度详细值
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 测量child view的宽高
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/**
* 获取child view的宽高详细值(MeasureSpec)
* @param spec 父布局的宽高详细值(MeasureSpec)
* @param padding 父布局的内边距
* @param childDimension child的布局参数中的宽高大小值。
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父布局的宽高布局模式
int specMode = MeasureSpec.getMode(spec);
//获取父布局的宽高大小值
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
/*通过父布局的布局模式和child view的布局参数(即LayoutParams)中的
宽高参数(LayoutParams.width和LayoutParams.height)来确定child view的MeasureSpec*/
switch (specMode) {
//当父布局模式为EXACTY(一般父布局的宽高参数设置为match_parent或者固定值)时
case MeasureSpec.EXACTLY:
// childDimension > 0,即当子布局有确切值时
if (childDimension >= 0) {
//child view的大小为自身布局参数中的宽高大小,大小模式为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//child view布局参数中的宽高值为MATCH_PARENT(-1)时
} else if (childDimension == LayoutParams.MATCH_PARENT {
//child view的大小为父布局的大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//child view布局参数中的宽高值为WRAP_CONTENT(-2)时
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child view的大小由自己决定,当父布局会给其约束一个最大值,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//当父布局的布局模式为AT_MOST(即父布局大小由最大值约束,一把是父布局的布局参数中大小设置为wrap_content)时
case MeasureSpec.AT_MOST:
//子布局有明确的大小值
if (childDimension >= 0) {
//child view的大小为自身确定的的大小值,模式为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//child view的布局参数中的大小设置为MATCH_PARENT(-1)时
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child view的大小为父布局的大小,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
//child view的布局参数中大小设置为WRAP_CONTENT(-2)时
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// child view的大小为父布局的大小,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父布局的大小为UNSPECIFIED(即没有指定大小,父布局要多大就给多大)时
case MeasureSpec.UNSPECIFIED:
//子布局有明确的大小值
if (childDimension >= 0) {
//child view的大小为自身确定的的大小值,模式为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//child view的布局参数中的大小设置为MATCH_PARENT(-1)时
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//通过sUseZeroUnspecifiedMeasureSpec(android 6.0以上为false,6.0一下为true)
//这个标志位来决定child view的大小是否为0或父布局大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec()来决定 ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
//child view的布局参数中大小设置为WRAP_CONTENT(-2)时
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//同上
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
二.Layout布局
Layout的作用是确定view在位置。对于自定义ViewGroup而言需要通过onLayout()来确定child view的位置。
//ViewGroup#onLayout
@Override
protected void onLayout(boolean changed,int l, int t, int r, int b){
int count = getChildCount();
for(int i = 0; i < count; i++){
View childView = getChild(i);
int l;
int t;
int r;
int b;
//通过计算分别给l,t,r,b的值来确定每个子布局相对于父布局的位置
...
child.layout(l,t,r,b);
}
}
复制代码
child.layout(l,t,r,b)四个参数说明:
名称 | 说明 | 对应函数 |
---|---|---|
l | 子布局相对于父布局左侧的距离 | getLeft() |
t | 子布局相对于父布局顶部的距离 | getTop() |
r | 子布局相对于父布局右侧的距离 | getRight() |
b | 子布局相对于父布局底部的距离 | getBottom() |
三.Draw绘制
1.android坐标系
1.android中的默认坐标系都是以左上角为原点,X轴向右方向为正方向,Y轴向下放下为正方向,这与传统的数学坐标系相反不同
2.android 中View的坐标是相对于父控件而言的:
canvans绘制基本图形
- 1.Paint常用api介绍:
//创建一个画笔
private Paint mPaint = new Paint();
//设置画笔颜色
mPaint.setColor(Color.Blue);
//设置画笔的填充模式
mPaint.setStyle(Paint.Style.Fill);
//设置画笔的宽度
mPaint.setStrokeWidth(10f);
//设置字体大小
mPaint.setTextSize();
//测量所绘制文字的宽度
mPaint.measureText(str);
//设置盖帽模式
mPaint.setStrokeCap(Paint.Cap.ROUND)
//设置抗锯齿
mPaint.setAntiAlias(true);
复制代码
其中Paint盖帽有三种模式,效果如下图:
关于Paint API的详细介绍可以参考这篇文章
-
2.canvas绘制基本图形:
- 绘制点:
//api /** * 绘制多个点 * @param pts 绘制点的坐标集合 * @param paint 画笔 */ public void drawPoint(float[] pts,Paint paint); /** * 绘制单个点 * @param x 绘制点的x坐标 * @param y 绘制点的y坐标 */ public void drawPoint(float x, float y,Paint paint); //示例 private void drawPoint(Canvas canvas) { final float OFFSET = 400; final float DRAW_POINT_HEIGHT = 100; float pointOffset = 20; float[] points = { OFFSET, DRAW_POINT_HEIGHT , OFFSET + pointOffset,DRAW_POINT_HEIGHT, OFFSET + 2*pointOffset,DRAW_POINT_HEIGHT, OFFSET + 3*pointOffset,DRAW_POINT_HEIGHT, OFFSET + 4*pointOffset,DRAW_POINT_HEIGHT }; // 绘制多个点 canvas.drawPoints(points,mPaint); // 绘制一个点 canvas.drawPoint(OFFSET + 5*pointOffset,DRAW_POINT_HEIGHT,mPaint); } 复制代码
- 绘制线:
//api /** * 两点绘制线 * @param startX 第一个点的x坐标 * @param startY 第一个点的y坐标 * @param stopX 第二个点的x坐标 * @param stopY 第二个点的y坐标 * @param paint 画笔 */ public void drawLine(float startX, float startY, float stopX, float stopY,Paint paint) //示例 private void drawLine(Canvas canvas) { final float OFFSET = 400; static final float DRAW_LINE_HEIGHT = 180; float lineWidth = 100; canvas.drawLine(OFFSET,DRAW_LINE_HEIGHT,OFFSET + lineWidth,DRAW_LINE_HEIGHT,mPaint); } 复制代码
- 绘制矩形:
//api /** * 绘制矩形 * @param rect 矩形的位置 * @param paint 画笔 */ public void drawRect(RectF rect,Paint paint); //示例 private void drawRect(Canvas canvas){ final float DRAW_RECT_HEIGHT = 430; float rectWidth = 200; float rectHeight = 160; RectF rect = new RectF(); rect.bottom = DRAW_RECT_HEIGHT; rect.top = rect.bottom - rectHeight; rect.left = OFFSET; rect.right = rect.left + rectWidth; canvas.drawRect(rect,mPaint); } 复制代码
- 绘制圆角矩形:
//api /** * 绘制圆角矩形 * @param rect 矩形的位置 * @param rx 用于形成圆角的椭圆的X轴半径 * @param ry 用于形成圆角的椭圆的Y轴半径 * @param paint 画笔 */ public void drawRoundRect(RectF rect, float rx, float ry,Paint paint); //示例 private void drawRoundRect(Canvas canvas){ final float DRAW_ROUND_RECT_HEIGHT = 680; float rectWidth = 200; float rectHeight = 160; float rx = 8; float ry = 10; RectF rect = new RectF(); rect.bottom = DRAW_ROUND_RECT_HEIGHT; rect.top = rect.bottom - rectHeight; rect.left = OFFSET; rect.right = rect.left + rectWidth; canvas.drawRoundRect(rect,rx,ry,mPaint); } 复制代码
- 绘制椭圆:
//api /** * 绘制椭圆 * @param oval 椭圆的外切矩形的位置 * @param paint 画笔 */ public void drawOval(RectF oval,Paint paint); //示例 private void drawOval(Canvas canvas){ final float DRAW_OVAL_HEIGHT = 930; float rectWidth = 200; float rectHeight = 160; RectF rect = new RectF(); rect.bottom = DRAW_OVAL_HEIGHT; rect.top = rect.bottom - rectHeight; rect.left = OFFSET; rect.right = rect.left + rectWidth; canvas.drawOval(rect,mPaint); } 复制代码
- 绘制圆:
//api /** * 绘制圆 * @param cx 圆心x坐标 * @param cy 圆心y坐标 * @param paint 画笔 */ public void drawCircle(float cx, float cy, float radius,Paint paint); //示例 private void drawCircle(Canvas canvas) { final float DRAW_CIRCLE_HEIGHT = 1180; float rectWidth = 200; float rectHeight = 200; RectF rect = new RectF(); rect.bottom = DRAW_CIRCLE_HEIGHT; rect.top = rect.bottom - rectHeight; rect.left = OFFSET; rect.right = rect.left + rectWidth; canvas.drawCircle((rect.left + rect.right) /2,(rect.top + rect.bottom) /2,(rect.bottom - rect.top) /2,mPaint); } 复制代码
- 绘制圆弧:
//api /** * 绘制圆弧 * @param oval 圆弧所在椭圆的外切矩形的位置 * @param startAngle 圆弧开始的角度 * @param sweepAngle 圆弧扫描过的角度 * @param useCenter 如果为ture,则圆弧的起点和终点会与圆心形成封闭的图形。 * @param Paint 画笔 */ public void drawArc(RectF oval,float startAngle,float sweepAngle,boolean useCenter,Paint paint); //示例 private void drawAngle(Canvas canvas) { final float DRAW_ANGLE_HEIGHT = 1430; float rectWidth = 200; float rectHeight = 180; RectF rect = new RectF(); rect.bottom = DRAW_ANGLE_HEIGHT; rect.top = rect.bottom - rectHeight; rect.left = OFFSET; rect.right = rect.left + rectWidth; mPaint.setStyle(Paint.Style.FILL); mPaint.setStrokeWidth(2); //useCenter:true canvas.drawArc(rect,-180,120f,true,mPaint); RectF rect1 = new RectF(); rect1.bottom = DRAW_ANGLE_HEIGHT; rect1.top = rect1.bottom - rectHeight; rect1.left = rect.right + 50 ; rect1.right = rect1.left + rectWidth; mPaint.setStrokeCap(mCap); mPaint.setAntiAlias(true); //设置圆形盖帽 mPaint.setStyle(Paint.Style.STROKE); //useCenter:true canvas.drawArc(rect1,-90,80f,false,mPaint); } 复制代码
基本图形效果如下图:
-
3.绘制文字
- Paint对于Text的设置
//普通设置 paint.setStrokeWidth(5):设置画笔宽度 paint.setAntiAlias(true):设置是否使用抗锯齿功能,如果使用,会导致绘图速度变慢 paint.setStyle(Paint.Style.FILL):设置绘图样式,对于设置文字和几何图形都有效,可取值有三种 :1、Paint.Style.FILL:填充内部 2、Paint.Style.FILL_AND_STROKE:填充内部和描边 3、Paint.Style.STROKE:仅描边 paint.setTextAlign(Align.CENTER):设置文字对齐方式 paint.setTextSize(12):设置文字大小 //样式设置 paint.setFakeBoldText(true):设置是否为粗体文字 paint.setUnderlineText(true):设置下划线 paint.setTextSkewX((float) -0.25):设置字体水平倾斜度,普通斜体字是 -0.25 paint.setStrikeThruText(true):设置带有删除线效果 复制代码
-
canvas绘制文字api
- 普通水平绘制
//绘制文字基本的api drawText (String text, float x, float y, Paint paint) //由于传入的参数是CharSequence类型,所以可以绘制带图片的扩展文字 drawText (CharSequence text, int start, int end, float x, float y, Paint paint) //截取绘制文字 drawText (String text, int start, int end, float x, float y, Paint paint) drawText (char[] text, int index, int count, float x, float y, Paint paint) 复制代码
在上面的api中,传入的x,y参数是绘制文字时基准点的坐标,默认请况下基准点在文字的左下角,基准点对应的X轴线叫基准线。此外,除了基准线外,还有另外4条线,分别是:
1:ascent线:绘制单个字符时,字符的最高高度所在线;
2: descent线:绘制单个字符时,字符的最低高度所在线,与ascent线相对于中心点对称;
3:top线: 可绘制的最高高度所在线;
4:bottom线,可绘制的最低高度所在线;这5条线对应的位置如下图所示:
获取ascent线,descent线,top线,bottom线相对于基准线的距离:
FontMetrics fontMetrics = mPaint.getFontMetrics(); //由于android 坐标系向下为正方向,top线和ascentxian在基准线的上方,所以他们距离基准线的距离为负数 float topToBaseLineDistance = fontMetrics.top; float ascentToBaseLineDistance = fontMetrics.ascent; //同理,bottom线和descent线距离基准线的距离为正数 float bottomToBaseLineDistance = fontMetrics.bottom; float descentToBaseLineDistance = fotMetrics.descent; 复制代码
通过上述关系可以通过中心点确定基准点的坐标:
/** * 获取基准线与中心点Y轴方向的距离 * @param paint * @return 基准线与中心点Y轴方向的距离 */ public static float getBaseLine(Paint paint){ Paint.FontMetrics fontMetrics = paint.getFontMetrics(); return (fontMetrics.descent - fontMetrics.ascent)/2 - fontMetrics.descent; } /** * 通过中心点获取基准点的坐标 * @param centerPointX 中心点的X轴坐标 * @param centerPointY 中心点的Y坐标 * @param mPaint 画笔 * @param txt 需要绘制的文本 * @return 基准点的坐标 */ public static PoinF getBasePointCoordinate(Paint mPaint,float centerPointX,float centerPointY,String txt){ PointF p = new PointF(); p.x = centerPointX - mPaint.measureText(txt)/2; p.y = centerPointY + getBaseLine(mPaint); return p; } 复制代码
绘制图片的基准点默认是在文字的左下角,我们也可以设置基准点在文字的中间和右下角:
//设置基准点在文字的中间 mPaint.setTextAlign(Paint.Align.CENTER); 复制代码
效果图如下:
//设置基准点在文字右边 mPaint.setTextAlign(Paint.Align.RIGHT); 复制代码
效果图如下:
- 沿Path路径绘制文字
//api /** * @param txt 需要绘制的文字 * @param path 绘制文字的路径 * @param hOffset 水平方向的偏移量 * @param vOffset 垂直方向的偏移量 * @param paint 画笔 */ public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint) /** * 沿指定路径绘制部分文本 * @param text 绘制的文本的char数组 * @param index 第一个绘制文字在char数组中的索引 * @parma count 绘制文字的数量 * @param path 路径 * @param hOffset 水平方向的偏移量 * @param vOffset 垂直方向的偏移量 * @param paint 画笔 */ public void drawTextOnPath (char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint 画笔) 复制代码
示例代码:
private void drawTxtFormPath(Canvas canvas){ mPaint.setTextSize(60f); mPaint.setStyle(Paint.Style.STROKE); String txt = "天下霸道之剑"; float circleRadius = 250f; Path path = new Path(); Path tempPath = new Path(); PathMeasure pathMeasure = new PathMeasure(); path.addCircle(centerX,centerY,circleRadius, Path.Direction.CCW); pathMeasure.setPath(path,false); pathMeasure.getSegment(0.125f * pathMeasure.getLength(),pathMeasure.getLength() * 0.375f,tempPath,true); mPaint.setColor(Color.RED); canvas.save(); canvas.rotate(180,centerX,centerY); canvas.drawPath(tempPath,mPaint); canvas.drawTextOnPath(txt,tempPath,0f,50f,mPaint); canvas.restore(); } 复制代码
效果如下图:
-
画布操作
在某些场景下使用画布操作可以更简单,更直接的实现我们想要的效果。例如 我们需要画一条与X轴的夹角为30度的直线,正常情况下我们需要利用三角函数去算两个点的坐标,但是使用画布操作,我们可以先画一条水平的直线,然后在将画布旋转30度,这样就可以更加简单直接的实现效果。
注意:所有的画布操作都只会影响后续的绘制,不会影响之前的绘制。- 画布平移(translate)
以当前位置移动坐标系,不是每次基于屏幕的左上角(0,0)移动。
api:pualic void translate(); 复制代码
/** * 画布位移操作,每次位移都是相对以当前canvas位置,而不是初始的坐标原点位置。 * @param canvas 画布 */ private void translateOperation(Canvas canvas) { final int TRANSLATE_OPERATION_HEIGHT = 280; final float OFFSET = 250; canvas.save(); RectF rectF = new RectF(); float width =200; float height = 200; rectF.bottom = TRANSLATE_OPERATION_HEIGHT; rectF.left = OFFSET; rectF.top = rectF.bottom - height; rectF.right = rectF.left + width; canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); canvas.translate(200,0); mPaint.setColor(Color.BLACK); canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); canvas.restore(); } 复制代码
效果图如下:
- 画布缩放(scale)
/** * 会以坐标原点为默认缩放中心进行缩放 * @param sx X轴缩放的比例 * @param sy Y轴缩放比例 */ public void scale(float sx,float sy); /** * 指定缩放中心进行缩放 * @param sx X轴缩放的比例 * @param sy Y轴缩放比例 * @param 缩放中心的x坐标 * @param 缩放中心的y坐标 */ public void scale(float sx, float sy, float px, float py); 复制代码
sx,sy的取值范围解释:
取值范围 说明 (-∞, 0) 先缩放,再根据中心轴进行翻转 (0, +∞) 只进行缩放 //缩放系数为正数 private void scaleOperation(Canvas canvas) { final int SCALE_OPERATION_HEIGHT = 680; final float OFFSET = 250; canvas.save(); RectF rectF = new RectF(); float width =200; float height = 200; rectF.bottom = SCALE_OPERATION_HEIGHT; rectF.left = OFFSET; rectF.top = rectF.bottom - height; rectF.right = rectF.left + width; canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); mPaint.setColor(Color.BLACK); //设置缩放比例 和缩放中心点,若果不设置缩放中心点,当前的坐标原点就是缩放中心点。 canvas.scale(0.5f,0.5f,(rectF.left + rectF.right) /2 ,(rectF.bottom + rectF.top)/2); canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); canvas.restore(); } 复制代码
效果图:
//缩放系数为负数 private void scaleOperation(Canvas canvas) { final int SCALE_OPERATION_HEIGHT = 680; final float OFFSET = 250; canvas.save(); RectF rectF = new RectF(); float width =200; float height = 200; rectF.bottom = SCALE_OPERATION_HEIGHT; rectF.left = OFFSET; rectF.top = rectF.bottom - height; rectF.right = rectF.left + width; canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); mPaint.setColor(Color.BLACK); //缩放参数为负数含义是: 在缩放之后还要按照中心轴翻转,不指定缩放中心点,则默认的中心轴为X轴,Y轴 canvas.scale(-0.5f,-0.5f,rectF.right,(rectF.bottom + rectF.top)/2); canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); canvas.restore(); } 复制代码
效果图如下:
- 画布旋转(rotate)
//api /** * 以坐标原点为中心点旋转 * @param degrees 旋转的角度 */ public void rotate(float degrees); /** * 指定旋转中心进行旋转 * @param degrees 旋转的角度 * @param px 旋转中心的x坐标 * @param py 旋转中心的y坐标 */ public void rotate(float degrees, float px, float py); 复制代码
示例:
private void rotateOperation(Canvas canvas) { final int ROTATE_OPERATION_HEIGHT = 1080; final float OFFSET = 250; canvas.save(); RectF rectF = new RectF(); float width =200; float height = 200; rectF.bottom = ROTATE_OPERATION_HEIGHT; rectF.left = OFFSET; rectF.top = rectF.bottom - height; rectF.right = rectF.left + width; mPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2,mPaint); canvas.drawCircle((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2,(rectF.bottom - rectF.top) /2 - 15,mPaint); String testStr = "1"; for(int i = 0 ; i<= 360 ; i += 10){ canvas.drawLine((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2 -85,(rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2 -100,mPaint); mPaint.setTextSize(15); float testStrBaseLineX = (rectF.left + rectF.right) /2 - mPaint.measureText(testStr); float testStrBaseLineY = (rectF.bottom + rectF.top)/2 - 70 + Tools.getBaseLine(mPaint); canvas.drawText(testStr,testStrBaseLineX,testStrBaseLineY,mPaint); canvas.rotate(10,(rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2); } canvas.restore(); } 复制代码
效果图:
- 画布错切
//api: /** * 错切 * @param sx 将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值 * @param sy 将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值 */ public void skew (float sx, float sy) 复制代码
示例:
private void skewOperation(Canvas canvas) { final int SKEW_OPERATION_HEIGHT = 1480; final float OFFSET = 250; canvas.save(); RectF rectF = new RectF(); float width =200; float height = 200; rectF.bottom = SKEW_OPERATION_HEIGHT; rectF.left = OFFSET; rectF.top = rectF.bottom - height; rectF.right = rectF.left + width; canvas.translate((rectF.left + rectF.right) /2,(rectF.bottom + rectF.top)/2); RectF rect1 = new RectF(-100,-100,100,100); // 矩形区域 canvas.drawRect(rect1,mPaint); canvas.skew(1,0); mPaint.setColor(Color.BLACK); RectF rect = new RectF(-100,-100,100,100); // 矩形区域 canvas.drawRect(rect,mPaint); } 复制代码
效果为:
- 画布平移(translate)
-
绘制图片
//api /** * @param bitmap 绘制图片的位图 * @param matrix 矩阵 通过矩阵可以对图片进行旋转,唯一,缩放等操作 * @param paint 画笔 */ public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint); /** * @param bitmap 绘制图片的位图 * @param left 图片左上角的x坐标 * @param top 图片左上角的y坐标 * @param paint 画笔 */ public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint); /** * @param bitmap 绘制图片的位图 * @param src 以图片坐上角为参考点,指定图片的要绘制的区域 * @param dst 以正常坐标系的原点为参考点,指点图片在屏幕上的显示区域 * @param paint 画笔 */ public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint); 复制代码
-
Path操作
通过canvas绘制的图形都是简单的规则图形,如果要绘制复杂的图形则需要用到Path;- Path的起点
path绘制直线和曲线时都需要设置其起点位置。在不设置起点的情况下,path的起点在坐标系原点位置或上一次绘制路径的终点。
//api /** * 设置path的下一次操作的起点位置(相对于坐标系的位置) * @param x path起点的x坐标 * @param y path起点的y坐标 */ public void moveTo(float x,float y); /** * 设置path的下一次操作的起点位置(相对于path当前起点在X轴和Y轴的偏移量) * @param dx 设置的起点相对于path当前起点位置在X轴方向的偏移量 * @param dy 设置的起点相对于path当前起点位置在Y轴方向的偏移量 */ public void rMoveTo(float dx, float dy); 复制代码
示例:
Path mPath = new Path(); mPath.moveTo(150,100); 复制代码
设置path的起点位置在(150,100)处。
Path mPath = new Path(); mPath.moveTo(150,100); mPath.rMoveTO(-50,50); 复制代码
mPath的起点位置在(100,150)处。
Path mPath = new Path(); 复制代码
不设置起点的情况下,此时path的起点在坐标原点(0,0)位置。
Path mPath = new Path(); mPath.lineTo(100,100); 复制代码
path此时的起点在上次绘制路径的终点位置,即(100,100)处。
- Path绘制直线
//api /** * path绘制直线(直线终点在坐标系内的位置) * @param x 绘制直线的终点的x坐标 * @param y 绘制直线的终点的y坐标 */ public void lineTo(float x, float y); /** * path绘制直线(直线终点的位置相对于path当前的起点的偏移量) * @param dx 直线终点位置相对于path起点X轴方向的偏移量 * @param dy 直线终点位置相对于path起点Y轴方向的偏移量 */ public void rLineTo(float dx, float dy); 复制代码
示例:
private void pathDrawLine(Canvas canvas){ Path mPath = new Path; mPath.moveTo(100,100); mPath.lineTo(500,100); canvas.drawPath(mPath,mRedPaint); mPath.reset(); mPath.moveTo(100,100); mPath.rLineTo(500,100); canvas.drawPath(mPath,mRedPaint); } 复制代码
效果图如下:
-
Path绘制曲线
-
贝塞尔曲线
贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。贝塞尔曲线通过控制数据点(曲线的起点和终点)和控制点来创造编辑图形。其中数据点是来确定曲线的起始和结束的位置,控制点用来控制曲线的弯曲程度。根据控制点的不同,贝塞尔曲线可以分为二阶曲线,三阶曲线,更高阶的曲线。Path只提供绘制二阶和三阶曲线的api,对于高阶的曲线,可以用低阶的曲线组合达到同样的效果。 -
二阶贝塞尔曲线
二阶贝塞尔曲线由两个数据点(起点和终点),一个控制点来描述曲线状态。参考GcsSloop博客
上图中A,C点为控制点,B点为控制点,我们选取其中一个状态说明贝塞尔曲线的产生。参考GcsSloop博客
连接AB,BC,在AB,BC上分别取点D,E,使其满足 ,连接DE,在DE上取点F,使得 ,这样子获取到的点F就是曲线上的的一个点F。动态过程如下:参考GcsSloop博客
Path 绘制二阶贝塞尔曲线api:
/** * 绘制二阶贝塞尔曲线,曲线的起始点为path当前的起点 * @param x1 控制点的x坐标 * @param y1 控制点的y坐标 * @param x2 曲线终点的x坐标 * @param y2 曲线终点的y坐标 */ public void quadTo(float x1, float y1, float x2, float y2); /** * 绘制二阶贝塞尔曲线,控制点和终点相对于path当前起点的偏移量 * @param dx1 控制点相对于path起点在X方向的偏移量 * @param dy1 控制点相对于path起点在Y方向的偏移量 * @param dx2 曲线终点相对于path递签在X方向的偏移量 * @param dy2 曲线终点相对于path递签在Y方向的偏移量 */ public void rQuadTo(float dx1, float dy1, float dx2, float dy2); 复制代码
示例:
private void drawCurve(Canvas canvas){ mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(8); startPoint = new PointF(-200f,0f); endPoint = new PointF(200f,0f); controlPoint = new PointF(0,200); float mWidth = getMeasuredWidth(); float mHeight = getMeasuredHeight(); canvas.translate(mWidth / 2, mHeight / 2); mPath.moveTo(startPoint.x,startPoint.y); mPath.quadTo(controlPoint.x,controlPoint.y,endPoint.x,endPoint.y); canvas.drawPath(mPath,mPaint); } 复制代码
效果如下图:
- 三阶贝塞尔曲线
三阶贝塞尔曲线由两个数据点(曲线的起点和终点),两个控制点来描述曲线状态,如下图:参考GcsSloop博客
A,D为数据的起始点,B,C为数据的两个控制点。
三阶曲线的计算过程与二阶类似,如下图:参考GcsSloop博客
三阶曲线api:
示例:利用三阶贝塞尔曲线绘制圆形。(绘制圆形的数据点与控制点的计算可以参考stackoverflow的一个回答How to create circle with Bézier curves?)/** * 绘制三阶贝塞尔曲线,曲线的起点是path当前的起点 * @param x1 控制点1的x坐标 * @param y1 控制点1的y坐标 * @param x2 控制点2的x坐标 * @param y2 控制点2的y坐标 * @param x3 曲线终点的x坐标 * @param y3 曲线终点的y坐标 */ public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3); /** * 绘制三阶贝塞尔曲线,参数是数据点和控制点相对于path起点的偏移量 * @param x1 控制点1位置相对于path起点在X轴方向的偏移量 * @param y1 控制点1位置相对于path起点在Y轴方向的偏移量 * @param x2 控制点2位置相对于path起点在X轴方向的偏移量 * @param y2 控制点2位置相对于path起点在Y轴方向的偏移量 * @param x3 曲线终点位置相对于path起点在X轴方向的偏移量 * @param y3 曲线终点位置相对于path起点在Y轴方向的偏移量 */ public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3); 复制代码
效果图如下:private void drawCurve(Canvas canvas){ //一个常量,用来计算圆形贝塞尔曲线控制点的位置 final float C = 0.551915024494f; float radius = 300; float mDiffrence = radius * C; mWidth = getMeasuredWidth();; mHeight = getMeasuredHeight(); mCenterX = mWidth/2; mCenterY = mHeight/2; for(int i = 0 ; i < pointDraws.length ; i++){ pointDraws[i] = new PointF(); } pointDraws[0].x = -radius; pointDraws[0].y = 0; pointDraws[1].x = 0; pointDraws[1].y = -radius; pointDraws[2].x = radius; pointDraws[2].y = 0; pointDraws[3].x = 0; pointDraws[3].y = radius; for(int i = 0 ; i < pointControls.length ; i++){ pointControls[i] = new PointF(); } pointControls[0].x = pointDraws[0].x; pointControls[0].y = pointDraws[0].y - mDiffrence; pointControls[1].x = pointDraws[1].x - mDiffrence; pointControls[1].y = pointDraws[1].y; pointControls[2].x = pointDraws[1].x + mDiffrence; pointControls[2].y = pointDraws[1].y; pointControls[3].x = pointDraws[2].x; pointControls[3].y = pointDraws[2].y - mDiffrence; pointControls[4].x = pointDraws[2].x; pointControls[4].y = pointDraws[2].y + mDiffrence; pointControls[5].x = pointDraws[3].x + mDiffrence; pointControls[5].y = pointDraws[3].y; pointControls[6].x = pointDraws[3].x - mDiffrence; pointControls[6].y = pointDraws[3].y; pointControls[7].x = pointDraws[0].x; pointControls[7].y = pointDraws[0].y + mDiffrence; canvas.translate(mCenterX,mCenterY); mPath.moveTo(pointDraws[0].x,pointDraws[0].y); mPath.cubicTo(pointControls[0].x,pointControls[0].y,pointControls[1].x,pointControls[1].y,pointDraws[1].x,pointDraws[1].y ); mPath.cubicTo(pointControls[2].x,pointControls[2].y,pointControls[3].x,pointControls[3].y,pointDraws[2].x,pointDraws[2].y); mPath.cubicTo(pointControls[4].x,pointControls[4].y,pointControls[5].x,pointControls[5].y,pointDraws[3].x,pointDraws[3].y ); mPath.cubicTo(pointControls[6].x,pointControls[6].y,pointControls[7].x,pointControls[7].y,pointDraws[0].x,pointDraws[0].y); canvas.drawPath(mPath,mPaint); } 复制代码
蓝色的点为数据点,灰色的点为控制点。
下图是利用二阶和三阶的贝塞尔曲线制作的效果:(gif效果有点卡顿)
点击查看源码; -
-
Path绘制规则图形:
//api /** * 添加矩形路径 * @param rect 添加到路劲中的矩形 * @param dir 添加矩形路径的方向:CW(顺时针),CCW(逆时针) */ public void addRect(RectF rect, Direction dir); /** * 添加圆角矩形路径 * @param rect 添加到路劲中的矩形 * @param rx 圆角对应的椭圆在X轴方向的半径 * @param ry 圆角对应的椭圆在Y轴方向的半径 * @param dir 添加矩形路径的方向:CW(顺时针),CCW(逆时针) */ public void addRoundRect(RectF rect, float rx, float ry, Direction dir); /** * 添加圆形路径 * @param x 圆心的x坐标 * @param y 圆心的y坐标 * @param radius 圆的半径 * @param dir 添加圆形路径的方向:CW(顺时针),CCW(逆时针) */ public void addCircle(float x, float y, float radius, Direction dir); /** * 添加椭圆路径 * @param oval 椭圆外切矩形 * @param dir 添加椭圆路径的方向:CW(顺时针),CCW(逆时针) */ public void addOval(RectF oval, Direction dir); /** * 添加圆弧路径 * @param oval 圆弧对应的椭圆的外切矩形 * @param startAngle 圆弧开始的角度 * @param sweepAngle 圆弧扫描过的角度 */ public void addArc(RectF oval, float startAngle, float sweepAngle); /** * 添加圆弧路径,与addArc不同,这个方法会将圆弧的终点和起点连接起来 * @param oval 圆弧对应的椭圆的外切矩形 * @param startAngle 圆弧开始的角度 * @param sweepAngle 圆弧扫描过的角度 */ public void arcTo(RectF oval, float startAngle, float sweepAngle); 复制代码
上面有些api中会有Direction类型的参数,这个参数是确定规则图形添加到路径中的顺序,它对path的填充结果会有影响。
Path绘制规则图形示例:private static final int DEFAULT_RECT_WIDTH = 300; protected void onDraw(Canvas canvas) { float mWidth = getMeasuredWidth(); float mHeight = getMeasuredHeight(); canvas.translate(mWidth/2,mHeight/2); addRect(); addRoundRect(); addCircle(); addOval(); canvas.drawPath(mPath,mPaint); } private void addRect() { RectF rect = new RectF(0,0,DEFAULT_RECT_WIDTH,DEFAULT_RECT_WIDTH); //顺时针画矩形 mPath.addRect(rect, CW); } private void addRoundRect() { RectF rectF = new RectF(0,0,-DEFAULT_RECT_WIDTH,DEFAULT_RECT_WIDTH); mPath.addRoundRect(rectF,15,15, CW); } private void addCircle() { RectF rectF = new RectF(0,0,-DEFAULT_RECT_WIDTH,-DEFAULT_RECT_WIDTH); mPath.addCircle((rectF.left + rectF.right) / 2,(rectF.top + rectF.bottom) / 2,(rectF.left - rectF.right)/2,CW); } private void addOval() { RectF rectF = new RectF(0,0,DEFAULT_RECT_WIDTH,-DEFAULT_RECT_WIDTH/2); mPath.addOval(rectF,CW); } 复制代码
效果如下图:
- Path形成封闭的图形
//api public void close(); 复制代码
close方法会连接路径的起始点和当前的终点,形成一个封闭的图形。
示例private void drawLineByPath(Canvas canvas) { final int FLAG_DESTANCE = 300; float mWidth = getMeasuredWidth(); float mHeight = getMeasuredHeight(); canvas.translate(mWidth/2,mHeight/2); canvas.save(); canvas.scale(1,-1,0,0); noClosePath.moveTo(0,0); noClosePath.lineTo(FLAG_DESTANCE,FLAG_DESTANCE); noClosePath.lineTo(FLAG_DESTANCE,0); canvas.drawPath(noClosePath,mBluePaint); path.moveTo(0,0); path.lineTo(-FLAG_DESTANCE,FLAG_DESTANCE); path.lineTo(-FLAG_DESTANCE,0); path.close(); canvas.drawPath(path,mPaint); canvas.restore(); } 复制代码
效果图如下:
-
Path的填充模式
当我们需要给封闭图形填充内部颜色时,首先要分清楚那一部分是图形的内部,哪一部分是图形的外部。Path提供了如下两种方法判断:①奇偶规则;②非零环绕数规则。-
奇偶规则(Even_Odd)
从图形任意点p作一条射线,如果封闭的图形与射线相交的边的数目为奇数,则p点在图形内部,如果为偶数,则p点在图形外部。参考GcsSloop的博客
上图中从P1发出一条射线,与图形的相交边数为0,偶数,P1点在图形外部;
从P2发出一条射线,与图形相交的变数为1,奇数,P2在图形内部;
从P3发出一条射线,与图形相交的变数为2,偶数,P3在图形外部。 -
非零环绕数规则(None-Zero Winding Number)
当给path添加规则图形时,会指定图形的添加顺序,另外当用path绘制直线或曲线时,是从一个点到另外一个点,是有方向的。因此,path中的线段都是有方向性的,这是非零环绕数规则的基础。
具体规则:先将图形线段按绘制方向矢量化。从任意一点P做射线,以图形与射线的相交边计数,每当图形的边从右到左穿过射线时环绕数+1,从左到右穿过时,环绕数-1。若环绕数非零,则P点在内部,为零则P点在外部。参考GcsSloop的博客
从P1点发出一条射线,没有边与其相交,环绕数为0,P1在外部;
从P2点发出一条射线,与图形左侧边相交,改变从右到左穿过射线,环绕数为1,P2在图形内部;
从P3点发出一条射线,与图形右边和底边相交,底边从右到左穿过射线,环绕数+1,右边从左到右穿过射线,环绕数-1,故最终的环绕数为零,P3在图形外部。
Path形成的封闭图形设置填充模式
/** * 设置path的填充模式 * @param ft 填充模式 */ public void setFillType(FillType ft); 复制代码
FillType是一个枚举,有4中取值:
模式 说明 EVEN_ODD 奇偶规则 INVERSE_EVEN_ODD 反奇偶规则,如果用奇偶规则判断某点在封闭图形内部,
则用反奇偶规则就会判断这点在次图形外部WINDING 非零环绕数规则 INVERSE_WINDING 反非零环绕数规则,如果用非零环绕数规则判断某点在封闭图形内部,则用反非零环绕数规则就会判断这点在次图形外部 -
-
Path布尔操作 布尔操作是两个Path之间的操作,主要作用是通过一些简单的图形通过特定的规则合成一些复杂的图形。
//api: /** * path1与path2进行布尔运算,并将合成路径保存到调用这个api的Path中 * @param path1 参与布尔运算的第一个path * @param path2 参与布尔运算的第二个path * @param op 布尔运算规则 * @return 是否操作成功 */ public boolean op(Path path1, Path path2, Op op); 复制代码
Op有五种取值:
①:DIFFERENCE:差集。path1减去与path2相交之后的剩余部分;
②:INTERSECT:交集。path1与path2相交的部分;
③:UNION:并集。包含path1与path2的全部部分;
④:REVERSE_DIFFERENCE:反差集。path2减去与path1相交之后的剩余部分;
⑤:XOR:异或。包含path1和path2但不包含两者交集的部分。
5中布尔操作的效果如下图:-
Path其它api介绍
计算path边界/** * 计算path的边界位置 * @param bounds 保存path边界位置 * @param exact 是否精确计算 */ public void computeBounds(RectF bounds, boolean exact); 复制代码
重置路径
/** * 清空path所有的直线和曲线,保留path的填充规则 */ public void reset(); /** * 与reset()方法效果一样,但会不会保留path的填充规则 */ public void rewind(); 复制代码
判断Path是否为空
public boolean isEmpty(); 复制代码
判断Path的路径是否为矩形
public boolean isRect(RectF rect); 复制代码
Path平移
/** * path进行平移,将平移后的路径保存到dst中 * @param dx X方向的偏移量 * @param dy Y方向的偏移量 * @dst 如果dst不为空,将平移后的路径保存到dst中 */ public void offset(float dx, float dy, Path dst); /** * 将path平移 * @param dx X方向的偏移量 * @param dy Y方向的偏移量 */ public void offset(float dx, float dy); 复制代码
通过一个Path给另一个Path赋值
/** * 用src的内容替换调用这个api的path的内容。 */ public void set(Path src) 复制代码
-
PathMeasure
PathMeasure是测量Path的类,利用PathMeasure我们可以的Path的长度,截取Path的任意片段,也可以获取Path路径上任意点的切线与X轴的夹角。
PathMeasure的构造函数/** * 不指定关联的Path的构造函数,需要后期调用set()方法指定关联的Path */ public PathMeasure(); /** * 指点关联Path的构造函数 * @param path 关联的path * @param forceClosed true 尝试闭合path。(注意:forceClosed的取值不会影响Path的状态, 即使path为非闭合路径) */ public PathMeasure(Path path, boolean forceClosed); 复制代码
关联Path:
/** * param path 关联的path * param forceClosed 如果为ture,则尝试闭合path。 */ public void setPath(Path path, boolean forceClosed); 复制代码
判断Path是否为闭合路径
public boolean isClosed(); 复制代码
获取Path的长度
public float getLength(); 复制代码
截取Path中的片段
/** * 截图Path的某一片段 * @param startD 截取片段的起始位置距离Path起点的长度 * startD取值范围为:0 ~ Path的总长度 * @param stopD 截取片段的终点位置距离Path起点的长度 stopD取值范围为:0 ~ Path的总长度 * @param dst 将截图的路径添加到dst中 * @param startWithMoveTo 为true,将dst的起点位置设置为截取片段的起点位置,这样可以保证截取的片段不变形 为false,将截取片段的起始位置设置为dst当前路径的最后一个,这样子可以保证dst路径的连续性 */ public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo); 复制代码
示例:利用PathMeasure制作一个进度条,效果如下图
//当前的进度值(0~1.0f) float currentProgress; protected void onDraw(Canvas canvas) { int DEFAULT_WIDTH = 600; int DEFAULT_HEIGHT = 800; int DEFAULT_RADIUS = 50; float mWidth = getMeasuredWidth(); float mHeight =getMeasuredHeight(); PathMeasure mPathMeasure = new PathMeasure(); Path path1 = new Path(); Path path2 = new Path(); Path path3 = new Path(); //将坐标原点平移到屏幕中央 canvas.translate(mWidth/2,mHeight/2); RectF rectF = new RectF(); rectF.left = -DEFAULT_WIDTH/2; rectF.top = -DEFAULT_HEIGHT/2; rectF.right = rectF.left + DEFAULT_WIDTH; rectF.bottom = rectF.top + DEFAULT_HEIGHT; //path1中添加圆角矩形 mPath1.addRoundRect(rectF, DEFAULT_RADIUS,DEFAULT_RADIUS,Path.Direction.CCW); //mPathMeasure关联path1 mPathMeasure.setPath(mPath1,false); //获取path1的长度 float path1Length = mPathMeasure.getLength(); float currentDrawLength = path1Length * currentProgress; //根据当前的进度值从path1中获取截取片段并将片段添加到path2 mPathMeasure.getSegment(0,currentDrawLength,mPath2,true); //获取path1中的圆角片段 mPathMeasure.getSegment(path1Length *0.990f,path1Length,mPath3,true); canvas.drawPath(mPath2,mYellowPaint); canvas.drawPath(mPath3,mYellowPaint); } 复制代码
切换轮廓
/** * @return true 则切换成功。 */ public boolean nextContour(); 复制代码
path可能由多段不相连的封闭或非封闭线段组成,PathMeasure的getLength (),getSegment()等方法的作用对象都是一段连续的线段,如果要测量path中其它的片段,则调用nextContour()方法即可。
注意:如果要测量path中下一段线段长度,必须先测量上一段线段长度,再调用则调用nextContour()方法。protected void onDraw(Canvas canvas) { path.reset(); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); canvas.translate(mWidth/2,mHeight/2); path.lineTo(300,0); path.lineTo(300,300); path.moveTo(0,50); path.lineTo(100,50); path.lineTo(100,150); canvas.drawPath(path,paint); pathMeasure.setPath(path,false); //先测量前一段线段的长度 float outLength = pathMeasure.getLength(); //再切换到下一段线段 boolean nextContourFlag = pathMeasure.nextContour(); //最后测量下一段线段的长度 float innerLength = pathMeasure.getLength(); Log.i(TAG,"path out contour length is:" + outLength); Log.i(TAG,"path inner contour length is:" + innerLength); } 复制代码
效果图如下:
同一Path路径中两段线段的长度分别为:PathNextContourView: path out contour length is:600.0 PathNextContourView: path inner contour length is:200.0 复制代码
后去path路径上某一位置的坐标和切线方向
//api /** * @param distance 距离path起点的长度,取回范围为0~path的总长度 * @param pos 该点的坐标值 pos[0]为X坐标,pos[1]为y坐标 * @param tan 该点的正切值,通过Math.atan2(tan[1], tan[0])*180/Math.PI可以获取到改点切线与X轴的夹角。 */ public boolean getPosTan(float distance, float pos[], float tan[]); 复制代码
示例:
protected void onDraw(Canvas canvas) { path.reset(); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); //平移坐标系到屏幕中央 canvas.translate(mWidth/2,mHeight/2); path.addCircle(0,0,300, Path.Direction.CW); paint.setColor(Color.BLUE); //绘制圆 canvas.drawPath(path,paint); pathMeasure.setPath(path,false); float cricleLength = pathMeasure.getLength(); float[] position = new float[2]; float[] tans = new float[2]; pathMeasure.getPosTan(cricleLength * progressValue,position,tans); //获取圆上某点的切线与X轴的夹角 float degree = (float) (Math.atan2(tans[1],tans[0]) * 180 / Math.PI); //绘制圆上某点的切线 canvas.translate(position[0],position[1]); canvas.rotate(degree); paint.setColor(Color.RED); canvas.drawLine(-300,0,300,0,paint); startAnimator(); } private ValueAnimator valueAnimator; private boolean isStartAnimator = false; private float progressValue = 0f; //开始动画 private void startAnimator(){ if(!isStartAnimator){ isStartAnimator = true; if(valueAnimator == null){ valueAnimator = ValueAnimator.ofFloat(0,1f); valueAnimator.setDuration(2000l); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.addUpdateListener(v ->{ progressValue = (float) v.getAnimatedValue(); postInvalidate(); }); } valueAnimator.start(); } } 复制代码
效果图如下:
- Path的起点