博客笔记:自定义View之绘图(1)--drawText

声明:
发现了一篇自定义View的博客索引,有比较系统的自定义View技术点。欣喜之下准备学习一下,把这些技术点学会,消化,变成自己的东西。做些笔记,或有copy,原文更好。看原文,请转到:启舰的博客—Android自定义控件三部曲文章索引

一、概述

1. 四线格与基线

在阅读启舰的博客: 自定义控件之绘图篇( 五):drawText()详解后,终于知道,原来canvas在绘制文字时,是有规则的,这个规则就是–基线。这也正是我之前疑惑之一, 为什么总画不好文字,感觉代码没错呀,文字位置为什么不对。看了此博客,终于解惑,感谢启舰的博客^_^。

记得初学字母时,要用到四线格作业本, 我们将字母按格式写到四线格内,见下图:

四线图

canvas在使用drawText绘制文字时是有标准的,它是以基线为基准的。

基线

由图可以看出,基线就相当于四线格的第三条线。基线位置确定,文字位置也就确定了。

2. canvas.drawText()
- 1> canvas.drawText()和基线

下面是canvas的drawText()方法:

/**
 * 使用指定的画笔从起点(x,y)开始绘制文字。 根据画笔对齐设置确定起点位置。 
 * @param text  要绘制的文字
 * @param x     文字绘制起点x坐标
 * @param y     文字绘制起点y坐标
 * @param paint 用于绘制文字的画笔(e.g. color, size, style)
 */
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint){
    ...
}

传入一个字符串text,设置绘制原点的x、y坐标和一个画笔paint。就可以绘制文字了。
但是这个原点坐标(x, y)的位置到底在哪里呢?之前以为是在左上角,但实际不是。其实原点(x, y)跟基线和对齐设置(paint.setTextAlign(align))有关。对齐设置后面再提。如下图,绘制文字“changes”时,起点就在基线上的绿点位置。

基线和起点

坐标(x, y)难搞的是y坐标,绘制图形时,原点(x, y)一般代表的是图形的左上角(left, top),但在绘制文字时,是个例外。y代表的是基线位置,x值确定以后,文字具体怎么显示还跟对齐设置有关。

- 2> 代码

自定义一个DrawTextView,继承View ,重写onDraw()方法,绘制文字和基线。

public class DrawTextView extends View {

    private Paint mPaint;

    public DrawTextView(Context context) {
        super(context);
         initPaint();
    }

    public DrawTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DrawTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();

    }

    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.MAGENTA);
        mPaint.setStrokeWidth(2);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        drawTextBase(canvas);
        //drawText_textAlign(canvas, 300,  200);
        //drawText_4Lines(canvas);
        //drawText_textBounds(canvas);
        //drawText_leftTop(canvas);
        //drawText_centerLine(canvas);


    }

    private void drawTextBase(Canvas canvas) {
        int baseX = 120;
        int baseLineY = 200;
        //写文字
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(120f);
        canvas.drawText("changes", baseX, baseLineY, mPaint);
        //画基线
        mPaint.setColor(Color.RED);
        canvas.drawLine(0, baseLineY, 3000, baseLineY, mPaint);

        //绘制原点
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeWidth(8f);
        canvas.drawPoint(baseX, baseLineY, mPaint);
   }
    ...
}

这里定义绘制文字的原点为(120, 200)。然后设置文字颜色和大小绘制文字,接着画基线和原点。
其中基线是一条从(0, 200)到(3000, 200)的水平线。
然后在布局文件中引用。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_draw_text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.wzhy.customviewdemos.customviews.drawtext.DrawTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

效果跟上面一样:

基线和起点

结论:

  • drawText(text, x, y, paint)t的y就是基线位置。
  • 只有原点(x, y)、文字大小、对齐方式确定后文字位置才真正确定。
3. 设置文字对齐:paint.setTextAlign(align)
- 1> 文字对齐和原点

上面我们知道了我们确定了drawText(text, x, y, paint)的y就是基线位置。但是,文字绘制的原点(x, y)、文字大小确定后,并不能完全确定文字位置,还要看文字对齐。

setTextAlign

我们在drawText(text, x, y, paint)中传入了原点(x, y),y代表基线位置,x就应该是文字绘制的起始了吧?但事实并不是想象的样子。x所代表的其实是水平方向上的一个参考点,文字可以以x为左边缘、右边缘以及中间位置。也就是文字相对于参考点x,有一个相对位置,这个相对位置就是文字对齐。下面是Paint类的设置文字对齐的方法:

/**
 * 设置Paint的文字对齐。这个方法控制文字相对于起点的位置。左对齐意味着所有的文字
 * 将会被绘制在原点的右边(原点指定文字的左边缘),以此类推。
 * 
 * @param align 设置Paint绘制文字的对齐参数值。align可以是:
 *              Paint.Align.LEFT    左对齐,文字以原点为左边缘,在原点右边
 *              Paint.Align.CENTER  居中对齐,文字以原点为中间位置
 *              Paint.Align.RIGHT   右对齐,文字以原点为右边缘,再远点左边
 */
public void setTextAlign(Align align) {
        nSetTextAlign(mNativePaint, align.nativeInt);
}
- 2> 代码和效果

下面,我们以(300, 200)为原点,绘制文字“Align”,但我们设置不同的文字对齐,看看有什么效果。代码:

private void drawText_textAlign(Canvas canvas, int baseX, int baseY) {
    //文字大小和对齐
    mPaint.setTextSize(120);
    mPaint.setTextAlign(Paint.Align.LEFT);
    //mPaint.setTextAlign(Paint.Align.CENTER);
    //mPaint.setTextAlign(Paint.Align.RIGHT);
    //绘制文字
    mPaint.setColor(Color.BLACK);
    canvas.drawText("Align", baseX, baseY, mPaint);
    //画基线
    mPaint.setColor(Color.RED);
    canvas.drawLine(0, baseY, 3000, baseY, mPaint);
    //画起始线
    canvas.drawLine(baseX, 0, baseX, baseY + 60, mPaint);
    //绘制原点
    mPaint.setColor(Color.GREEN);
    mPaint.setStrokeWidth(8f);
    canvas.drawPoint(baseX, baseY, mPaint);
    mPaint.setStrokeWidth(2f);
}

在onDraw()方法中调用drawText_textAlign(canvas, 300, 200),效果如下:

mPaint.setTextAlign(Paint.Align.LEFT)
这里写图片描述

mPaint.setTextAlign(Paint.Align.CENTER)
这里写图片描述

mPaint.setTextAlign(Paint.Align.RIGHT)
这里写图片描述

从效果图看出,原点(x, y)的x坐标,表示文字的相对位置。其实设置文字的对齐,就是相对于原点的对齐:
LEFT,表示文字左边对齐于原点;
CENTER,表示文字中间对齐于原点;
RIGNT,表示文字右边对齐于原点。
这样文字的位置就确定了。同时我们也知道了,确定了原点坐标和对齐方式,文字的位置就确定了。

二、绘制文字的四线格和FontMetrics

1. 绘制文字的四线格

上面我们知道文字的基线就是绘制文字原点的y坐标。其实系统绘制文字时还有其他几条线,见下图:
-图2.1.1-
text_4lines

由图,位置文字时除了基线外,还有四条线(topLine,ascentLine,descentLine,bottomLine),它们意义分别是:
-1> topLine–可绘制最高线
-2> ascentLine–建议绘制单行字符最高线
-3> descentLine–建议绘制单行字符最低线
-4> bottomLine–可绘制最低线

从图中我们还可以看到,ascentLine距离文字顶部的距离大于descentLine距离文字底部的距离,多出来的部分是做什么用呢?看下面的图片:
ascent_space
原来是不同国家文字不同,需要空出空间放置注音等符号。

2. FontMetrics
- 1> FontMetrics源码及概述

FontMetric是Paint类的一个静态内部类,可由Paint的getFontMetrics()获得。下面是它的源码:

/**
 * 此类描述了给定文字大小字体的尺寸变量。记住Y值向下为正,向上为负。
 * 测量值相对于基线,在基线下的为正值,在基线上的为负值。此类是Paint类
 * 的一个静态内部类。保存几个测量值。由Paint的getFontmetrics()返回。
 */
public static class FontMetrics {
    /**
     * The maximum distance above the baseline for the tallest glyph in the font at a given text size.
     * 距离baseline之上最大的距离。
     */
    public float   top;
    /**
     * The recommended distance above the baseline for singled spaced text.
     * 在单行文字里距离baseline之上推荐的距离。
     */
    public float   ascent;
    /**
     * The recommended distance below the baseline for singled spaced text.
     * 在单行文字里距离baseline之下推荐的距离。
     */
    public float   descent;
    /**
     * The maximum distance below the baseline for the lowest glyph in
     * the font at a given text size.
     * 距离baseline之下最大的距离。
     */
    public float   bottom;
    /**
     * The recommended additional space to add between lines of text.
     * 行距:在两行文字之间推荐的额外空间。
     */
    public float   leading;
}

从上面我们知道了原点的y坐标,表示基线位置。那么其他4条线位置怎么确定,FontMetrics的属性top、ascent、descent、bottom与4条线之间的关系是什么?我们接着看。
除了leading,FontMetrics还有四个值:top,ascent,descent,bottom。
由源码可知,它们分别表示对应线距基线的距离。
text_4lines
由源码和图可知FontMetrics的四个值分别是:

fontMetrics.top = topLineY - baselineY;
fontMetrics.ascent = ascentLineY - baselineY;
fontMetrics.descent = descentLineY - baselineY;
fontMetrics.bottom = bottomLineY - baselineY;

注意:Y值向下为正。这四个值都是以baseline为基准的,那么top和ascent就会为负值。

由上面可得四条线的位置:

topLineY = fontMetrics.top + baselineY;
ascentLineY = fontMetrics.ascent + baselineY;
descentLineY - fontMetrics.descent + baselineY;
bottomLineY = fontMetrics.bottom + baselineY;

而FontMetrics的leading表示的是行距,见下图:
leading

· 获得FontMetrics

//获得FontMetrics
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
//或
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
- 2> 代码和效果

上面我们计算通过基线和FontMetrics获得了四条线的位置,我们就可以绘制出这四条线。我们在文字对齐代码基础上绘制这四条线,代码如下:

private void drawText_4Lines(Canvas canvas) {

    int baseX = 60;
    int baseLineY = 300;
    drawText_textAlign(canvas, baseX, baseLineY);
    //计算四条线的位置
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    float ascentY = fontMetrics.ascent + baseLineY;
    float descentY = fontMetrics.descent + baseLineY;
    float topY = fontMetrics.top + baseLineY;
    float bottomY = fontMetrics.bottom + baseLineY;
    //画top
    mPaint.setColor(Color.BLUE);
    canvas.drawLine(0, topY, 3000, topY, mPaint);
    //画ascent
    mPaint.setColor(Color.GREEN);
    canvas.drawLine(0, ascentY, 3000, ascentY, mPaint);
    //画descent
    mPaint.setColor(Color.MAGENTA);
    canvas.drawLine(0, descentY, 3000, descentY, mPaint);
    //画bottom
    mPaint.setColor(Color.CYAN);
    canvas.drawLine(0, bottomY, 3000, bottomY, mPaint);
}

原点坐标为(60, 300),文字对齐为左对齐,根据FontMetrics和baseLineY计算出四条线位置,然后画出四条线。
效果跟上面一样:
ascent_space

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

这部分,将了解如何获取文字所占区域的高度、宽度和仅包裹文字的最小矩形。

text_bounds

图中绿色矩形是文字所占区域,蓝色区域是仅包裹文字的最小矩形。

1. 文字的宽度和高度
- 1> 文字高度

文字高度容易获取,直接用bottomLineY - topLineY即可。

Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int bottomY = fontMetricsInt.bottom + baseLineY;
int topY = fontMetricsInt.top + baseLineY;
//所占高度
int height = bottomY - topY;
- 2> 文字宽度

文字的宽度要用到Paint类的一个方法:measureText(String text)

mPaint.setTextSize(120f);
int width = (int) mPaint.measureText(text);

注意:使用前,一定要设置好文字的大小(如,mPaint.setTextSize(160f)),不然无法测量文字的宽度。

- 3> 最小矩形

要得到绘制文字的最小矩形,同样要用到Paint类的一个方法:

/** 
 * 获取指定字符串所对应的最小矩形,以(0,0)点所在位置为基线 
 * @param text  要测量最小矩形的字符串 
 * @param start 要测量起始字符在字符串中的索引 
 * @param end   所要测量的字符的长度 
 * @param bounds 接收测量结果 
 */  
public void getTextBounds(String text, int start, int end, Rect bounds){...}

使用起来很简单:

mPaint.setTextSize(120f);
 /*最小矩形*/
 Rect minRect = new Rect();
 mPaint.getTextBounds(text, 0, text.length(), minRect);
 Log.i("Rect", "minRect: " + minRect.toShortString());

使用之前同样需要设置文字大小。
Log打印结果为:

I/Rect: minRect: [2,-144][570,34]

可以看到,这个矩形左上角位置为(2, -144),右下角坐标为(570, 34)。

有点疑惑,左上角的Y坐标为什么是负数?我们在代码中并没有给getTextBounds()设置原点,那么它就是以点(0, 0)为原点(y = 0作为基线),来绘制的矩形。所以跟绘制文字一样,这个最小矩形是以(0, 0)为原点来绘制的(同样以y坐标为基线)。

而我们上面绘制文字时传入了原点,这个最小矩形就会与实际文字位置错开,所以这个矩形需要考虑加上绘制文字时的原点。

minRect.left += baseX;
minRect.top += baseLineY;
minRect.bottom += baseLineY;
minRect.right += baseX;

经过相加才是正确的最小矩形位置。因为文字以(baseX, baseLineY)为原点绘制,而最小矩形的测量基准为(0, 0)。相当于平移了最小矩形。

- 2> 代码和效果

上面我们知道了怎样获得文字宽度、高度以及最小矩形,我们就可以绘制出文字所站区域和最小矩形。代码如下:

private void drawText_textBounds(Canvas canvas) {
    //定义原点
    int baseX = 300;
    int baseLineY = 300;
    //定义要绘制的文字
    String text = "AgeÂǎЙ";
    //设置文字的大小和对齐方式
    mPaint.setTextSize(160f);
    mPaint.setTextAlign(Paint.Align.LEFT);
    /*字符串所占的高度和宽度*/
    Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
    int bottomY = fontMetricsInt.bottom + baseLineY;
    int topY = fontMetricsInt.top + baseLineY;
    //所占高度  int height = bottomY - topY;
    //宽度
    int width = (int) mPaint.measureText(text);
    //绘制所占区域
    Rect rect = new Rect(baseX, topY, baseX + width, bottomY);
    mPaint.setColor(Color.GREEN);
    canvas.drawRect(rect, mPaint);
    /*最小矩形*/
    Rect minRect = new Rect();
    mPaint.getTextBounds(text, 0, text.length(), minRect);
    Log.i("Rect", "minRect: " + minRect.toShortString());
    minRect.left += baseX;
    minRect.top += baseLineY;
    minRect.bottom += baseLineY;
    minRect.right += baseX;
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(minRect, mPaint);
    //绘制文字
    mPaint.setColor(Color.BLACK);
    canvas.drawText(text, baseX, baseLineY, mPaint);
    //画基线
    mPaint.setColor(Color.RED);
    canvas.drawLine(0, baseLineY, 3000, baseLineY, mPaint);
    //画起始线
    canvas.drawLine(baseX, 0, baseX, baseLineY + 60, mPaint);
    //绘制原点
    mPaint.setColor(Color.GREEN);
    mPaint.setStrokeWidth(8f);
    canvas.drawPoint(baseX, baseLineY, mPaint);
    mPaint.setStrokeWidth(2f);
}

效果如下:
文字所占区域和包裹文字最小矩形

四、定点写字

我们在实际绘制文字时,一般不会直接得到原点来绘制文字。较为常见的是给出左上角或水平中线,或被约束在一个宽高中居中显示。
而我们绘制文字时就是根据原点和文字对齐来定位文字位置的。所以我们需要计算出原点位置(或基线位置)。

1. 给定左上顶点绘图

如果给出左上顶点(left, top),我们需要计算出基线的位置。
上面我们知道:
topLineY = fontMetrics.top + baselineY
=>baselineY = topLineY - fontMetrics.top
我们就得到了baseline位置,也就是原点的y坐标;而left就是原点的x坐标。
这里的top就是topLine的y坐标topLineY。
那么原点坐标就是:
(left, top - fontMetrics.top)

先看效果图:

给定左上定点绘制文字

再看代码:
给定了左上定点(60, 60),得到FontMetrics,计算得到基线位置,最后绘制文字。

private void drawText_leftTop(Canvas canvas) {
    String text = "AngelÂ";
    int topX = 60;
    int topY = 60;
    //设置paint
    mPaint.setTextSize(200);//单位:px
    mPaint.setTextAlign(Paint.Align.LEFT);
    //画左上顶点
    mPaint.setColor(Color.BLUE);
    mPaint.setStrokeWidth(8f);
    canvas.drawPoint(topX, topY, mPaint);
    mPaint.setStrokeWidth(2f);
    //画top线
    mPaint.setColor(Color.RED);
    canvas.drawLine(0, topY, 3000, topY, mPaint);
    //找到基线位置
    Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
    int baseLineY = topY - fontMetricsInt.top;
    //画基线
    mPaint.setColor(Color.GREEN);
    canvas.drawLine(0, baseLineY, 3000, baseLineY, mPaint);
    /*写文字*/
    mPaint.setColor(Color.BLACK);
    canvas.drawText(text, topX, baseLineY, mPaint);
}
2. 给定中线位置绘制文字

给定中线位置centerLineY,我们同样需要通过它计算出基线位置baselineY才能绘制文字。
下面是计算步骤,根据坐标运算:
给出中线位置绘制文字

①centerLine作为ascentLine和descentLine的中间线
centerLineY = (ascentLineY + descentLineY)/2
<=> centerLineY = (ascent + baselineY + descent + baselineY)/2
<=> centerLineY = baselineY + (ascent + descent)/2
<=>baselineY = centerLineY - (ascent + descent)/2
∵ ascent = fontMetrics.ascent, descent = fontMetrics.descent
∴ baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2

②centerLine作为topLine和bottomLine的中间线
centerLineY = (bottomLineY + topLineY)/2
<=> centerLineY = (bottom + baselineY + top + baselineY)/2
<=> centerLineY = baselineY + (bottom + top)/2
<=>baselineY = centerLineY - (bottom + top)/2
∵ bottom = fontMetrics.bottom, top = fontMetrics.top
∴ baseLineY = centerLineY - (fontMetrics.bottom + fontMetrics.top)/2

因为topLine与ascentLine的距离大于bottomLine与descentLine距离。所以①方法更接近与文字中线。选择①的计算结果为基线位置(①和②差别不很大)。

代码如下:

private void drawText_centerLine(Canvas canvas) {
    String text = "AngelÂ";
    int baseX = 120;
    int centerLineY = 200;
    //设置文字大小和文字排列
    mPaint.setTextSize(200);//单位px
    mPaint.setTextAlign(Paint.Align.LEFT);
    //画中线
    mPaint.setColor(Color.BLUE);
    canvas.drawLine(0, centerLineY, 3000, centerLineY, mPaint);
    //计算基线位置
    /*
    * centerLineY  = (ascentLineY + descentLineY)/2
    * <=> centerLineY = (ascent + baselineY + descent + baselineY)/2
    * <=> centerLineY = baselineY + (ascent + descent)/2
    * <=>baselineY = centerLineY - (ascent + descent)/2
    * ∵ ascent = fontMetrics.ascent, descent = fontMetrics.descent
    * ∴ baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2
    * */
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    //float baselineY = centerLineY - (fontMetrics.top + fontMetrics.bottom) / 2;
    float baselineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent) / 2;

    //画基线
    mPaint.setColor(Color.GREEN);
    canvas.drawLine(0, baselineY, 3000, baselineY, mPaint);
    //画文字
    mPaint.setColor(Color.BLACK);
    canvas.drawText(text, baseX, baselineY, mPaint);
    //画出其他几条线
    mPaint.setColor(Color.MAGENTA);
    float topY = baselineY + fontMetrics.top;
    float bottomY = baselineY + fontMetrics.bottom;
    float ascentY = baselineY + fontMetrics.ascent;
    float decentY = baselineY + fontMetrics.descent;
    canvas.drawLine(0, topY, 3000, topY, mPaint);
    canvas.drawLine(0, bottomY, 3000, bottomY, mPaint);
    canvas.drawLine(0, ascentY, 3000, ascentY, mPaint);
    canvas.drawLine(0, decentY, 3000, decentY, mPaint);
}

效果:
①baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2

这里写图片描述

②baseLineY = centerLineY - (fontMetrics.bottom + fontMetrics.top)/2

这里写图片描述

从效果可以看出,centerLine作为ascentLine和descentLine的中线更接近于文字真正的中线,而作为topLine和bottomLine中线则接近与文字上部。

笔记终于做完,经过自己的学习和验证,多有收获。看似有点耗时,但是确实值得。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值