一、绘图入门
Canvas 翻译为“画布”,可以理解成画家作画时的宣纸。Canvas 供了若干方法用于绘制各种图形图案——点、线、圆等等。Paint 翻译为“画笔”,为绘图定义各种参数——颜色、线条样式、图案样式等等。通常的绘图思路是先定义 Paint 对象,指定绘图参数,再通过 Canvas 对象进行图 形绘制,绘图的结果因 Paint 的不同而不同。Paint 类用于定义绘图时的参数,主要包含颜色、文本、图形样式、位图模式、滤镜等几个方面。 颜色是指绘图时使用的颜色,在 Android 中颜色可以指定透明度,使用 16 进制来表示颜色时,格式通常为#AARRGGBB,其中,AA 表示透明度、RR 表示红色、GG 表示绿色、BB 表示蓝色,Color 类定义了颜色信息,内置了常用颜色的 int 型常量,比如 Color.RED 是红色,Color.BLUE 是蓝色……如果您习惯了 16 进制的颜色,Color 类的静态方法 parseColor(String colorString)可以将 16进制颜色转换成 Color 类型。 先来看看简单的使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv1);
Bitmap bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.LEFT);
paint.setTextSize(32f);
paint.setTextSkewX(0.5f);
paint.setUnderlineText(true);
paint.setFakeBoldText(true);
canvas.drawText("hello 你好啊!", 10, 100, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
paint.setStrokeWidth(20);
paint.setStrokeJoin(Paint.Join.BEVEL);
canvas.drawRect(new Rect(20, 200, 350, 350), paint);
iv.setImageBitmap(bitmap);
}
效果图:
二、绘制位图
常用方法
public void drawBitmap( Bitmap bitmap, float left, float top, Paint paint)
public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
ps:绘制位图时,除非需要进行位图运算,否则,并不需要指定 paint 对象,直接传递null 即可。
案例-使用drawBitmap方法在ImageView上绘制2个位图,一个按原始大小绘制,另一个则按2倍大小绘制
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 800, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Bitmap sourceBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
canvas.drawBitmap(sourceBitmap, 0, 0, null);
int bmpWidth = sourceBitmap.getWidth();
int bmpHeight = sourceBitmap.getHeight();
Rect src = new Rect(0, 0, bmpWidth, bmpHeight);
Rect dest = new Rect(0, bmpHeight, bmpWidth * 2, bmpHeight + bmpHeight * 2);
canvas.drawBitmap(sourceBitmap, src, dest, null);
iv.setImageBitmap(bitmap);
}
效果图如下:
三、绘制点
点的大小取决于 setStrokeWidth()方法的参数,参数值越大,点也就越大。所以,不要以为一个点就是屏幕上的一个像素。如果将 stroke 的宽度设置为足够大,我们发现最终绘制出来的点其实是一个正方形。绘制点的方法一共有三个:
public void drawPoint(float x, float y, Paint paint)
public void drawPoints(float[] pts, Paint paint)
public void drawPoints(float[] pts, int offset, int count, Paint paint)
示例如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(10);
canvas.drawPoint(120, 20, paint);
paint.setColor(Color.BLUE);
float[] points = new float[]{10, 10, 50, 50, 50, 100, 50, 150};
canvas.drawPoints(points, paint);
paint.setColor(Color.GREEN);
points = new float[]{20, 20, 60, 60, 60, 80, 60, 180};
canvas.drawPoints(points, 3, 4, paint);
iv.setImageBitmap(bitmap);
}
效果图:
四、绘制直线
两个点确定一条直线,所以,绘制线条时,需要指定两个点的坐标。同画点一样,绘制线条也有 3 个重载的方法:
public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
public void drawLines(float[] pts, Paint paint)
public void drawLines(float[] pts, int offset,int count, Paint paint)
五、绘制矩形
绘制矩形时,参数分为两种:一种是指定 left、top、right、bottom 等 4 个参数,另一种直接指定一个 Rect 对象或 RectF 对象。绘制直角矩形的三个重载方法如下:
public void drawRect(float left,float top, float right,float bottom,Paint paint)
public void drawRect(Rect r,Paint paint)
public void drawRect(RectF r,Paint paint)
圆角矩形的几何形状比直角矩形相对复杂一些,我们需要指定 4 个拐角的弧度,4 个角的弧度不能单独设置,而是统一设置为相同的值。拐角弧度实际上是圆或椭圆的一段弧线,如图所示: 绘制圆角矩形一共有 2 个重载的方法:
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,Paint paint)
public void drawRoundRect(RectF rect,float rx,float ry,Paint paint)
下面的代码绘制了三个矩形,一个直角矩形,一个为空心圆角矩形,另一个是有填充颜色的实心圆角矩形:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main5);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setColor(Color.RED);
canvas.drawRect(new Rect(0, 0, 100, 100), paint);
paint.setColor(Color.GREEN);
canvas.drawRoundRect(new RectF(100, 100, 200, 200), 20, 20, paint);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
canvas.drawRoundRect(new RectF(200, 200, 400, 400), 10, 20, paint);
iv.setImageBitmap(bitmap);
}
效果图:
六、绘制圆
在对图形进行分类时,我将圆、椭圆、扇形、弧线统一归类到“圆”这一类中,扇形和弧线可以认为是圆或椭圆的一部分,椭圆的大小是由他的外切矩形来决定的,这实际上和几何学中的定义完全一致,如图所示: 绘制椭圆的方法如下:
public void drawOval(float left, float top, float right, float bottom, Paint paint)
public void drawOval(RectF oval, Paint paint)
绘制椭圆时,如果外切矩形的长和宽相等,即为正方形,绘制出来的图形就是一个正圆,但 是 Cavnas 类供了另一个更加简单实用的方法,供圆点的坐标和半径即可。
public void drawCircle(float cx, float cy, float radius, Paint paint)
弧线和扇形本质上更是相似,弧线是椭圆上的一段,而扇形则是将弧线的两个端点和椭圆中心点使用线条连接形成的闭合区域。理解弧线和扇形的演变过程便很容易明白方法中的参数意义,如下图所示: 绘制弧线和扇形的方法如下:
public void drawArc(RectF oval,float startAngle,float sweepAngle,boolean useCenter,Paint paint)
public void drawArc(float left, float top, float right, float bottom,
float startAngle, float sweepAngle,boolean useCenter,Paint paint)
下面的代码演示了弧线和扇形的绘制方法,采用了 Style.STROKE 的图形模式,如果将 Style设置为 Style.FILL,不管是弧线还是扇形,都可以使用颜色进行填充
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main6);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStyle(Paint.Style.STROKE);
RectF oval = new RectF(100, 100, 400, 200);
paint.setColor(Color.GRAY);
canvas.drawOval(oval, paint);
paint.setColor(Color.RED);
canvas.drawArc(oval,90,45,false,paint);
paint.setColor(Color.GREEN);
canvas.drawArc(oval,0,-45,true,paint);
iv.setImageBitmap(bitmap);
}
效果图:
七、绘制路径
Path 是 Graphics2D 中一个非常重要的概念,表示“路径”,理解该概念时保持“路径”的本色就好。路径可以是直的、也可以是弯的,可以是闭合的、也可以是非闭合的,可以是圆形的、也可以是方形的,可以是单个的、也可以是多个的,可以是简单的、也可以是复杂的……总的来说,路径是基于普通图形但是功能比普通图形更强的一种复杂图形。
Path 是一个类,用于绘制复杂图形,创建之初什么也没有,只有往 Path 中添加了具体的形状,Path 才会清晰可见。绘制 Path 时,所有信息都存储在 Path 对象中,Canvas 根据 Path 对象来绘制相应的图形。
我们将 Path 的功能归纳成以下几类:
7.1 往 Path 中添加线条
通过 Path 可以绘制出奇形怪状的线条,并能将线条组合在一起变成折线,闭合后就是一个多边形了。这就是 Path 的厉害之处。为此,Path 类中定义了 5 个方法
public void moveTo(float x,float y)
public void rMoveTo(float dx,float dy)
public void lineTo(float x,float y)
public void rLineTo(float dx,float dy)
public void close()
下面的案例使用 Path 绘制了一个五角星,这不是一个完美的五角星几何图形,因为五个点的坐标并没有正确计算出来,只是算了个大概。首先调用了 moveTo(0, 150)定义好这次绘图的起 点,接下来调用 rLineTo()方法通过相对定位计算出下一个点的坐标,并使用直线连接,最后,调用 close()方法连接最后一点和第一个点以形成闭合区域。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main7);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
Path path = new Path();
path.moveTo(0, 150);
path.rLineTo(300, 0);
path.rLineTo(-300, 150);
path.rLineTo(150, -300);
path.rLineTo(150, 300);
path.close();
canvas.drawPath(path, paint);
iv.setImageBitmap(bitmap);
}
效果图:
7.2 往 Path 中添加矩形、椭圆、弧
如果要往 Path 对象中添加矩形、椭圆、圆和弧,需要调用 Path 类中定义的一组以“add”开 头的方法,这组方法有些需要传递一个类型为 Path.Direction 的参数,这是一个枚举类型,枚举值 CW 表示顺时针,CCW 表示逆时针,下一节内容沿着图形绘制文字时,我们可以清晰地感受到方向对绘图带来的影响。
public void addRect(RectF rect,Path.Direction dir)
public void addRect(float left,float top,float right,float bottom, Path.Direction dir)
public void addRoundRect(RectF rect,float[] radii, Path.Direction dir)
public void addRoundRect(RectF rect,float rx,float ry,Path.Direction dir)
public void addRoundRect(float left, float top, float right, float bottom, float[] radii,Path.Direction dir)
public void addOval(RectF oval, Path.Direction dir)
public void addOval(float left, float top, float right, float bottom, Path.Direction dir)
public void addCircle(float x, float y, float radius, Path.Direction dir)
public void addArc(RectF oval, float startAngle, float sweepAngle)
public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)
我们在下面的代码中绘制了一个 Path 对象,对象中同时包含了矩形、圆角矩形、椭圆、圆、弧线等图形,显然,Path 对象绘制出来的图形更加复杂了。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main8);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 620, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setColor(Color.RED);
Path path = new Path();
path.addRect(new RectF(10, 10, 300, 100), Path.Direction.CCW);
path.addRoundRect(new RectF(10, 120, 300, 220),
new float[]{10, 20, 20, 10, 30, 40, 40, 30},
Path.Direction.CCW);
path.addOval(new RectF(10, 240, 300, 340), Path.Direction.CCW);
path.addCircle(60, 390, 50, Path.Direction.CCW);
path.addArc(new RectF(10, 500, 300, 600), -30, -60);
canvas.drawPath(path,paint);
iv.setImageBitmap(bitmap);
}
效果图:
7.3 往 Path 中添加曲线和贝塞尔曲线
曲线包括弧线和贝塞尔曲线,与前面讲的矩形、圆或弧线不同,绘制曲线时需要确定一个起点,绘制的曲线会与该起点进行连接,形成一个更加复杂的图形。 贝塞尔曲线(Bézier curve)是图形开发中的一个重要工具,通过三个点的,确定一条平滑的曲线,又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具。贝塞尔曲线又分为一阶贝塞尔曲线、二阶贝塞尔曲线、三阶贝塞尔曲线和高阶贝塞尔曲线,一阶贝塞尔曲线就是一条线段,Path 类支持二阶贝塞尔曲线和三阶贝塞尔曲线。如下图所示分别是: 一阶、二阶、三阶 前面提到,贝塞尔曲线通过 3 个点来绘制一条平滑的曲线,这 3 个点分别是起点、控制点和终点。比如,如果要绘制一条二阶贝塞尔曲线,必须调用 moveTo()方法定义起点,再调用如下方法绘制贝塞尔曲线
public void quadTo(float x1,float y1,float x2,float y2)
我们通过一段代码演示如何绘制贝塞尔曲线
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main9);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 400, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setColor(Color.RED);
Path path = new Path();
path.moveTo(100, 100);
path.quadTo(200, 50, 300, 300);
canvas.drawPath(path, paint);
paint.setColor(Color.GREEN);
canvas.drawPoints(new float[]{100, 100, 200, 50, 300, 300}, paint);
paint.setStrokeWidth(2);
paint.setTextSize(22);
canvas.drawText("起点",90,140,paint);
canvas.drawText("控制点",220,55,paint);
canvas.drawText("终点",280,340,paint);
iv.setImageBitmap(bitmap);
}
效果图: 三阶贝塞尔曲线有 1 个起点,2 个控制点,1 个终点,Path 类中通过下面方法进行绘制
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
quadTo()和 cubicTo()的控制点和终点利用绝对定位来进行确定,其实还有另外两个方法,通过相对定位对各点进行定义:
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
添加曲线可以使用arcTo()方法,arcTo()方法可以和 moveTo()配合使用,通过 moveTo()确定一个起点,再通过 arcTo()绘制弧线。弧线是基于矩形的内切圆上的一段,该弧线的起始点会和 moveTo()方法定义的点进行连接。
public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
public void arcTo(RectF oval, float startAngle, float sweepAngle)
public void arcTo(float left, float top, float right, float bottom,
float startAngle, float sweepAngle, boolean forceMoveTo)
上面三个方法的参数在前面都有说明,不再赘述,参数 forceMoveTo 为 true 时,表示开始一个新的图形,不和上一个点进行连接,为 false 时才和上一个点连接 我们通过一小段代码来演示 artTo()方法的使用技巧:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main10);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
RectF oval1 = new RectF(150, 100, 350, 250);
RectF oval2 = new RectF(150, 250, 350, 400);
paint.setColor(Color.GRAY);
canvas.drawOval(oval1, paint);
canvas.drawOval(oval2, paint);
Path path = new Path();
paint.setColor(Color.GREEN);
path.moveTo(100, 100);
path.arcTo(oval1, -30, 60, true);
path.arcTo(oval2, 90, -45);
canvas.drawPath(path, paint);
iv.setImageBitmap(bitmap);
}
效果图: 从图中可以看出下面的弧线和上一个点是连接起来的,它的上一个点刚好就是上一个圆弧的终点
7.4 将 Path 中的图形进行运算
我们还可以将多个 Path 进行图形运算,得到更加复杂和不规则的图形。Path 有一个静态内部类 Op,定义了 5 种运算规则:
Path.Op. DIFFERENCE:差集,图形 A 减去与图形 B 重叠的区域后 A 余下的区域。 Path.Op. INTERSECT:交集,图形 A 和图形 B 的重叠区域。 Path.Op. REVERSE_DIFFERENCE:反差集,图形 B 减去与图形 A 重叠的区域后 B 余下的区域。 Path.Op. UNION:并集,包含了图形 A 和图形 B 的所有区域。 Path.Op.XOR:补集,即图形 A 和图形 B 的所有区域减去他们的重叠区域后余下的区域。 我们通过以下的表格来比较这 5 种图形运算的不同效果: 图形A代表黑色正方形,图形B代表红色圆形 下面通过代码演示,先绘制原图查看效果:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main11);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
Path path1 = new Path();
path1.addRect(new RectF(10, 10, 110, 110), Path.Direction.CCW);
canvas.drawPath(path1, paint);
paint.setColor(Color.RED);
Path path2 = new Path();
path2.addCircle(100, 100, 50, Path.Direction.CCW);
canvas.drawPath(path2, paint);
iv.setImageBitmap(bitmap);
}
效果图如下: 然后对path进行5种效果运算演示
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main11);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(800, 500, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
Path path1 = new Path();
path1.addRect(new RectF(10, 10, 110, 110), Path.Direction.CCW);
canvas.drawPath(path1, paint);
paint.setColor(Color.RED);
Path path2 = new Path();
path2.addCircle(100, 100, 50, Path.Direction.CCW);
canvas.drawPath(path2, paint);
paint.setColor(Color.WHITE);
path1.reset();
path2.reset();
path1.addRect(new RectF(10, 170, 110, 270), Path.Direction.CCW);
path2.addCircle(100, 260, 50, Path.Direction.CCW);
path1.op(path2, Path.Op.DIFFERENCE);
canvas.drawPath(path1,paint);
paint.setColor(Color.WHITE);
path1.reset();
path2.reset();
path1.addRect(new RectF(160, 170, 260, 270), Path.Direction.CCW);
path2.addCircle(250, 260, 50, Path.Direction.CCW);
path1.op(path2, Path.Op.INTERSECT);
canvas.drawPath(path1,paint);
paint.setColor(Color.WHITE);
path1.reset();
path2.reset();
path1.addRect(new RectF(260, 170, 360, 270), Path.Direction.CCW);
path2.addCircle(350, 260, 50, Path.Direction.CCW);
path1.op(path2, Path.Op.REVERSE_DIFFERENCE);
canvas.drawPath(path1,paint);
paint.setColor(Color.WHITE);
path1.reset();
path2.reset();
path1.addRect(new RectF(450, 170, 550, 270), Path.Direction.CCW);
path2.addCircle(540, 260, 50, Path.Direction.CCW);
path1.op(path2, Path.Op.UNION);
canvas.drawPath(path1,paint);
paint.setColor(Color.WHITE);
path1.reset();
path2.reset();
path1.addRect(new RectF(650, 170, 750, 270), Path.Direction.CCW);
path2.addCircle(740, 260, 50, Path.Direction.CCW);
path1.op(path2, Path.Op.XOR);
canvas.drawPath(path1,paint);
iv.setImageBitmap(bitmap);
}
效果图:
7.5 绘制文字
Canvas为我们供了两组方法,一组直接从指定的位置开始绘制文字,另一组沿着 Path 绘制文字:
public void drawText(char[] text, int index, int count, float x, float y, Paint paint)
public void drawText(String text, float x, float y, Paint paint)
public void drawText(String text, int start, int end, float x, float y, Paint paint)
public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
上面这一组方法是从指定的位置(坐标)开始绘制文字,虽然都是字符串,但是供了三种形式:char[]、String 和 CharSequence,本质上并没有什么不同,参数 index 和count、start 和 end 可以从字符串中取出子串,而参数 x、y 就是文字绘制的坐标位置,其中 y 是文字的 baseline 的值
public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
public void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)
上面这两个重载的 drawTextOnPath()方法用于沿着 Path 定义好的路径绘制文字,这是一个很在趣的功能,文字在 Path 的带领下龙飞凤舞,灵活多变。参数 hOffset 和 vOffset 用于定义文字离 Path 的水平偏移量和垂直偏移量,正数和负数影响文字与路径的相对位 置。同样的,也支持绘制从字符数组中截取的子串,index 表示起始索引,count 表示要截取的长度。
下面的案例中绘制了 4 个字符串,一个绘制所有的字符串,中间两个截取子串进行绘制,最后一个沿着 Path 绘制出所有的文字,为了更好的理解文字与路径的关系,所以把对应的路径也绘制出来了
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main12);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(800, 450, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setTextSize(30);
String text = "天王盖地虎,宝塔镇河妖;野鸡闷头钻,哪能上天王山";
canvas.drawText(text, 10, 50, paint);
paint.setColor(Color.RED);
canvas.drawText(text, 6, 11, 10, 100, paint);
paint.setColor(Color.BLUE);
canvas.drawText(text.toCharArray(), 12, 5, 10, 150, paint);
Path path = new Path();
path.moveTo(10, 300);
path.quadTo(100, 100, 700, 400);
paint.setColor(Color.GREEN);
canvas.drawTextOnPath(text, path, 10, 30, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.RED);
canvas.drawPath(path, paint);
iv.setImageBitmap(bitmap);
}
效果图:
7.5.1 在指定位置绘制文本
下面2个方法都是标记了过时的方法
public void drawPosText(@NonNull String text, @NonNull @Size(multiple = 2) float[] pos,@NonNull Paint paint);
public void drawPosText(@NonNull char[] text, int index, int count,@NonNull @Size(multiple = 2) float[] pos,@NonNull Paint paint);
通过这2个方法可以按自定的坐标集绘制字符串或者字符数组,例如要实现一个垂直方向的文本绘制可以这样实现
class MyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, def: Int = 0) : View(context, attrs, def) {
private val paint = Paint().apply {
isAntiAlias = true
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 22f, resources.displayMetrics)
color = Color.RED
}
private val text = "月黑风高"
private val array = FloatArray(text.length * 2).apply {
for (i in 0 until size step 2) {
set(i, 100f)
set(i + 1, (i + 1) * paint.textSize)
}
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawColor(Color.BLACK)
canvas?.drawPosText(text, array, paint)
}
}
效果图:
八、Paint的FontMetrics使用
粉红色就是TextView的背景色, 可以看到在Ascent和Descent之外分别还有一点距离才到TextView的边缘, 也就是右侧使用橙色方块标出的fontPadding
FontMetrics提供了如下属性: top: 即上边界, 因为在Android中, y轴正方向是向下的, 而基准线(base line)是y=0, 所以这个值是一个负数.它的值等于它到base line距离的负数 ascent: 字体文件中设置的Ascent值也是负数, 理由同上,它的值是ascent到base line的距离的负数 descent: 字体文件中设置的Descent值正数,因为在基准线下面,它的值等于它到base line的距离 bottom: 下边界, 正数,理由同上,它的值等于它到base line的距离 leading: 两行之间, 上一行的bottom和下一行的top的间距, 然而这个值总是0, 可以忽略,用下图来描述leading
8.1 行距
行距就是相邻两行的基线之间的距离.默认行距的实际值等于字体设置中的|Descent| + |Aescent|,在Android的TextView中, 可以通过android:lineSpacingExtra和android:lineSpacingMultiplier修改行距. 其中lineSpacingExtra默认值为0, lineSpacingMultiplier默认值为1, 有以下公式
行距=默认行距 * lineSpacingMultiplier + lineSpacingExtra
8.2 计算fontPadding
顶部的fontPadding= |top - ascent |,底部的font padding= bottom - descent,通过android:includeFontPadding可以决定字体的高度是否包含fontPadding
Android中的字体高度是|bottom| + |top|, 而普通软件(例如word, Sketch或者其他设计软件)中, 字体高度使用的是|descent| + |ascent|, 所以Android中的字体在垂直方向上总是比设计稿的多占一点空间. 对于普通的字体, 要完美复刻设计稿的字体高度, 应该把android:includeFontPadding设置为false,默认是true
8.3文本在控件水平方向居中显示
原理很简单,只需要计算绘制文字的x坐标 = 控件宽度的一半 - 文字宽度的一半即可, 代码如下:
private val mPaint: Paint = Paint().apply {
isAntiAlias = true
color = Color.RED
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val txt = "hello go yes!!!"
val x = measuredWidth / 2f
val y = measuredHeight / 2f
canvas?.drawLine(0f, y, measuredWidth.toFloat(), y, mPaint)
canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)
val rect = Rect()
mPaint.getTextBounds(txt, 0, txt.length, rect)
val finalX = x - rect.width() / 2f
canvas?.drawText(txt, finalX, y, mPaint)
}
效果图: 由上图可见文本在水平方向是居中了的,但是垂直方向并没有居中(看感叹号的位置就能对比出来了),这是因为我们将绘制文本的y坐标(baseline)定为了控件高度的一半,这是错误的,我们需要将文本处于控件居中位置, 而不应该是y坐标(baseline), 这就需要向下移动一个baseline的距离,这个距离的计算可以看下面介绍.
8.4 文本在控件垂直方向居中显示
- 计算文字的高度一半到baseLine的距离
Baseline在文本的垂直方向很重要,只有先确定了Baseline的位置,换句话说就是y坐标的值,我们才能准确的将文字绘制在我们想要的位置上。Baseline的概念在我们使用TextView等系统控件直接设置文字内容时是用不到的,但是如果我们想要在Canvas画布上面绘制文字时,Baseline的概念就必不可少了。以4个参数的drawText方法为例:
public void drawText(String text, float x, float y, Paint paint)
计算原理:
int distanceY = (int) (((fontMetrics.bottom - fontMetrics.top) / 2) - fontMetrics.bottom);
注意: 文本的正中的"值"并不是控件的getHeight()/2,也就是说getHeight() != (fontMetrics.bottom - fontMetrics.top)因为fontMetrics的值是相对于baseline的.而控件是相对左上角top来计算的.所以如果要在自定义View的正中央绘制文本,那么可以这么做
private val mPaint: Paint = Paint().apply {
isAntiAlias = true
color = Color.RED
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val txt = "hello go yes!!!"
val x = measuredWidth / 2f
val y = measuredHeight / 2f
canvas?.drawLine(0f, y, measuredWidth.toFloat(), y, mPaint)
canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)
val rect = Rect()
mPaint.getTextBounds(txt, 0, txt.length, rect)
val finalX = x - rect.width() / 2f
val metrics = mPaint.fontMetrics
val distance = (metrics.bottom - metrics.top) / 2f - metrics.bottom
val finalY = y + distance
canvas?.drawText(txt, finalX, finalY, mPaint)
}
效果图: 注意看感叹号的位置是居中的, 换个中文就很明显了
8.5 Paint的setTextAlign使用
该方法用于控制文本在水平方向的对齐方式, 可以理解成该字符串与起点的相对位置, 常用有3种: 1)Align.LEFT: 居左绘制,即通过drawText函数指定的起点在最左侧,文字从起点位置开始绘制 2)Align.CENTER:居中绘制,即通过drawText函数指定的起点在文字中间位置 3)Align.Right: 居右绘制,即通过drawText函数指定的起点在文字右侧位置. 如下图所示: 因此如果需要将文本绘制在控件的水平居中的位置,可以这么做
private val mPaint: Paint = Paint().apply {
isAntiAlias = true
color = Color.RED
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val txt = "我是胜哥"
val x = measuredWidth / 2f
canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)
canvas?.drawText(txt, x, 100f, mPaint)
}
效果如下:
九、绘制验证码
在 ImageView 绘制一个空心矩形,随机产生 100 条干扰线,并随机生成 4 个数,字显示在矩形框内。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
Bitmap bitmap = Bitmap.createBitmap(500, 300, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(new Rect(100, 100, 370, 200), paint);
char[] text = new char[]{'我', '你', '他', '它', '东', '西', '南', '北', '中'};
Random random = new Random();
char[] randomText = new char[4];
int index = 0;
while (index < 4 && randomText[index] == 0) {
char getChar = text[random.nextInt(text.length)];
boolean contains = false;
for (int i = 0; i < randomText.length; i++) {
if (getChar == randomText[i]) {
contains = true;
break;
}
}
if (!contains) {
randomText[index] = getChar;
index++;
}
}
paint.setColor(Color.BLACK);
paint.setTextSize(30);
paint.setFakeBoldText(true);
paint.setLetterSpacing(1);
canvas.drawText(randomText, 0, 4, 110, 160, paint);
paint.setStyle(Paint.Style.STROKE);
for (int i = 0; i < 100; i++) {
int color = Color.argb(150,
55 + random.nextInt(200),
55 + random.nextInt(200),
55 + random.nextInt(200));
paint.setColor(color);
Path path = new Path();
int controlX = 100 + random.nextInt(100);
int controlY = 100 + random.nextInt(100);
path.moveTo(100, 100 + random.nextInt(100));
int endX = 370;
int endY = 100 + random.nextInt(100);
path.quadTo(controlX, controlY, endX, endY);
canvas.drawPath(path, paint);
}
iv.setImageBitmap(bitmap);
}
效果图:
|