Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

时间:2022-07-05 04:36:22

开篇

上篇介绍了 Canvas 的基本操作,绘制圆、矩形、椭圆、弧等,Canvas 除了这些操作还有两个比较重量级的绘制能力 Text(文本)Path(路径),今天就先看一下 Text 的绘制

先看一下 Paint 对于 Text 都提供了那些设置吧:

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):设置带有删除线效果

  • 其他设置

    paint.setTextScaleX(2):设置水平拉伸,高度不会变

使用 Canvas 绘制文字

  • 1、普通水平绘制

void drawText (String text, float x, float y, Paint paint)
void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
void drawText (String text, int start, int end, float x, float y, Paint paint)
void drawText (char[] text, int index, int count, float x, float y, Paint paint)

说明:
- 第一个构造函数时最简单的构造函数
- 第三、四个构造函数:实现截取一部分字体绘图
- 第二个构造函数最强大,因为传入的可以是 CharSequence 类型字体,所以可以实现绘制带图片的扩展文字,而且还能截取一部分绘制

这几个函数都比较简单,就不在具体演示了

  • 2、指定各个文字位置

void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
void drawPosText (String text, float[] pos, Paint paint)

参数:
- char[] text:要绘制的文字数组
- int index:第一个要绘制的文字的索引
- int count:要绘制的文字的个数,用来算最后一个文字的位置,从第一个绘制的文字开始算起
- float[] pos:每个字体的位置,两个为一组

onDraw 方法:

    private void init() {
        //初始化画笔
        paint = new Paint();
        paint.setTextSize(50);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPosText("巴扎黑", new float[]{100, 100, 100, 200, 100, 300}, paint);
    }

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

  • 3、沿路径绘制

    void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
    void drawTextOnPath (char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)


参数:
  • Path path:文字的绘制路径
  • char[] text:要绘制的文字数组
  • int index:第一个要绘制的文字的索引
  • int count:要绘制的文字的个数,用来算最后一个文字的位置,从第一个绘制的文字开始算起
  • float hOffset:与路径起始点的水平偏移距离
  • float vOffset:与路径中心点的垂直偏移量

onDraw 方法:

    private void init() {
        //初始化画笔
        paint = new Paint();
        paint.setTextSize(50);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        path = new Path();
        //设置路径,以圆作为我们文本显示的路线
        path.addCircle(300, 300, 200, Path.Direction.CW);  //路径的绘制方式 CW 表示正序绘制,CCW表示倒序绘制

        path1 = new Path();
        path1.addCircle(800, 300, 200, Path.Direction.CW);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制出路径原型,方便后面比较
        canvas.drawPath(path, paint);
        canvas.drawPath(path1, paint);
        //把文字绘制在要显示的路径上,默认不偏移
        canvas.drawTextOnPath("搞笑我们是认真的!!!", path, 0, 0, paint);
        //把文字绘制在要显示的路径上,路径起始点偏移150,中心垂直点偏移 50
        canvas.drawTextOnPath("搞笑我们是认真的!!!", path1, 150, 50, paint);
    }

效果图
Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

可以看到,两个文本显示的位置有点不一样,这就是我们设置的偏移量导致的,第二个文本的起始位置比第一个文本偏移了150个像素,第二个文本是在路径的外面,而第一个文本是在路径的里面,这就是我们设置的中心垂直偏移导致的

drawText 绘制技巧

我们都知道绘制文本只需要调用 Canvas 的 drawText 方法就可以在任何位置绘制我们想要的文字,但是你知道你知道 drawText 方法绘制文本时是依据什么条件进行绘制的么? 我们来看一个例子吧:

onDraw 方法:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //标准线,先绘制一条线出来,等会你就会发现一个非常不可思议的事情
       canvas.drawLine(100,100,1000,100,paint);

       canvas.drawText("gaoxiaowomenshirenzhende....”,200,100,paint); }

效果图:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

你会发现一个很有意思的事情,就是我们明明给我们要绘制的文本设置的位置是 (200,100),而我们绘制的线的位置 Y 坐标是在 100 的位置上,为什么我们绘制的文本的第一个字母 “g” 会出现在标准线的下方呢?

为什么会出现这种情况呢?下面慢慢介绍

四线格与基线

还记得我们小时候写拼音使用的四线格本子么?带你们回忆回忆童年,那时候我们都知道要写在四线格内

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

那么问题来了,其实 Canvas 在调用 drawText 方法绘制文本时,也是有规则的,这个规则就是基线

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

也就是说 Canvas 在调用 drawText 绘制文本时,是根据基线的位置来确定要绘制的 Text 的位置的,要想把 Text 绘制到正确的位置上,必须知道基线的位置

canvas.drawText() 与 基线

下面我们来重新审视一下 canvas.drawText() 这个函数

/** * text:要绘制的文字 * x:绘制原点x坐标 * y:绘制原点y坐标 * paint:用来做画的画笔 */  
public void drawText(String text, float x, float y, Paint paint)  

上面这个函数是绘制文本最常用的方法,我们之前对传递进去的(x,y)都产生了误解,以为(x,y) 就是我们所要绘制的文字的左上角的坐标,其实不然,我们传进去的(x,y)其中的 y 表示的其实是上图中基线的位置, 而 x 当然也不可能是你想象的 x 了,你以为 x 表示的就是文本开始绘制的位置么?骚年,你还是太年轻了,来看一下吧

paint.setTextAlign(Paint.Align.XXX);

我们知道这个函数是用来设置文字的对齐方式的,它的 取值有三个,左对齐(Panit.Align.LEFT)、居中对齐(Paint.Align.CENTER)和 右对齐(Paint.Align.RIGHT ),我们分别来试一下吧,看结果说话:

        @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //标准线,以位置为 (5500) 和 (5501500)绘制一条标准线,X 轴距离坐标轴原点距离为 550
        canvas.drawLine(550,0,550,1500,paint);

        //左对齐
        paint.setTextAlign(Paint.Align.LEFT);
        //基线位置
        canvas.drawLine(0,200,1500,200,paint);
        canvas.drawText("搞笑我们是认真的",550,200,paint);

        //居中对齐
        paint.setTextAlign(Paint.Align.CENTER);
        //基线位置
        canvas.drawLine(0,300,1500,300,paint);
        canvas.drawText("搞笑我们是认真的",550,300,paint);

        //右对齐
        paint.setTextAlign(Paint.Align.RIGHT);
        //基线位置
        canvas.drawText("搞笑我们是认真的",550,400,paint);
        canvas.drawLine(0,400,1500,400,paint);

    }

效果图:
Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

上面我们先绘制了一条垂直的标准线,X 坐标距离原点的距离为 550,
然后以这个标准线和设置的对齐方式开始绘制文本(搞笑我们是认真的),发现不同的对齐方式显示的位置差别很大,这是为什么呢?其实当我们传入(x,y)时,x 指代的只是一个相对距离,并不是要绘制的文本的 X 轴的坐标

  • 左对齐时:
    当对齐方式为左对齐时,x 指代的就是文本要绘制的 X 轴的坐标,(x,y)就是要绘制的文本的起始位置
  • 居中对齐时:
    当对齐方式为居中对齐时,x 指代的是一个相对距离,这个相对的是原点(0,0)的距离,要绘制的文本会以这个距离标准居中显示
  • 右对齐时:
    原理同上

drawText的四线格与 FontMetrics

前面我们提到在绘制 Text 是依据 **基线** 来进行绘制的,其实系统在绘制 Text 时,还有其他线存在的,而 基线 只是用来绘制 Text 的一个标准线 ![image.png](https://upload-images.jianshu.io/upload_images/11455341-8d2059619f878345.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 从上图可以看出,除了 基线 以外,还有另外 四 条线,分别是 top、bottom、ascent、descent、它们所表示的意思分别是:
  • ascent:系统建议的,绘制单个字符时,字符应当的最高高度所在线
  • descent:系统建议的,绘制单个字符时,字符应当的最低高度所在线
  • top:可绘制的最高高度所在线
  • bottom:可绘制的最低高度所在线

光从字面意思可能很难理解这几个值到底是什么意思,没事别着急,我们举个例子来分析下,我们来看一下电视的显示,有用过视频处理工具的同学(比如 premiere、AE、绘声绘影等)应该都会知道,在制作视频时,视频显示位置都会有一个安全区域狂,如下所示:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

黑色部分表示电视屏幕,红色框就表示安全区域框。
这个安全区域框是用来干嘛的?这个安全框就是系统推荐给我们的显示区域,虽然说我们可以将电视屏幕的每个区域都显示图像,但是由于制式不同,每个国家的屏幕大小并不一定和我们这里的屏幕大小一致,当遇到不一致时,就会裁剪。但系统给我们推荐的显示区域是无论那种制式都是可以完整显示出来的,所以我们在制作视频时,尽量要把要显示的图像放在系统推荐的显示区域内。

同样,我们在绘制文字时, ascent 是推荐的绘制文字的最高高度,就表示在绘制文字时,尽力要在这个最高高度以下绘制文字,descent 是推荐的绘制文字的最低高度线,同样表示是在绘制文字时尽量在这个 descent 线以上来绘制文字。而 top 线则表示该文字可以绘制的最高高度线,bottom 则表示该文字可以绘制的最低高度线。ascent 和 descent 是系统建议的绘制高度,而 top 和 bottom 则是物理上屏幕高度,他们的差别与我们上面说的视频处理的安全框和屏幕是一个道理

FontMetrics

上面已经对top、bottom、ascent、descent、baseline 这五条线进行了介绍,那么这五条线之间是什么关系?以及这五条线的位置是如何计算出来呢?

Android 为我们提供了一个类:FontMetrics ,它里面有四个成员变量:

  • FontMetrics.ascent
  • FontMetrics.descent
  • FontMetrics.top
  • FontMetrics.bottom

它们之间的关系如下:

  • ascent = ascent 线的 Y 坐标 - baseline 线的 Y 坐标
  • descent = descent 线的 Y 坐标 - baseline 线的 Y 坐标
  • top = top 线的 Y 坐标 - baseline 线的 Y 坐标
  • bottom = bottom 线的 Y 坐标 - baseline 线的 Y 坐标

我们来看图分析下,看是不是这么个情况:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

从这个图中,我们先说明两个问题,然后在讨论上面的公式:

  • X 轴 和 Y 轴 都是有正反方向的,X 轴是向右是正方向,Y 轴是向下是正方向,所以越往下表示 Y 轴坐标越大
  • 千万不要讲 FontMetrics 中的 ascent、descent、top、bottom 与现实中的 ascent、descent、top、bottom 所在线弄混淆了,这几条线是真实存在的,而 FontMetrics 中的 ascent、descent、top、bottom 这些变量的值就是为了计算着几条线的位置的,下面我们就利用这几个变量来计算出这几条线应该处于的位置,并绘制出来:

其实上面的几个公式得到的 ascent、descent、top、bottom 就是 baseline 到各个线的位置,不过对于 top 和 ascent 来说,baseline 线位于这两条线的下方,所以 baseline 线的 Y 坐标肯定大于 top 和 ascent 线的 Y 坐标,所以 ascent 和 top 的值肯定是负的,而 bottom 和 descent 这两条线都处于 baseline 线下方,所以都是正的,下面我们通过上面的公式分别来求出各个线的 Y 坐标,然后通过 Y 坐标把各条线绘制出来

  • ascent Y 坐标 = baseline Y 坐标 + FontMetrics.ascent
  • descent Y 坐标 = baseline Y 坐标 + FontMetrics.descent
  • top Y 坐标 = baseline Y 坐标 + FontMetrics.top
  • bottom Y 坐标 = baseline Y 坐标 + FontMetrics.bottom

FontMetrics 对象的获取

        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float ascent = fontMetrics.ascent;
        float descent = fontMetrics.descent;
        float top = fontMetrics.top;
        float bottom = fontMetrics.bottom;

从这里可以看到,通过 paint.getFontMetrics() 得到对应的 FontMetrics 对象。这里还有另外一个 FontMetrics 同样的类叫做 FontMetricsInt 它的意义与 FontMetrics 完全相同,只是得到的类型不一样而已。FontMetericInt 中的四个成员变量的值都是 Int 类型,而 FontMetrics 得到的四个成员变量的值都是 float 类型的。

绘制出各个线的位置

效果图如下:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

onDraw 方法:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int baselineX = 10; //基线的 X 轴
        int baselineY = 300; //基线的 Y 轴

        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        //获取各个线距离 baseline 线的距离
        float ascent = fontMetrics.ascent;
        float descent = fontMetrics.descent;
        float top = fontMetrics.top;
        float bottom = fontMetrics.bottom;

        //绘制文本
        canvas.drawText("搞笑我们是认真的!!!", baselineX, baselineY, paint);

        paint.setStrokeWidth(3);

        //绘制基线
        paint.setColor(Color.RED);
        canvas.drawLine(baselineX, baselineY, 1000, baselineY, paint);

        //绘制 ascent 线
        paint.setColor(Color.BLUE);
        canvas.drawLine(baselineX, baselineY + ascent, 1000, baselineY + ascent, paint);

        //绘制 descent 线
        paint.setColor(Color.BLACK);
        canvas.drawLine(baselineX, baselineY + descent, 1000, baselineY + descent, paint);

        //绘制 top 线
        paint.setColor(Color.GREEN);
        canvas.drawLine(baselineX, baselineY + top, 1000, baselineY + top, paint);

        //绘制 bottom 线
        paint.setColor(Color.YELLOW);
        canvas.drawLine(baselineX, baselineY + bottom, 1000, baselineY + bottom, paint);
    }

上面代码中已经注释的很清楚了,就不在单独拿出来进行介绍了

获取所绘制文字的宽度、高度和最小矩形

在这里,我们将搞定如何获取或绘制字符串所占区域的高度、宽度以及仅仅包裹字符串的最小矩形。我们先来看下示例图:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

从这张图中,文字底部的绿色框就是所绘制字符串占据的大小,我们要求的宽度和高度也就是这个绿色框的宽度和高度。

从图中也可以看到,红色框部分,它的宽和高紧紧包裹着字符串,所以红色框就是我们要求的最小矩形,既能包裹字符串的最小矩形

字符串所占高度和宽度

  • 1、高度
    字符串所占高度很容易得到,直接用 FontMetrics.bottom - FontMetrics.top(因为 FontMetrics.top 是负值) 就是字符串所占的高度:

  • 2、宽度是非常容易得到的,直接利用下面的函数就可以得到:

float width = paint.measureText(String text);

使用示例如下:

        paint = new Paint();
        paint.setTextSize(100);     //单位为 sp
        float width = paint.measureText("搞笑我们是认真的”);
  • 最小矩形
    要获取最小矩形,也是通过系统函数来获取的,函数定义如下:
/** * 获取指定字符串所对应的最小矩形,以(0,0)点所在位置为基线 * @param text 要测量最小矩形的字符串 * @param start 要测量起始字符在字符串中的索引 * @param end 所要测量的字符的长度 * @param bounds 接收测量结果 */  
public void getTextBounds(String text, int start, int end, Rect bounds);  

代码示例:

        paint.setTextSize(100);     //单位为 sp
        String text = "搞笑我们是认真的”;
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);

我们看一下输出结果:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

可以看到这个矩形的left、top、right、bottom 分别为 3、-82、792、11 ,可能大家会疑惑为什么 top 会为负数呢?其实从代码中我们也可以得出结论,我们并没有给 getTextBounds() 方法传递基线位置,那它就是以(0,0)为基线来得到这个最小矩形的!所以这个最小矩形的位置就是以(0,0)为基线的结果。

既然已经拿到了最小矩形的left、top、right、bottom,并且知道这个最小矩形是以(0,0)为基线绘制的,那么我们想要把这个矩形绘制在 Text 显示的位置,那么也很简单,只需要加上 baseline 的距离就OK了 我们来试一下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int baselineX = 100;      //基线的 X 轴
        int baselineY = 300;    //基线的 Y 轴

        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        //获取各个线距离 baseline 线的距离
        float ascent = fontMetrics.ascent;
        float descent = fontMetrics.descent;
        float top = fontMetrics.top;
        float bottom = fontMetrics.bottom;

        float height = bottom - top;

        String text = "搞笑我们是认真的”; Rect rect = new Rect(); paint.getTextBounds(text, 0, text.length(), rect); paint.setColor(Color.YELLOW); paint.setStyle(Paint.Style.FILL); canvas.drawRect(rect.left +baselineX, baselineY + rect.top, rect.right+baselineX, rect.bottom + baselineY, paint); //绘制文本 paint.setColor(Color.RED); canvas.drawText(text, baselineX, baselineY, paint); }

效果图:

Android高级进阶——绘图篇(二)Canvas绘制文本 drawText详解

完整代码如下:

    private void init() {
        //初始化画笔
        paint = new Paint();
        paint.setTextSize(100);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setTextSize(100); //单位为 sp

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int baselineX = 100; //基线的 X 轴
        int baselineY = 300; //基线的 Y 轴

        String text = "搞笑我们是认真的";

        //获取当前线到baseline线的距离
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float top = fontMetrics.top; //为负值
        float bottom = fontMetrics.bottom;
        //获取字符串所占高度
        float height = fontMetrics.bottom - fontMetrics.top;

        //获取字符串所占宽度
        float width = paint.measureText(text);

        //绘制字符串所占区域
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(baselineX, baselineY + top, width + baselineX, bottom + baselineY, paint);

        //获取最小矩形  默认是以(00)为基线获取,所以要想把最小矩形绘制到正确位置,需要 + baseline Y 
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);

        paint.setColor(Color.YELLOW);
        paint.setStyle(Paint.Style.FILL);
        int left = rect.left + baselineX;
        top = baselineY + rect.top;
        int right = rect.right + baselineX;
        bottom = rect.bottom + baselineY;
        //绘制最小矩形
        canvas.drawRect(left, top, right, bottom, paint);

        //绘制文本
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawText(text, baselineX, baselineY, paint);
    }

完事,终于搞完了,后面开始 Path 绘制,贝塞尔曲线我来了……..