自定义view(二) Path绘画详解 圆形进度条
目录
简介
基础api
圆形进度条
总结
简介
view的绘制可以由无数个形状组成,在canvas基础图形绘制中,我们已经把api提供好的基本图形讲过了。Path之所以单独一章出来是因为path可以由我们自己定义形状。在绝大多数情况下,只靠上篇文章中讲的那些图形并不能实现设计师设计出来那些优美炫酷的效果。当然对于一个炫酷的效果,path也只是一块砖,要想完成炫酷效果,要一步步来。以下做demo的时候要把硬件加速关掉。以免出异常。
基础api
以下的接口介绍,并没有区分api的版本号,比如API21以后的方法也都包括在内。大家使用的时候要自己注意。其实path的路径可以看成是一系列的点的集合。所以保存这些点都是有顺序的。
作用 | 相关接口 | 备注 |
---|---|---|
移动起点 | moveTo | 移动到指定位置,当做下一次操作的起点。 |
连接直线 | lineTo | 从已经完成的最后一个点,连接到指定点的直线 |
设置最后的点 | setLastPoint | 用参数中指定的点代替原来的终点作为最后一个点 |
形成闭合回路 | close | 连接最后一个点和第一个点形成一个闭合的图形 |
rxxxx族 | rLineTo rMoveTo rCubic等 |
与lineto,moveTo,cubicTo作用一样,只是r开头的都是以path的最后一个点为原点,而不是以(0,0)为原点 |
添加路径 | addCircle addRoundRect等add方法 | 将add后面代表的路径等添加到path中. |
2个path合并 | op函数 | 对两个path进行与,非等操作,获取结果 |
贝塞尔曲线 | quadTo,cubicTo | 二次三次贝尔曲线 |
填充模式 | setFillType等有关filltype的方法 | 设置变化填充等效果 |
重置路径 | reset,remind |
清除Path中的内容 reset不保留内部数据结构,但会保留FillType. rewind会保留内部的数据结构,但不保留FillType |
矩阵变化 | transfor | 以矩阵的形式改变path |
位置偏移 | offset | 对整体path进行位移 |
优化内存 | incReserve | 提示剩余还有多少个点需要加入到path中。 |
计算边界 | computeBounds(Rect ct,boolean flag) | 计算path边界,并将边界值赋予ct,第二个参数已经无意义。如果path只包含0,或1个点,则返回(0,0,0,0) |
还有其他一些接口,我们看名字就知道其含义,就没有必要一一列举了。
1 add族函数
通过add函数比如addRect(),addCircle()函数,可以为path添加一些固定的图案,但是在此add组函数中,有一个参数是Path.Direction, 这是一个枚举类型,它的值为:CW,CCW,分别为顺时针和逆时针,表示添加的图案是以哪种顺序添加。如果只是添加一个图案没有后续操作,那么这个参数体现不出来,因为它代表团的顺序,不会改变添加的图案。比如下图:
我们移动原点之后,通过add方法顺时针添加A,B,C,D组成的绿色的矩形,因为是顺时针添加,这个时候path的最后一个点是D,而我们通过setLastPoint方法将(-50,600)这个点设置为最终点,因此D就被舍弃,最终形成了黑色path的结果。如果这个时候是逆时针添加。那么最后一个点就是B(220,-100),那么就是(-50,600)代替了B。那么最终的path是由A,D,C,E(-50,600)组成。
private void drawAddRect(Canvas canvas) {mPaint.setStrokeWidth(5);mPaint.setColor(Color.RED);canvas.translate(500, 500);mPaint.setColor(Color.BLACK);//顺时针添加一个矩形mPath.addRect(new RectF(100, -100, 220, 100), Path.Direction.CW);//将最后一个点设置为(-50,400)mPath.setLastPoint(-50, 400);canvas.drawPath(mPath, mPaint);}
下面是通过顺时针,逆时针添加一个圆形,并且沿着圆形书写文字,效果如下:
2 setLastPoint(px,py)
这个接口就是将点(px,py)设置为path的最后一点。就是代替原来的最后一点。具体使用在上面的代码中已经使用。我们可以通过path的源码看出这一点:
//这是setlastpath最终实现方法,属于lib中的skia模块,
//如果当前path是空,那么相当于执行movtTo(),
//否则将path的最后一个点指向此点。原来的最后点舍弃void SkPath::setLastPt(SkScalar x, SkScalar y) {SkDEBUGCODE(this->validate();)int count = fPathRef->countPoints();if (count == 0) {this->moveTo(x, y);} else {SkPathRef::Editor ed(&fPathRef);ed.atPoint(count-1)->set(x, y);}
}
3 rLineTo, rMoveTo等r族函数
这类函数和不加r的区别就是,r族的函数都是相对path的最后一个点为标准作出的操作,也就是都是相对位置。不是绝对位置。(绝对位置是指相对于原点的位置)。
private void drawPathClose(Canvas canvas) {mPaint.setColor(Color.BLACK);mPaint.setStrokeWidth(5);//移动到100,100mPath.moveTo(100, 100);//画(100,100)到(200,350)的直线mPath.lineTo(200, 350);//再画从(200,350)到(400,80)的直线mPath.lineTo(400, 80);//以(400,80)为原点,画到(100,100)的点。//如果是lineTo(100,100),那么就和起始点重合,整个path应该是一个闭合的图案。mPath.rLineTo(100,100);// mPath.lineTo(100, 100);}
效果如下:
我们可以看看rLineTo的底层源码:
void SkPath::rLineTo(SkScalar x, SkScalar y) {this->injectMoveToIfNeeded(); // This can change the result of this->getLastPt().SkPoint pt;this->getLastPt(&pt);this->lineTo(pt.fX + x, pt.fY + y);
}
从代码中我们可以看出,首先获取到path的最后一个点pt,然后再执行lineTo(pt.fx+x, pt.fy+y), 这就是相对最后一个点做的移动,如果以原点看的话,他的绝对位移应该是pt.px+x, 和pt.fy+y, . 所以一定要分清楚相对和绝对的关系。
4 offset偏移
offset函数重载了2个方法,offSet(tx,ty) , offSet(tx,ty,Path dst), 第一个函数就是直接将path的每个点的坐标(x,y)做位移操作 x+tx, y+ty. 第二个函数首先要将当前的path复制给dst,然后对dst做操作,所以移动之后dst是操作后的path。原来的path并没有变化。
这一点我们可以通过源码看到:
//先将当前path复制到dst。然后对dst做offset(dx,dy)的操作public void offset(float dx, float dy, @Nullable Path dst) {if (dst != null) {dst.set(this);} else {dst = this;}dst.offset(dx, dy);
}
我们可以可以看如下代码:
private void drawOffSet(Canvas canvas) {mPath = new Path();mPath.moveTo(300, 300);mPath.lineTo(400, 350);mPath.lineTo(200, 400);//将三个点画出来mPaint.setColor(Color.RED);mPaint.setStrokeWidth(10);canvas.drawPoint(400, 350, mPaint);canvas.drawPoint(200, 400, mPaint);mPaint.setColor(Color.BLACK);mPaint.setStrokeWidth(5);Path tempPath = new Path();tempPath.lineTo(600, 100);//通过offset对path进行位移,因为参数中包含temppath,所以先将path复制给temppath//然后对temppath进行位移。mPath.offset(300, 0, tempPath);canvas.drawPath(mPath, mPaint);mPaint.setColor(Color.BLUE);canvas.drawPath(tempPath,mPaint);}
效果如下:
5 Op族函数,op()
op函数表示2个path做合并操作,至于怎样合并,根据参数之一的Path.Op这个枚举类型来决定。
枚举值 | 效果 |
---|---|
DIFFERENCE | path1 减去path2之后剩余的部分 |
REVERSE_DIFFERENCE | path2减去path1剩余的部分 |
INTERSECT | path1和path2想交的部分。 |
XOR | 包含Path1与Path2但不包括两者相交的部分,以path1,path2为真个集合,正好是对INTERSECT取反 |
UNION | 包含path1和path2的全部 |
这就是2个圆做XOR的效果
6 fillType
filetype的作用是决定path怎样填充,在讲这个之前 ,我们先看两个小例子,首先我们通过如下代码,画两个有部分相交的圆:
private void drawFillType(Canvas canvas){mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(Color.RED);mPath.addCircle(400,400,200, Path.Direction.CW);//先正向画一个相交的圆,mPath.addCircle(700,400,200, Path.Direction.CW);//再反向画一个相交的圆// mPath.addCircle(700,400,200, Path.Direction.CCW);canvas.drawPath(mPath,mPaint);
}
添加2个同样的圆只因添加顺序不一样就导致不一样的效果,这是因为绘制的填充类型起作用。(因为如果path中路径无交叉的情况,跟大家理解的一直,我们主要想path中有想交的情况),FileType是枚举类型,其值如下:
我们再绘制path的时候,即使不通过setFillType设置属性,Path也会默认一个,默认值是WINDING,可以翻译作为是:非零环绕原则,这个原则是指,在path之内的一点,向外做射线,一直穿透到path界外为止,这个时候肯定与path有交点,如果与顺时针的path边界线相交则加1(初始值默认是0),如果与逆时针的边界线相交则减1,把所有点计算完毕之后,如果结果为0, 那么就不在path之内,就不需要填充,也就是不需要绘制 。如果结果不为0.就需要绘制。如下图所示:
看图,这就是我们再上面绘制的第二种情况,使用的填充策略是默认的:WINDING,在我们选中的点向外做三条射线,每天射线与path都有2个交点,我们可以看出,交点1是顺时针,交点2是逆时针,相加结果为0.所以不会绘制这个点。以此类推,在中央灰色区域的任何一点向外做射线,与path的交点之和都是0 所以都不绘制。 而红色区域的任何一点,向外做射线,可能与path有一个交点 ,也可能有三个, 相加的结果都不是0. 所以都需要绘制填充。
我们看完了WINDING, 还有一个INVERSE_WINDING, 见名知意,它是对WINDING取反,所以它需要填充的点是哪些做射线之后与path的交点相加为0的那些点。仍然以上面的作为例子,我们猜测一下,绘制的红色区域应该变成两个圆相交的那个区域和那些path边界外的区域。代码如下:我们只需通过setFillType(Path.FillType.INVERSE_WINDING)来设置填充策略:
private void drawFillType(Canvas canvas){mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(Color.RED);mPath.setFillType(Path.FillType.INVERSE_WINDING);mPath.addCircle(400,400,200, Path.Direction.CW);mPath.addCircle(700,400,200, Path.Direction.CCW);canvas.drawPath(mPath,mPaint);}
效果图如下:
接下来我们看EVEN_ODD这个值,我们可以看官方文档:Specifies that "inside" is computed by an odd number of edge crossings. 我们可以看出,他和WINDING 一样,也是从某一点向path之外做射线,但是与WINDING不同的是,他只在乎交点的个数,不关心方向的问题。 与path边界的交点个数为奇数,那么就需要绘制填充,为偶数就不需要。所以我们仍然以上面的代码为例。只是setFillType(Path.FillType.WINDING) 改为:setFillType(Path.FillType.EVEN_ODD),因为与方向无关,所以无论我们以什么样的顺序添加2个园,他都不会绘制填充两圆相交的模块。效果均为如下:
而INVERSE_EVEN_ODD与EVEN_ODD正好相反,绘制填充哪些交点个数为偶数的区域。
7 reset,remind重置路径
reset不保留内部数据结构,但会保留FillType. 会保留内部的数据结构,但不保留FillType
8 incReserve
这个函数是提示还有几个点需要添加,看api说明是可以优化存储,我看这个函数对应的源码如下:
void incReserve(int additionalVerbs, int additionalPoints) {SkDEBUGCODE(this->validate();)size_t space = additionalVerbs * sizeof(uint8_t) + additionalPoints * sizeof (SkPoint);this->makeSpace(space);SkDEBUGCODE(this->validate();)}
这个函数通过调用makeSpace一次性的开辟了一块内存。而通过lineto或者其他方式添加每次都用调用makeSpace来开辟内存。 我觉的是因为一次性开辟一大块内存,比多次开辟小内存。会减少内存碎片。这只是给个人的猜想。希望确认这块原理的人给我留了言不吝赐教。
圆形进度条
华为的手机管家页面,打开之后是一个圆形的评分页面,其实也相当于一个进度条。其实就是一个view绘制的简单应用
这里的动画效果我设置的时间比较长,所以看着很慢。代码如下:
public class CircleProgressView extends View {public static final String TAG = CircleProgressView.class.getSimpleName();private ValueAnimator mAnimator;/*** 内外圆距离竖线的距离*/public static final int CIRCLE_PADDING = 15;/*** 表示进度的竖线的长度*/private int scaleLength = 30;/*** 圆半径*/private int circleRadius = 300;/*** 进度*/private float process;Paint mPaint;/*** 圆心*/Point mCenterPoint;/*** 曲线渐进色*/SweepGradient sweepGradient;/*** 两根直线之间的角度间隔*/int degreeInterval = 4;int maxValue;int currentValue;/*** 内圆外切矩形和外圆内切矩形*/RectF innerRectf, exRectf;public CircleProgressView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);Log.e(TAG, "THE X ==" + getX() + ", y==" + getY()+ ", left=" + getLeft() + ", right=" + getRight()+ ", top=" + getTop() + ", width=" + getWidth() + ", height==" + getHeight());int centerx = (getRight() - getLeft()) / 2;int centery = (getBottom() - getTop()) / 2;mCenterPoint.set(centerx, centery);innerRectf = new RectF(mCenterPoint.x - circleRadius + CIRCLE_PADDING,mCenterPoint.y - circleRadius + CIRCLE_PADDING, mCenterPoint.x + circleRadius - CIRCLE_PADDING,mCenterPoint.y + circleRadius - CIRCLE_PADDING);exRectf = new RectF(mCenterPoint.x - circleRadius - CIRCLE_PADDING - scaleLength,mCenterPoint.y - circleRadius - CIRCLE_PADDING - scaleLength, mCenterPoint.x + circleRadius + CIRCLE_PADDING + scaleLength,mCenterPoint.y + circleRadius + CIRCLE_PADDING + scaleLength);}private void init() {mPaint = new Paint();//抗锯齿mPaint.setAntiAlias(true);mCenterPoint = new Point();//设置圆弧的渐变int[] gradients = {Color.BLUE, Color.GREEN, Color.RED, Color.YELLOW, Color.parseColor("#ff00ff")};sweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, gradients, null);}@Overridepublic void onDraw(Canvas canvas) {super.onDraw(canvas);Log.e(TAG, "IN THE ON DRWA");drawScaleCircleProress(canvas);}public void setProgress(float progress) {this.process = progress;invalidate();}/*** 带有刻度的原型进度条** @param canvas 画布*/private void drawScaleCircleProress(Canvas canvas) {mPaint.setStrokeWidth(3.0f);drawInnerCircle(canvas);drawExcircle(canvas);drawLines(canvas);drawText(canvas, mCenterPoint, "测试文字");/* Log.e(TAG,"THE X =="+getX()+", y=="+getY()+", left="+getLeft()+", right="+getRight()+", top="+getTop());*/}/*** 画内圆** @param canvas 画布*/private void drawInnerCircle(Canvas canvas) {//表示空心,这样画出的是线,否则就是实心的图形mPaint.setStyle(Paint.Style.STROKE);//设置渐近线mPaint.setShader(sweepGradient);mPaint.setStrokeWidth(5.0f);canvas.drawArc(innerRectf, 0, -180, false, mPaint);mPaint.setShader(null);}/*** 画外圆** @param canvas 画布*/private void drawExcircle(Canvas canvas) {//表示空心,这样画出的是线,否则就是实心的图形mPaint.setStyle(Paint.Style.STROKE);//设置渐近线mPaint.setShader(sweepGradient);mPaint.setStrokeWidth(5.0f);canvas.drawArc(exRectf, 0, -180, false, mPaint);mPaint.setShader(null);/* mPaint.setStyle(Paint.Style.STROKE);mPaint.setShader(sweepGradient);canvas.drawCircle(centerPoint.x,centerPoint.y,circleRadius+CIRCLE_PADDING+scaleLength,mPaint);mPaint.setShader(null);*/}private void drawLines(Canvas canvas) {canvas.save();mPaint.setColor(Color.parseColor("#aabbcc"));canvas.drawPoint(mCenterPoint.x, mCenterPoint.y, mPaint);int size = 180 / degreeInterval;for (int i = 0; i <= size; i++) {canvas.drawLine(mCenterPoint.x + circleRadius, mCenterPoint.y,mCenterPoint.x + circleRadius + scaleLength, mCenterPoint.y, mPaint);canvas.rotate(-degreeInterval, mCenterPoint.x, mCenterPoint.y);}canvas.restore();canvas.save();if (currentValue > 0) {//因为需要有左边向右边依次画效果,左右需要先旋转-180c,保证画笔在x轴的负方向。canvas.rotate(-180, mCenterPoint.x, mCenterPoint.y);mPaint.setColor(Color.parseColor("#ff00ff"));int sizeSelect = (int) (process * 180) / degreeInterval;for (int i = 0; i <= sizeSelect; i++) {canvas.drawLine(mCenterPoint.x + circleRadius, mCenterPoint.y,mCenterPoint.x + circleRadius + scaleLength, mCenterPoint.y, mPaint);canvas.rotate(degreeInterval, mCenterPoint.x, mCenterPoint.y);}}canvas.restore();}private void drawText(Canvas canvas, Point centerPoint, String data) {canvas.save();mPaint.setTextSize(40);mPaint.setStrokeWidth(2);float stringWidth = mPaint.measureText(data);float beginx = (getRight() - getLeft() - stringWidth) / 2;canvas.drawText(data, beginx, centerPoint.y, mPaint);canvas.restore();}private void startAnimator(int start, int end, long animTime) {mAnimator = ValueAnimator.ofInt(start, end);mAnimator.setDuration(animTime);mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {int value = (int) animation.getAnimatedValue();DecimalFormat df = new DecimalFormat("0.000");String progress = df.format((float) value / maxValue);process = Float.parseFloat(progress);Log.e(TAG, "the current process===" + progress);invalidate();}});mAnimator.start();}public void setCurrentValue(int value) {if (maxValue <= 0 || value < 0) {throw new IllegalArgumentException("the max value and set value must larger than 0," +"now the max value is " + maxValue + ", the set value=" + value);}if (value > maxValue) {throw new IllegalArgumentException("the max value must larger than set value," +"now the max value is " + maxValue + ", the set value=" + value);}currentValue = value;startAnimator(0, value, 1000 * 5);}public int getMaxValue() {return maxValue;}public void setMaxValue(int maxValue) {this.maxValue = maxValue;}
总结
以上就是关于path一些长用的接口,如果有错误的地方,请大伙留言指出。拜谢。 关于贝塞尔在这里没有介绍。因为用的太多。并且篇幅也不够的原因。留待下一篇单独讲。
自定义view(二) Path绘画详解 圆形进度条相关推荐
- Android自定义View之画圆环(进阶篇:圆形进度条)
前言: 如果你想读懂或者更好的理解本篇文章关于自定义圆环或圆弧的内容.请你务必提前阅读下Android自定义View之画圆环(手把手教你如何一步步画圆环).在这篇文章中,详细描述了最基本的自定义圆环的 ...
- 【5年Android从零复盘系列之二十】Android自定义View(15):Matrix详解(图文)【转载】
[转载]本文转载自麻花儿wt 的文章<android matrix 最全方法详解与进阶(完整篇)> [5年Android从零复盘系列之二十]Android自定义View(15):Matri ...
- android canvas_Android 自定义View篇(七)实现环形进度条效果
前言 Android 自定义 View 是高级进阶不可或缺的内容,日常工作中,经常会遇到产品.UI 设计出花里胡哨的界面.当系统自带的控件不能满足开发需求时,就只能自己动手撸一个效果. 本文就带自定义 ...
- 微信小程序进度条组件自定义数字_微信小程序之圆形进度条(自定义组件)
前言 昨天在微信小程序实现了圆形进度条,今天想把这个圆形进度条做成一个组件,方便以后直接拿来用. 根据官方文档自定义组件一步一步来 创建自定义组遇新是直朋能到件 第一步创建项遇新是直朋能到分览目结构 ...
- Android 自定义View 之 RectF用法详解
在之前通过Circle画了一个奥运五环,这次通过RectF来画矩形,常规的就是长方形正方形之类的. 还是新建一个自定义View,CustomViewRectF,然后继承View,实现里面的两个基本的构 ...
- 精通Android自定义View(四)自定义属性使用详解
1.简述 对于自定义属性,遵循以下几步,就可以实现: 自定义一个CustomView(extends View )类 编写values/attrs.xml,在其中编写styleable和item等标签 ...
- 【5年Android从零复盘系列之六】Android自定义View(1):基础详解(图文)
1.基础一:坐标计算 1.1 Android窗口坐标系计算以屏幕左上角为原点, 向右为X轴正向,向下为Y轴正向 1.2 View坐标系 [注意获取的坐标是像素值,不是dp值] [注意获取的坐标是像素值 ...
- Android 自定义View实现环形带刻度颜色渐变的进度条
上次写了一篇Android 自定义View实现环形带刻度的进度条,这篇文章就简单了,只是在原来的基础上加一个颜色渐变. 按照惯例,我们先来看看效果图 一.概述 1.相比于上篇文章,这里我们的颜色渐变主 ...
- android 自定义view 加载图片,Android自定义View基础开发之图片加载进度条
学会了Paint,Canvas的基本用法之后,我们就可以动手开始实践了,先写个简单的图片加载进度条看看. 按照惯例,先看效果图,再决定要不要往下看: 既然看到这里了,应该是想了解这个图片加载进度条了, ...
最新文章
- 如何从0写一个服务网关?
- JVM调优总结(二)
- JavaScript更改class和id的方法
- 一个cube的大小位置方向不断和另一个cube重合
- 线性表的顺序表示以及实现
- 机器学习笔记(三十二):集成学习、随机森林
- Kepware KEPServerEX连接Cimplicity OPC UA Server
- 夜神模拟器调试Android应用程序
- 水果店营业额下降原因,水果店如何提高营业额
- day8--socket回顾
- 批量修改MP3文件信息
- 渗透测试——痕迹清除
- 第一课 程小奔之晃一晃
- 基于android的手机位置系统,如何打造定位更精准的手机?基于Android系统的SDK方案了解一下~...
- 穿越时空的爱恋-Z80 CPU的前世今生
- java 一周的第一天_Java获取某年某周的第一天
- c语言自动售货机实验报告,c语言自动售货机实验报告(15页)-原创力文档
- Cookie经典案例—实现显示用户上次服务时间的显示
- ant design vue 表格table 默认选择几项getCheckboxProps
- 电脑店、大白菜PE工具去除捆绑软件的方法
热门文章
- 凸优化中凸函数定义、直线与线段、凸集、仿射集合、仿射函数
- Google 中文名发布 | 历史上的今天
- [转载]电机 螺旋桨 电池之间的关系(普及版)
- esp8266 system_partition_table_regist fail 蓝灯闪一下就灭
- Bootstrap弹性布局Flex
- 网传京东37岁程序员在工位猝死。当事人:我还没死,还能加班!
- php实训方案,最新PHP实训实践报告资料
- 爬取搜狗微信文章笔记1
- 扫地僧Backtrader量化回测与交易闭环生态系列教程
- Ubuntu 20.04 VNC server 搭建及客户端访问