上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定义View的详细绘制方法。如果把自定义View比作盖一座房子,那么上篇文章就相当于教会了我们怎么一步步的搭建房子的骨架,而本篇文章将要教会我们的是为房子的骨架添砖加瓦直至成型,甚至是怎么装修。
为了后文更为方便的讲解Canvas的常用方法的使用,我们先来做一些准备工作,创建一个自定义View框架,先初始化一下Paint画笔,并设置相关方法:
public class StudyView extends View {
private Paint mPaint;
private Context mContext;
public StudyView(Context context) {
super(context);
init(context);
}
public StudyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mPaint = new Paint();
mPaint.setAntiAlias(true); // 消除锯齿
mPaint.setStrokeWidth(5); // 设置笔尖宽度
mPaint.setStyle(Paint.Style.STROKE); // 不填充
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
Canvas提供drawArc()方法,通过传递不同的参数可用来绘制圆弧和扇形,此方法有两个重载方法,详细参数如下:
此方法第一个参数是一个RectF类,也是边界,就是把一个方法的left,top,right,bottom封装到了RectF类中,剩余参数与上一个方法一致。
接下来用着两个重载方法分别绘制两个90°的扇形和两个90°的圆弧:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制扇形
canvas.drawArc(0, 0, 200, 200, 0, 90, true, mPaint);
RectF rectF = new RectF(0, 0, 200, 200);
canvas.drawArc(rectF, 180, 90, true, mPaint);
// 绘制圆弧
canvas.drawArc(300, 0, 500, 200, 0, 90, false, mPaint);
RectF rectF1 = new RectF(300, 0, 500, 200);
canvas.drawArc(rectF1, 180, 90, false, mPaint);
}
绘制效果如下图所示,另外需要说明的一点是,drawArc的第五个参数startAngle中的角度,0°是指坐标系中第四象限中与x重合的角度,顺时针方向代表角度增大的方向,如下图中红色线条所示。
在Canvas中提供了drawBitmap方法,此方法可以让我们直接获取一张图片绘制到画布上,有了它可以让我们的自定义View锦上添花,同时也让我们实现一些复杂效果有了一个更加方便的途径。下面是drawBitmap的几个比较常用的重载方法:
在onDraw方法中drawBitmap的以上重载方法,注意在使用完Bitmap之后记得用Bitmap.recycle()来回收掉资源,以防止oom。
/** drawBitmap */
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), android.R.mipmap.sym_def_app_icon);
// 绘制图片
canvas.drawBitmap(bitmap, 0, 300, null);
// 将图片拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, null, new RectF(200, 300, 500, 500), null);
// 截取图片的四分之一拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2), new RectF(500, 300, 800, 500), null);
Matrix matrix = new Matrix();
matrix.postTranslate(800, 300); // 将bitmap平移到此位置
canvas.drawBitmap(bitmap, matrix, mPaint);
// 为防止oom,及时回收bitmap
bitmap.recycle();
效果如下图(红框内)。
canvas.drawCircle(100, 700, 100, mPaint);
效果如下图:
canvas.drawPoint(100, 700, mPaint); // 绘制一个点
float[] points = new float[] {
130, 700,
160, 700,
190, 700,
210, 700,
240, 700
};
canvas.drawPoints(points, 2, 4, mPaint); // 绘制一组点(代表跳过前两个值,处理4个值,也就是实际绘制2个点)
效果如下图:
在left、top、right、bottom围成的区域内绘制一个椭圆。
RectF rectF2 = new RectF(300, 600, 700, 800); // 创建一个RectF
canvas.drawOval(rectF2, mPaint);
效果如下图:
drawRect的参数非常好理解,这里就不啰嗦了,直接上代码看效果:
canvas.drawRect(rectF2, mPaint);
注:这里的rectF2即上文绘制椭圆时创建的RectF对象。
drawRoundRect是绘制圆角矩形,用法和drawRect类似,唯一不同的是多了两个参数:
上代码,看效果:
canvas.drawRoundRect(rectF2, 60, 30, mPaint);
这里为了突出两个方向的圆角弧度,特地将rx和ry设置差距比较大,效果如下图:
drawLine和drawLines一个是绘制一个点,一个是绘制一组点,其中drawLines中的float数组中四个值为一组点,其用法可以参照drawPoints。
canvas.drawLine(100, 820, 800, 820, mPaint);
float[] lines = new float[]{
100f, 850f, 800f, 850f,
100f, 900f, 800f, 900f,
100f, 950f, 800f, 950f
};
canvas.drawLines(lines, mPaint); // 按floats数组中,四个数为1组,绘制多条线
效果如下图:
上面的这些Canvas方法固然已经很强大了,但是我们如果想要绘制一些不规则的图形怎么办,这时候就要用到强大的drawPath()方法了,通过对Path进行设置不同的坐标、添加不同图形,最后传入drawPath方法中可以绘制出复杂的且不规则的形状。以下是drawPath的方法及参数:
这里的关键参数就是Path,Path类的方法较多,大部分用法类似,这里挑几个说一下:
接下来使用drawPath绘制一个楼梯:
// 使用 Path 绘制一个楼梯
Path path = new Path();
path.lineTo(0, 1000);
path.lineTo(100, 1000);
path.lineTo(100, 1100);
path.lineTo(200, 1100);
path.lineTo(200, 1200);
path.lineTo(300, 1200);
path.lineTo(300, 1300);
path.lineTo(400, 1300);
path.lineTo(400, 1400);
path.lineTo(0, 1400);
path.lineTo(0, 1000);
path.close();
canvas.drawPath(path, mPaint);
效果如下图:
再用drawPath方法绘制一个Android小机器人:
/ 使用 Path 绘制一个Android机器人
// 绘制两个触角
path.reset();
path.moveTo(625, 1050);
path.lineTo(650, 1120);
path.moveTo(775, 1050);
path.lineTo(750, 1120);
path.addArc(new RectF(600, 1100, 800, 1300), 180, 180); // 绘制头部
path.addCircle(666.66f, 1150, 10, Path.Direction.CW); // 绘制眼睛,CW:顺时针绘制, CCW:逆时针绘制
path.addCircle(733.33f, 1150, 10, Path.Direction.CW);
path.addRect(new RectF(600, 1200, 800, 1300), Path.Direction.CW); // 身体
canvas.drawPath(path, mPaint);
效果图如下:
最后,上文中Canvas示例的全部代码如下:
public class StudyView extends View {
private Paint mPaint;
private Context mContext;
public StudyView(Context context) {
super(context);
init(context);
}
public StudyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mPaint = new Paint();
mPaint.setAntiAlias(true); // 消除锯齿
mPaint.setStrokeWidth(5); // 设置笔尖宽度
mPaint.setStyle(Paint.Style.STROKE); // 不填充
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/** 1、drawArc */
// 绘制扇形
canvas.drawArc(0, 0, 200, 200, 0, 90, true, mPaint);
RectF rectF = new RectF(0, 0, 200, 200);
canvas.drawArc(rectF, 180, 90, true, mPaint);
// 绘制圆弧
canvas.drawArc(300, 0, 500, 200, 0, 90, false, mPaint);
RectF rectF1 = new RectF(300, 0, 500, 200);
canvas.drawArc(rectF1, 180, 90, false, mPaint);
/** 2、drawBitmap */
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), android.R.mipmap.sym_def_app_icon);
// 绘制图片
canvas.drawBitmap(bitmap, 0, 300, null);
// 将图片拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, null, new RectF(200, 300, 500, 500), null);
// 截取图片的四分之一拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2), new RectF(500, 300, 800, 500), null);
Matrix matrix = new Matrix();
matrix.postTranslate(800, 300); // 将bitmap平移到此位置
canvas.drawBitmap(bitmap, matrix, mPaint);
// 为防止oom,及时回收bitmap
bitmap.recycle();
/** 3、drawCircle */
canvas.drawCircle(100, 700, 100, mPaint);
/** 4、绘制一个点 */
canvas.drawPoint(100, 700, mPaint); // 绘制一个点
float[] points = new float[] {
130, 700,
160, 700,
190, 700,
210, 700,
240, 700
};
canvas.drawPoints(points, 2, 4, mPaint); // 绘制一组点(代表跳过前两个值,处理4个值,也就是实际绘制2个点)
RectF rectF2 = new RectF(300, 600, 700, 800); // 创建一个RectF
/** 5、drawOval 绘制椭圆 */
canvas.drawOval(rectF2, mPaint);
/** 6、drawRect 绘制矩形*/
canvas.drawRect(rectF2, mPaint);
canvas.drawRoundRect(rectF2, 60, 30, mPaint);
/** 7、drawLine */
canvas.drawLine(100, 820, 800, 820, mPaint);
float[] lines = new float[]{
100f, 850f, 800f, 850f,
100f, 900f, 800f, 900f,
100f, 950f, 800f, 950f
};
canvas.drawLines(lines, mPaint); // 按floats数组中,四个数为1组,绘制多条线
/** 8、drawPath */
// 使用 Path 绘制一个楼梯
Path path = new Path();
path.moveTo(0, 1000);
path.lineTo(100, 1000);
path.lineTo(100, 1100);
path.lineTo(200, 1100);
path.lineTo(200, 1200);
path.lineTo(300, 1200);
path.lineTo(300, 1300);
path.lineTo(400, 1300);
path.lineTo(400, 1400);
path.close();
canvas.drawPath(path, mPaint);
// 使用 Path 绘制一个Android机器人
// 绘制两个触角
path.reset();
path.moveTo(625, 1050);
path.lineTo(650, 1120);
path.moveTo(775, 1050);
path.lineTo(750, 1120);
path.addArc(new RectF(600, 1100, 800, 1300), 180, 180); // 绘制头部
path.addCircle(666.66f, 1150, 10, Path.Direction.CW); // 绘制眼睛,CW:顺时针绘制, CCW:逆时针绘制
path.addCircle(733.33f, 1150, 10, Path.Direction.CW);
path.addRect(new RectF(600, 1200, 800, 1300), Path.Direction.CW); // 身体
canvas.drawPath(path, mPaint);
}
}
完整效果图如下:
其实Canvas除了可以绘制图形之外,还可以绘制文字,Canvas的绘制文字的方法有drawText()、drawTextOnPath()、drawTextRun()等方法,在绘制文字是和Paint的结合更为紧密,所以讲绘制文字的方法放在下文和Paint一起讲可能效果会更好一些,好了,废话不多说了,接下来咱们就开始Paint的篇章。
为了更为清晰的讲解Paint的用法,先来新建一个自定义类,暂叫PaintStudyView,接下来创建一个它的大体骨架,在此类中定义了一些变量,变量的意义请见注释:
public class PaintStudyView extends View {
private Paint mTextPaint; // 绘制文字的Paint
private Paint mPointPaint; // 绘制参考点的Paint
private Context mContext;
private final static float Y_SPACE = 100; // y轴方向的间距
public PaintStudyView(Context context) {
super(context);
init(context);
}
public PaintStudyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true); // 消除锯齿
mTextPaint.setStrokeWidth(1); // 设置笔尖宽度
mTextPaint.setStyle(Paint.Style.FILL); // 填充
mTextPaint.setTextSize(30);
mPointPaint = new Paint();
mPointPaint.setAntiAlias(true);
mPointPaint.setStrokeWidth(5);
mPointPaint.setColor(Color.RED); // 将参考点的Paint设置为红色
mPointPaint.setStyle(Paint.Style.STROKE);// 不填充
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
drawText() 是Canvas的绘制文字中的最长用的方法,它只能按照从左至右的普通方式来绘制文字。
例如:我是一个自定义View的控件,start=1,end=6,截取后为:是一个自定
下面两个重载方法可以参考第二个很容易就能理解:
以下示例说明了文字的不同位置,同时也说明了第二个和第四个重载方法对字符串截取时的用法:
String str = "我是一个自定义View的控件";// 待绘制文字
float x = getWidth() / 2;
float y = 100;
canvas.drawPoint(x, y, mPointPaint); // 绘制参考点,便于观察文字处于x,y坐标的位置,从而来学习setTextAlign()方法
mTextPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(str, x, y, mTextPaint);
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(str, 0, 6, x, y, mTextPaint);
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(str.toCharArray(), 1, 6, x, y, mTextPaint);
效果图如下:
其中的红点为额外添加的参考坐标,目的是为了突出setTextAlign中参数的位置。
drawTextOnPath() 由方法名字我们就可以看出来他可以按照Path的走向来绘制文字,例如我们在path中传入一个圆弧,那么绘制出来的文字走向就是圆弧状的,是不是很酷,来看一下它的重载方法:
关键点:有一点一定要提的就是,这里的hOffset是相对于path路径的水平偏移量,而vOffset也是相对于path路径的垂直偏移量,这么说可能还有点不清楚,结合下面的示例来说明,请仔细体会这里的意思:
// 1、下开口圆弧方向绘制文字
mTextPaint.setTextAlign(Paint.Align.LEFT);
y += Y_SPACE;
Path path = new Path();
path.addArc(new RectF(x - 150, y, x + 150, y + 300), 180,180);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint); // 按照path路径绘制文字,不偏移
canvas.drawTextOnPath(str, path, 30, 30, mTextPaint);// 向水平、垂直方向各偏移30
canvas.drawTextOnPath(str, path, 60, 60, mTextPaint);// 向水平、垂直方向各偏移60
// 2、上开口圆弧方向绘制文字
path.reset();
y += Y_SPACE;
path.addArc(new RectF(x - 150, y, x + 150, y + 300), 0, 180);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 30, mTextPaint);
canvas.drawTextOnPath(str, path, 60, 60, mTextPaint);
path.close();
// 3、竖直方向绘制文字
path.reset();
path.moveTo(200, y);
path.lineTo(200, y + 4 * Y_SPACE);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 60, mTextPaint);
y += Y_SPACE;
y += Y_SPACE;
y += Y_SPACE;
y += Y_SPACE;
// 4、水平方向绘制文字
path.reset();
path.moveTo(x, y);
path.lineTo(x + 4 * Y_SPACE, y);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 60, mTextPaint);
如下是效果图,注意看图片中的红色部分,红色的线是用代码绘制出来的path参考线,红色的箭头是path的水平和垂直方向的走向,结合下图可以更好的理解drawTextOnPath的hOffset和vOffset参数。
这个方法的套路想必不用解释了。
drawTextRun()可以文字的是从左到右还是从右到左的顺序来绘制,其中倒数第二个参数isRtl就是用来控制方向的,true就是倒序绘制,false就是正序绘制,其他的参数就没啥好说的了,这个方法用法比较简单,这里就不贴代码了。另外这个方法是在API 23才开始添加的,使用时要注意。
到目前为止,Canvas的常用用法基本介绍完了,接下来就可以着重来看Paint的使用了,Paint和Canvas两者是不可分离的,两者协作,相辅相成。所以在下面的用法示例中不免要用到Canvas的相关方法。
我们在开发自定义控件时,免不了要精确定位文字的文字,例如必须把文字放在某个区域的正中间,或者必须让一行文字的几何中心精确的处于某个点上,这时我们如果不懂这里的窍门可能就要盲目的试位置了,这样一点一点试出来的位置很不可靠,可能换个屏幕尺寸位置就不对了,接下来怎么来看看怎么样用最优雅的姿势来精确的定位文字。
其实在水平方向的定位还比较好说,直接使用Paint.setTextAlign()就能搞定大多需求,主要是在水平方向上稍稍复杂一点,想要定位位置,首先需要先获取文字的高度,要用到Paint的以下两个方法:
有了这两个方法那就非常好办了,首先用代码结合效果图说明一下基线、ascent、descent和文字的关系:
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
canvas.drawLine(x - 300, y, x+300, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.CENTER);// 水平方向上让文字居中
float ascent = mTextPaint.ascent(); // 根据文字大小获取文字顶端到文字基线的距离(返回的是负值)
float descent = mTextPaint.descent(); // 根据文字大小获取文字底部到文字基线的距离(返回的事正值)
canvas.drawLine(x - 300, y + ascent, x+300, y + ascent, mPointPaint);
canvas.drawLine(x - 300, y + descent, x+300, y + descent, mPointPaint);
canvas.drawText(str, x, y, mTextPaint);
效果图如下,它们之间的关系注意看图片里面的说明。(注:在这里感谢园友在截图中指出的一处错误,现已修正)
接下来就让文字的中心落在参考点上:
// 将文字的中心定位在参考点上
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
canvas.drawText(str, x, y - ascent / 2 - descent / 2, mTextPaint);
效果图如下,仔细看参考点(红点)和文字的位置:
使用 setShader() 方法可以添加渐变颜色也可以使用图片作为背景,其参数是一个Shader类,传入不同的Shader子类可以实现不同的渐变效果或者添加背景图片,其子类有一下几种:
上面接个Shader的子类在使用方式上都差不多,这里只用LinearGradient为例说明一下,并注意对LinearGradient构造器的最后一个参数传入不同的参数对应的效果图:
/* Shader 渐变 */
y = 100;
Shader shader = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.CLAMP);
mTextPaint.setShader(shader);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);
y += 3 * Y_SPACE;
Shader shader1 = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.REPEAT);
mTextPaint.setShader(shader1);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);
y += 3 * Y_SPACE;
Shader shader2 = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.MIRROR);
mTextPaint.setShader(shader2);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);
效果图如下:
除了以上这些,Paint的用法还有很多很多,一时半会也列不完,博主在这也只能抛砖引玉,感兴趣的朋友可查看官方API自行探索。
最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!
参考文献: