Android绘图篇(三)——绘制Path路径及贝塞尔曲线
前言:前几篇稍微总结了下canvas和Path的内容,Paint都是顺带而过,今天准备来个Paint大详解,把Paint所有的api都过一遍,关于Paint,我准备用两到三篇博客来写,这一篇先写写之前没讲到的api,后面会重点介绍下Paint中setColorFilter、setShader、setXfermode这几个方法,因为这几个方法涉及到的知识挺多的,一时半会讲不完。
好了,我们主要总结setXX方法,getXX方法和isXX方法这里不做总结,弄懂了setXX,其它的都是获取setXX所设置的值,没什么好说的 先看下之前没讲过的api列表,然后一个个过:
public void setAlpha(int a) //设置透明度
public void setARGB(int a, int r, int g, int b) //设置A、R、G、B,分别代表透明度、Green、Blue的值
public void setColor(@ColorInt int color) //设置画笔颜色
public void setDither(boolean dither) //设置防抖动
public void setFakeBoldText(boolean fakeBoldText) //设置是否粗体
public void setFilterBitmap(boolean filter) //设置是否使用双线性过滤来绘制 Bitmap ,优化Bitmap放大绘制的效果
public void setFlags(int flags) //添加标识,用以实现特定的效果
public void setHinting(int mode) //设置画笔的隐藏模式
public void setLetterSpacing(float letterSpacing) //设置字符间的间距
public void setLinearText(boolean linearText) //设置线性文本
public PathEffect setPathEffect(PathEffect effect) //设置Path样式
public void setTextLocale(Locale locale) //设置文字绘制的区域
public void setTextSkewX(float skewX) //设置文字倾斜
public void setTextScaleX(float scaleX) //设置文字X方向拉伸
public void setStrikeThruText(boolean strikeThruText) //设置文字是否有删除线
public void setStrokeCap(Cap cap) //设置线帽样式
public void setStrokeJoin(Join join) //设置结合处的样式
public void setStrokeMiter(float miter) //设置画笔的倾斜度
一个一个来,走你!
1. setAlpha
设置透明度,取值范围从0~255。值越小,约模糊,举个栗子:
private void init() {
//初始化画笔
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(5);
mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(100, 100, 300, 300, mPaint);
canvas.save();
canvas.translate(200, 0);
mPaint.setAlpha(90);
canvas.drawLine(100, 100, 300, 300, mPaint);
canvas.restore();
}
效果:
左边直线的透明度是255,右边直线透明度是90,各位可以自己试一试。
2. setARGB
设置画笔颜色,参数分别是透明度、红、绿、蓝的颜色值,举例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setARGB(255,210,177,240);
mPaint.setTextSize(45);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归",100,150,mPaint);
}
效果:
好了,没什么可说的,颜色可以自己调。
3. setColor
同样是设置颜色,可以有如下几种设置方式:
1、 mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
2、 mPaint.setColor(Color.RED);
3、 mPaint.setColor(Color.parseColor("#ff0000"));
第一种获取资源文件中定义的颜色;第二种去Color中定义的常量颜色;第三种将十六进制的颜色代码转换成颜色。这几种方式都可以设置颜色,根据需求选择使用即可。
4. setDither
设置防抖动,使用也非常简答,就一行代码:
mPaint.setDither(true);
这个api现在用的不多了,因为现在的 Android 版本的绘制,默认的色彩深度已经是 32 位的 ARGB_8888 ,效果已经足够清晰了。只有当你向自建的 Bitmap 中绘制,并且选择 16 位色的 ARGB_4444 或者 RGB_565 的时候,开启它才会有比较明显的效果
5. setFakeBoldText
设置文字是否粗体:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setARGB(255,210,60,40);
//设置是否粗体
mPaint.setFakeBoldText(true);
mPaint.setTextSize(45);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归",100,150,mPaint);
}
就是文字加粗了:
6. setFilterBitmap
是否使用双线性插值法来来绘制 Bitmap ,优化Bitmap放大绘制的效果。其使用也很简单,我们来看下原图:
很漂亮,然后我们绘制该图:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap source = BitmapFactory.decodeResource(getResources(), R.drawable.test);
canvas.drawBitmap(source, 100, 100, mPaint);
}
如果在绘制之前加一行代码:
mPaint.setFilterBitmap(true);
你会发现在手机上运行的差别不大,因为你没放大,所以看不到差别,但放大后你会看到区别,下图是正常情况下放至最大的效果:
调用mPaint.setFilterBitmap(true);后放大至最大后的效果:
可以很明显的发现,两者虽然放大了都会有马赛克一样的东西,但是明显后者的看起来要相对柔和一点,不像第一张图那么生硬。其实这个api是调用了一种名叫双线性过滤的算法,用以优化图片放大时的显示效果,关于该算法本身,这里不做介绍,感兴趣的可以自行百度。
7. setFlags
设置标签,为画笔实现相应的功能。举个栗子?
mPaint.setFlags(Paint.ANTI_ALIAS_FLAG); //设置抗锯齿
上面的代码等价于:
mPaint.setAntiAlias(true); //如果设置为false,Paint.ANTI_ALIAS_FLAG的Flag会被清除。
还有设置文字的下划线:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//设置下划线
mPaint.setFlags(Paint.UNDERLINE_TEXT_FLAG );
mPaint.setARGB(255,210,60,40);
//设置是否粗体
mPaint.setFakeBoldText(true);
mPaint.setTextSize(45);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归",100,150,mPaint);
}
文字会被绘制下划线:
可以将多种Flag同时设置给Paint对象,中间用|隔开,例如:
mPaint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.UNDERLINE_TEXT_FLAG); //同时设置抗锯齿和下划线
常用的Flag有如下几种:
Paint.ANTI_ALIAS_FLAG :抗锯齿标志
Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志
Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志
Paint.UNDERLINE_TEXT_FLAG : 下划线
Paint.STRIKE_THRU_TEXT_FLAG : 中划线
Paint.FAKE_BOLD_TEXT_FLAG : 加粗
Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志
Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志
Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志
8. setHinting
说是设置画笔的隐藏模式,可选值有以下两种:
public static final int HINTING_OFF = 0x0;
public static final int HINTING_ON = 0x1;
我试验了下,两者没什么区别。网上说这个方法几乎不用了。
9. setLetterSpacing
设置字符间的间距:
mPaint.setLetterSpacing(2);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归", 100, 150, mPaint);
字符间隔两个字符:
10. setLinearText
这个是用来控制字体的平滑缩放:
mPaint.setLinearText(true);
它也有个对应的Flag:
mPaint.setFlags(Paint.LINEAR_TEXT_FLAG);
两者效果是一样的,这个Flag一般配合SUBPIXEL_TEXT_FLAG这个Flag一起使用,不过平时几乎用不到,本人试验了一次,没什么区别。
11. setPathEffect
用来设置Path的效果的,在路径上添加一定的效果,参数是PathEffect的子类,主要有六个子类,一个一个说
1. CornerPathEffect
在此之前,我们先来看段代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(50,100);
mPath.lineTo(80,120);
mPath.lineTo(120,60);
mPath.lineTo(150,90);
mPath.lineTo(210,130);
mPath.lineTo(250,70);
mPath.lineTo(300,150);
mPath.lineTo(320,60);
mPath.lineTo(360,90);
mPath.lineTo(400,130);
mPath.lineTo(410,70);
mPath.lineTo(450,150);
mPath.lineTo(500,70);
mPath.lineTo(530,150);
mPath.lineTo(550,60);
mPath.lineTo(600,90);
mPath.lineTo(650,130);
mPath.lineTo(700,70);
mPath.lineTo(800,150);
mPath.lineTo(840,90);
mPath.lineTo(890,130);
mPath.lineTo(950,70);
mPath.lineTo(1020,150);
canvas.drawPath(mPath,mPaint);
}
就绘制了一个Path,看下不加任何效果是什么样子:
很简单,如果设置Path的效果为CornerPathEffect呢?在绘制Path加入如下代码:
mPaint.setPathEffect(new CornerPathEffect(10));
然后你会发现:
是不是发现这条路径变圆滑了,折角出也不再是生硬的,看起来有点曲线的味道了,这就是CornerPathEffect所能施加在Path上的效果。构造CornerPathEffect只需要传入一个参数,即半径:
public CornerPathEffect(float radius)
啥意思,我们来看张图:
假设这是一条Path的两条线段,如果不设置任何效果,两条线段相交的地方会以折角的形式存在,如果设置了CornerPathEffect,A和B之间将会被图中所示的AB弧线所替代,所以看起来圆滑了点,传入的半径的值就是图中圆形的半径。如果传入的半径增大,则:
可以看到,传入的半径的值越大,替代折角的弧线也会越大。 通过传入的半径,寻找和两条线段同时相切的圆,用两个切点之间的圆弧替代折角,就是这个CornerPathEffect的原理。 大家可以自行试一试。
2. DashPathEffect
为路径设置虚线效果。看下构造方法:
public DashPathEffect(float intervals[], float phase)
在解释参数之前,我们先来看一下虚线是怎么形成的。我们都知道,如果你画一条直线,然后在直线上的某几点用橡皮擦擦了一下,那么你擦除后就会形成一条虚线,如下:
由图可以看到,虚线是实线和空白交替形成的。即实线+空白+实线+空白+实线+空白+实线+空白。。。于是我们可以解释下第一个参数了:
1、float intervals[]
需要传入一个float数组,这个数组代表整条虚线中的最小单元,什么意思,假如我们传入[70,30,30,20],那么整条线的最小单元就是:实线+空白+实线+空白,第一条实线的长度是70,第二条空白的长度是30,第三条实线的长度是30,第四条空白的长度是20,整条虚线就是由这样一个最小单元循环构成。这就是最小单元的定义,举个例子:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(100, 100);
mPath.lineTo(800, 100);
//定义最小单元
float[] intervals = new float[]{70, 30, 30, 20};
//设置Path虚线效果
mPaint.setPathEffect(new DashPathEffect(intervals, 0));
canvas.drawPath(mPath, mPaint);
}
然后就可以绘制出虚线来了:
可以看到,整条虚线就是由最小单元循环而成的。
2、phase
先传个值看下效果,就好理解了:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.moveTo(100, 100);
mPath.lineTo(800, 100);
//定义最小单元
float[] intervals = new float[]{70, 30, 30, 20};
//phase传默认值0
mPaint.setPathEffect(new DashPathEffect(intervals, 0));
//画第一条路径
canvas.drawPath(mPath, mPaint);
//画布向下平移100
canvas.translate(0, 100);
//phase传50
mPaint.setPathEffect(new DashPathEffect(intervals, 50));
//画第条路径
canvas.drawPath(mPath, mPaint);
}
这里画了两条虚线路径,方便于比较,先看下效果:
可以明显看到下面第一个虚线单元的第一条实线的距离比上面短了很多,其实你可以理解为把这条虚线向左边移动了50,所以导致第一个虚线单元的第一条实线短了50,所以只剩下20的长度了,有人说,那你向左移动,最后面不应该比第一条虚线短么,因为你截掉了50的长度啊? 其实不会的,截出去的50会按照最小单元的规则补偿回来,不过它会绘制完最小单元中的某一段,比如到最后,刚好需要绘制一段50的实线,那么它会把这个实线绘制完成,不会因为长度的原因只绘制其中的一部分。 所以在上面的图中,第二条线比第一条看起来还要稍微长那么一点点。
3. PathDashPathEffect
假设一有一条路,为了美观,我在路上铺满了方形的青石板,这个青石板起到一个装饰路面的作用,路还是那一条路,只是铺上了一块块青石板,变得更好看了,原来路面的泥土也被青石板所替代。在Android中,PathDashPathEffect就是起到这样一个装饰Path的作用的,会在一条Path上铺满某一样东西。 好吧,看下其构造方法:
public PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)
解释下参数:
1、shape:就相当于我们的青石板,不过青石板是方形的,它也具有形状,既然有形状,当然可以用Path绘制出来,所以这里传入的是青石板的形状的path。
2、advance:表示两块青石板之间的距离,没什么好说的。
3、advance:同上面的phase,偏移量。
4、style:表示在遇到转角时,如何操作印章以使转角平滑过渡,取值有:Style.ROTATE、Style.MORPH、Style.TRANSLATE;Style.ROTATE表示通过旋转印章来过渡转角;Style.MORPH表示通过变形印章来过渡转角;Style.TRANSLATE表示通过位移来过渡转角。
好了,搞一下呗:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画出原始路径
Path path = new Path();
path.moveTo(100, 600);
path.lineTo(400, 100);
path.lineTo(700, 900);
canvas.drawPath(path, mPaint);
//构建印章路径,采用lineTo,然后close来构建一个矩形Path.
Path stampPath = new Path();
stampPath.moveTo(0, 0);
stampPath.lineTo(20, 0);
stampPath.lineTo(20, 10);
stampPath.lineTo(0, 10);
stampPath.close();
//使用印章路径效果
canvas.translate(0, 200);
mPaint.setPathEffect(new PathDashPathEffect(stampPath, 35, 0, PathDashPathEffect.Style.TRANSLATE));
canvas.drawPath(path, mPaint);
}
好吧,看下效果呗:
嘻嘻,小方块铺满整条路咯。我们缩小下青石板之间的距离:
mPaint.setPathEffect(new PathDashPathEffect(stampPath, 15, 0, PathDashPathEffect.Style.TRANSLATE));
然后:
奶奶的,缩小为0,则:
你会发现路径没有装饰效果了,和最初一样。好了,最后的Style,这里就不一一去试了,感兴趣的可以自己试一试,找到符合自己需求的就行啦。
4. DiscretePathEffect
主要是给Path添加一种杂乱的效果的:
public DiscretePathEffect(float segmentLength, float deviation)
正常情况下,如果你想要最杂乱的线,你可以把第一个参数尽可能的小,第二个参数尽可能的大。相同的偏移距离下,segmentLength越小,则显得越杂乱;相同的segmentLength下,deviation越大显得越杂乱。好了 看代码:
mPaint.setPathEffect(new DiscretePathEffect(10, 15));
canvas.drawPath(mPath, mPaint);
canvas.translate(0, 150);
mPaint.setPathEffect(new DiscretePathEffect(10, 5));
canvas.drawPath(mPath, mPaint);
canvas.translate(0, 150);
mPaint.setPathEffect(new DiscretePathEffect(10, 10));
canvas.drawPath(mPath, mPaint);
canvas.translate(0, 150);
mPaint.setPathEffect(new DiscretePathEffect(3, 10));
canvas.drawPath(mPath, mPaint);
于是:
将第一条线和第二条线对比;第三条和第四条对比可以发现上述规律是成立的。至于其中的效果,大家自行实验体会。
5. ComposePathEffect与SumPathEffect
这两个都是用来合并路径特效的。既然分了两个,说明两者是有一定区别的,我们通过实验来看:
//绘制原始路径
canvas.drawPath(mPath, mPaint);
//圆角特效
canvas.translate(0, 150);
CornerPathEffect cornerPathEffect = new CornerPathEffect(30);
mPaint.setPathEffect(cornerPathEffect);
canvas.drawPath(mPath, mPaint);
//虚线特效
canvas.translate(0, 150);
DashPathEffect dashPathEffect = new DashPathEffect(new float[]{22, 10, 40, 15}, 5);
mPaint.setPathEffect(dashPathEffect);
canvas.drawPath(mPath, mPaint);
//ComposePathEffect叠加特效
canvas.translate(0, 150);
mPaint.setPathEffect(new ComposePathEffect(dashPathEffect, cornerPathEffect));
canvas.drawPath(mPath, mPaint);
//SumPathEffect叠加特效
canvas.translate(0, 150);
mPaint.setPathEffect(new SumPathEffect(cornerPathEffect, dashPathEffect));
canvas.drawPath(mPath, mPaint);
效果:
得出两点结论:
需要注意的是:
ComposePathEffect是先应用第二个参数的效果,在应用第一个参数的效果,上述例子中,先应用cornerPathEffect,然后再应用dashPathEffect。 这一点我们从源码的注释中也能发现:
/**
* Construct a PathEffect whose effect is to apply first the inner effect
* and the the outer pathEffect (e.g. outer(inner(path))).
*/
public ComposePathEffect(PathEffect outerpe, PathEffect innerpe) {
native_instance = nativeCreate(outerpe.native_instance,
innerpe.native_instance);
}
SumPathEffect则是按照参数的顺序来合并路径的。
12. setTextLocale
设置文字的地理位置,取值是Local中的常量,但我试验了下,相同的文字,设置不同Local效果差不多,有细微的差别,比如,不设置的时候是这个样子的:
mPaint.setTextLocale(Locale.KOREA);
是这样的:
少年的“少”和“归”有细微的差别,其它的没哈区别。
13. setTextSkewX
设置文字倾斜,没什么好说的,一行代码:
public void setTextSkewX(float skewX)
传入一个float类型的值,传入正值向倾斜,传入负值向右倾斜:
//原始文字
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归", 100, 100, mPaint);
//向左清晰
canvas.translate(0, 100);
mPaint.setTextSkewX(-0.2f);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归", 100, 100, mPaint);
//向右清晰
canvas.translate(0, 100);
mPaint.setTextSkewX(0.2f);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归", 100, 100, mPaint);
效果:
14. setTextScaleX
设置文字X方向拉伸,举个例子:
//原始文字
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归", 100, 100, mPaint);
//拉伸
canvas.translate(0, 100);
mPaint.setTextScaleX(1.2f);
canvas.drawText("三旬尚远浓烟散,一如年少迟夏归", 100, 100, mPaint);
嘻嘻~
15. setStrikeThruText
setStrikeThruText(true),用来文字加删除线:
16. setStrokeCap
设置线帽的样式,什么为线帽:
mPaint.setStrokeWidth(50);
canvas.drawLine(100, 100, 800, 100, mPaint);
//无线帽
canvas.translate(0, 100);
mPaint.setStrokeCap(Paint.Cap.BUTT);
canvas.drawLine(100, 100, 800, 100, mPaint);
//圆形线帽
canvas.translate(0, 100);
mPaint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawLine(100, 100, 800, 100, mPaint);
//方形线帽
canvas.translate(0, 100);
mPaint.setStrokeCap(Paint.Cap.SQUARE);
canvas.drawLine(100, 100, 800, 100, mPaint);
看图:
第一条是原始直线,第二条没加线帽,第三条第四条首尾都比原始直线多出来一小部分,多出来的这一部分就叫做线帽,Paint.Cap.ROUND是圆形的线帽,Paint.Cap.SQUARE是方形的线帽,看图理解起来应该很容易。
17.setStrokeJoin
设置线段结合处的样子。举例:
Path path1= new Path();
path1.moveTo(100,100);
path1.lineTo(400,100);
path1.lineTo(400,200);
//线段结合处为直线
mPaint.setStrokeJoin(Paint.Join.BEVEL);
canvas.drawPath(path1,mPaint);
canvas.translate(0,200);
Path path2= new Path();
path2.moveTo(100,100);
path2.lineTo(400,100);
path2.lineTo(400,200);
//线段结合处为锐角
mPaint.setStrokeJoin(Paint.Join.MITER);
canvas.drawPath(path2,mPaint);
canvas.translate(0,200);
Path path3= new Path();
path3.moveTo(100,100);
path3.lineTo(400,100);
path3.lineTo(400,200);
//线段结合处为圆角
mPaint.setStrokeJoin(Paint.Join.ROUND);
canvas.drawPath(path3,mPaint);
可以很明显的看到区别:
依次向下是Paint.Join.BEVEL、Paint.Join.MITER、Paint.Join.ROUND。
18. setStrokeMiter
设置画笔的倾斜角度,想想假如给你一直记号笔,让你画一个三角形,当你笔和纸面垂直时画出的和笔和纸面呈45度夹角画出来三角形会有什么不同?
mPaint.setStrokeWidth(20);
mPath.moveTo(100,100);
mPath.lineTo(400,100);
mPath.lineTo(400,200);
mPath.close();
canvas.drawPath(mPath,mPaint);
canvas.translate(0,200);
mPaint.setStrokeMiter(0.8f);
canvas.drawPath(mPath,mPaint);
canvas.translate(0,200);
mPaint.setStrokeMiter(1.5f);
canvas.drawPath(mPath,mPaint);
给个效果自己体会,还是有不同的:
呼,终于讲完了,下一篇讲讲Paint的setColorFilter、setShader、setXfermode这几个方法。累屎了-。-
下一篇:Android绘图篇(五)——setShader 设置着色器