文章

Paint之文字绘制

Paint之文字绘制

Paint 之文字绘制

文字绘制

TextPaint

Paint 有一个唯一的子类 TextPaint 就是专门为文本绘制量身定做的 “ 笔 “,而这支笔就如 API 所描述的那样能够在绘制时为文本添加一些额外的信息, 这些信息包括:baselineShift, bgColor, density, drawableState, linkColor。

  1. 绘制文本时能够实现换行绘制,在正常情况下 Android 绘制文本是不能识别换行符之类的标识符的,这时候如果我们想实现换行绘制就得另辟途径使用 StaticLayout 结合 TextPaint 实现换行

常用 API

float ascent () 返回 ascent

float descent () 返回 descent

FontMetrics getFontMetrics () 返回 FontMetrics

float getFontMetrics (FontMetrics metrics)

这个和我们之前用到的 getFontMetrics () 相比多了个参数,getFontMetrics () 返回的是 FontMetrics 对象而 getFontMetrics (Paint. FontMetrics metrics) 返回的是文本的行间距,如果 metrics 的值不为空则返回 FontMetrics 对象的值;如果为 null 返回的是合适的 paint 属性值

FontMetricsInt getFontMetricsInt ()

该方法返回了一个 FontMetricsInt 对象,FontMetricsInt 和 FontMetrics 是一样的,只不过 FontMetricsInt 返回的是 int 而 FontMetrics 返回的是 float

FontMetricsInt getFontMetricsInt (Paint. FontMetricsInt fmi)

getFontMetrics(FontMetrics metrics),只是返回值为 int

float getFontSpacing () 返回字符行间距

setUnderlineText (boolean underlineText) 设置下划线

setTypeface

setTypeface (Typeface typeface) 设置字体类型 Android 中字体有四种样式:BOLD(加粗), BOLD_ITALIC(加粗并倾斜), ITALIC(倾斜), NORMAL(正常);而其为我们提供的字体有五种:DEFAULT, DEFAULT_BOLD, MONOSPACE, SANS_SERIFSERIF

breakText

int breakText (CharSequence text, int start, int end,  boolean measureForwards, float maxWidth, float[] measuredWidth) 这个方法让我们设置一个最大宽度在不超过这个宽度的范围内返回实际测量值否则停止测量

1
2
3
4
5
6
text: 表示我们的字符串
start表示从第几个字符串开始测量
end表示从测量到第几个字符串为止
measureForwards表示向前还是向后测量
maxWidth表示一个给定的最大宽度在这个宽度内能测量出几个字符
measuredWidth为一个可选项,可以为空,不为空时返回真实的测量值。

同样的方法还有 breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)

这些方法在一些结合文本处理的应用里比较常用,比如文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串

setTextSkewX

void setTextSkewX (float skewX) 这个方法可以设置文本在水平方向上的倾斜,效果类似下图

1
2
// 设置画笔文本倾斜
textPaint.setTextSkewX(-0.25F);

这个倾斜值没有具体的范围,但是官方推崇的值为 -0.25 可以得到比较好的倾斜文本效果,值为负右倾值为正左倾,默认值为 0

setTextSize

setTextSize (float textSize)  文字大小

要注意该值必需大于零

setTextScaleX

setTextScaleX (float scaleX) 将文本沿 X 轴水平缩放,默认值为 1,当值大于 1 会沿 X 轴水平放大文本,当值小于 1 会沿 X 轴水平缩放文本

1
textPaint.setTextScaleX(0.5F);

setTextScaleX 不仅放大了文本宽度同时还拉伸了字符!这是亮点~~

setTextLocale

setTextLocale (Locale locale) 设置 Locale,国际化用到

setTextAlign

setTextAlign (Paint. Align align) 文本对齐方式

设置文本的对其方式,可供选的方式有三种:CENTER, LEFT 和 RIGHT

文本的绘制是从 baseline 开始没错,但是是从哪边开始绘制的呢?左端还是右端呢?而这个 Align 就是为我们定义在 baseline 绘制文本究竟该从何处开始

  1. Align.LEFT 则从文本的左端开始往右绘制,drawText 的时候起点 x = 0
  2. Align.RIGHT 则从文本的右端开始往左绘制,drawText 的时候起点 x = canvas.getWidth ()
  3. Align.CENTER 从文本的中间往两边绘制,drawText 的时候起点 x = canvas.getWidth () / 2

image.png

两种居中绘制方式

1
2
3
4
5
// Align.LEFT居中绘制
baseX = (int) (canvas.getWidth() / 2 - textPaint.measureText(TEXT) / 2);

// Align.CENTER居中绘制
canvas.drawText(TEXT, canvas.getWidth() / 2, baseY, textPaint);

setSubpixelText

setSubpixelText (boolean subpixelText) 设置 SUBPIXEL_TEXT_FLAG

设置是否打开文本的亚像素显示,什么叫亚像素显示呢?你可以理解为对文本显示的一种优化技术,如果大家用的是 Win 7+ 系统可以在控制面板中找到一个叫 ClearType 的设置,该设置可以让你的文本更好地显示在屏幕上就是基于亚像素显示技术。

setStrikeThruText 删除线

Paint. setStrikeThruText (boolean strikeThruText) 文本删除线

setLinearText

Paint. setLinearText (boolean linearText)

设置是否打开线性文本标识,这玩意对大多数人来说都很奇怪不知道这玩意什么意思。想要明白这东西你要先知道文本在 Android 中是如何进行存储和计算的。在 Android 中文本的绘制需要使用一个 bitmap 作为单个字符的缓存,既然是缓存必定要使用一定的空间,我们可以通过 setLinearText (true) 告诉 Android 我们不需要这样的文本缓存。

setFakeBoldText

setFakeBoldText (boolean fakeBoldText) 设置文本仿粗体

measureText

  • measureText (String text),
  • measureText (CharSequence text, int start, int end),
  • measureText (String text, int start, int end),
  • measureText (char[] text, int index, int count) 测量文本宽度

API 21 中还新增了两个方法

setLetterSpacing 行间距

setLetterSpacing (float letterSpacing) 设置行间距,默认是 0

FontMetrics/FontMetricsInt

FontMetrics 意为字体测量。y 轴向下增长。FontMetrics 其实是 Paint 的一个内部类,而它里面呢就定义了 top, ascent, descent, bottom, leading 五个成员变量其他什么也没有。


image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class FontMetrics {
    /**
     * The maximum distance above the baseline for the tallest glyph in
     * the font at a given text size.
     */
    public float   top;
    /**
     * The recommended distance above the baseline for singled spaced text.
     */
    public float   ascent;
    /**
     * The recommended distance below the baseline for singled spaced text.
     */
    public float   descent;
    /**
     * The maximum distance below the baseline for the lowest glyph in
     * the font at a given text size.
     */
    public float   bottom;
    /**
     * The recommended additional space to add between lines of text.
     */
    public float   leading;
}

在 Android 中,文字的绘制都是从 Baseline 处开始。

ascent

ascent,Baseline 往上至字符最高处的距离我们称之为 ascent(上坡度),负数

descent

Baseline 往下至字符最底处的距离我们称之为 descent(下坡度),负数

leading

leading(行间距)则表示上一行字符的 descent 到该行字符的
ascent 之间的距离

top

top 除了 Baseline 到字符顶端 ascent 的距离外还应该包含这些符号的高度,比 ascent 高一些,正数

bottom

bottom 出了 Baseline 到字符底端 descent 的距离外还包含这些符号的高度,比 descent 低一些,正数

一般情况下我们极少使用到类似的符号所以往往会忽略掉这些符号的存在,但是 Android 依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么 top 和 bottom 总会比 ascent 和 descent 大一点的原因。而在 TextView 中我们可以通过 xml 设置其属性 android:includeFontPadding=”false” 去掉一定的边距值但是不能完全去掉

image.pngimage.png

中线计算公式

中线 = canvas.height/2 - descent + (descent-ascent)/2 = canvas.height/2 - (descent+ascent)/2

需要将 Baseline 往下移 descent

获取 ascent/descent

  1. paint.getFontMetrics().ascent/descent/top/bottom/leading
  2. paint.descent()/ascent()

Typeface

defaultFromStyle

static Typeface defaultFromStyle (@Style int style) 将 Android 中字体有四种样式:BOLD(加粗), BOLD_ITALIC(加粗并倾斜), ITALIC(倾斜), NORMAL(正常);封装成 Typeface

create

create (String familyName, int style) 和 create (Typeface family, int style) 创建 Typeface

两者大概意思都一样,比如

1
2
textPaint.setTypeface(Typeface.create("SERIF", Typeface.NORMAL));
textPaint.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));

createFromAsset(AssetManager mgr, String path)createFromFile(String path)createFromFile(File path)
这三者也是一样的,它们都允许我们使用自己的字体比如我们从 asset 目录读取一个字体文件:

1
2
3
// 获取字体并设置画笔字体
Typeface typeface = Typeface.createFromAsset(context.getAssets(), "kt.ttf");
textPaint.setTypeface(typeface);

文字测量

measureText 测量文字的宽度并返回

getTextBounds (String text, int start, int end, Rect bounds)

获取文字的显示范围。

  • 参数
    参数里,text 是要测量的文字,start 和 end 分别是文字的起始和结束位置,bounds 是存储文字显示范围的对象,方法在测算完成之后会把结果写进 bounds。
  • bounds(相对于 baseline)
    查看它的属性值 top、bottom 会发现 top 是一个负数;bottom 有时候是 0,有时候是正数。

baseline 坐标看成原点(0,0),那么相对位置 top 在它上面就是负数,bottom 跟它重合就为 0,在它下面就为正数。像小写字母 j g y 等,它们的 bounds bottom 都是正数,因为它们都有降部(在西文字体排印学中,降部指的是一个字体中,字母向下延伸超过基线的笔画部分)。

measureText 和 getTextBounds 区别

measureText () 测出来的宽度总是比 getTextBounds () 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。

  1. getTextBounds: 它测量的是文字的显示范围(关键词:显示)。形象点来说,你这段文字外放置一个可变的矩形,然后把矩形尽可能地缩小,一直小到这个矩形恰好紧紧包裹住文字,那么这个矩形的范围,就是这段文字的 bounds。
  2. measureText (): 它测量的是文字绘制时所占用的宽度(关键词:占用)。前面已经讲过,一个文字在界面中,往往需要占用比他的实际显示宽度更多一点的宽度,以此来让文字和文字之间保留一些间距,不会显得过于拥挤。

文字绘制的方式

见 [[#文字绘制方式]] 章节

其他

Bitmap 上绘制文字

见 [[Canvas基础#Bitmap 上绘制文字]] 章节

文字绘制方式

Canvas 绘制文字的方式有三个:drawText()drawTextRun()drawTextOnPath()

Canvas. drawTextXXX 单行

drawText

  • drawText (@NonNull String text, float x, float y, @NonNull Paint paint)
1
2
参数x: x坐标
参数y: 是文字基线baseline的y坐标向上上负数向下正数

这个坐标并不是文字的左上角,在绘制文字的时候把坐标填成 (0, 0),文字并不会显示在 View 的左上角,而是会几乎完全显示在 View 的上方,到了 View 外部看不到的位置。y 指的是文字的基线( baseline ) 的位置。

image.png

特点

  1. 不能在 View 的边缘自动折行
  2. 不能在换行符 \n 处换行,在换行符 \n 的位置并没有换行,而只是加了个空格

drawTextRun  API 23 一个增加了「上下文」和「文字方向 RTL」支持的增强版本的 drawText ()

  • drawTextRun (@NonNull CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint)
1
2
3
4
5
6
7
8
text:要绘制的文字
start:从那个字开始绘制
end:绘制到哪个字结束
contextStart:上下文的起始位置。contextStart 需要小于等于 start
contextEnd:上下文的结束位置。contextEnd 需要大于等于 end
x:文字左边的坐标
y:文字的基线坐标
isRtl:是否是 RTL(Right-To-Left,从右向左)

drawTextOnPath 沿着一条 Path 来绘制文字

1
2
3
text:要绘制的文字
path:路径
hOffset 和 vOffset:它们是文字相对于Path的水平偏移量和竖直偏移量,利用它们可以调整文字的位置。例如你设置 hOffset 为 5, vOffset 为 10,文字就会右移 5 像素和下移 10 像素。

drawTextOnPath () 使用的 Path ,拐弯处全用圆角,别用尖角。

示例:

1
2
3
4
5
6
7
8
9
Path path = new Path();
path.addOval(50, 50, 300, 200, Path.Direction.CW);
canvas.drawPath(path, mRedPaint);
canvas.drawTextOnPath("This is a text", path, 0, 0, mBluePaint);

path.reset();
path.addOval(400, 50, 650, 200, Path.Direction.CCW);
canvas.drawPath(path, mRedPaint);
canvas.drawTextOnPath("This is a text", path, 0, 0, mBluePaint);

image.png

Layout

BoringLayout 单行

主要负责显示单行文本,并提供了 isBoring 方法来判断是否满足单行文本的条件。

StaticLayout 多行,静态文本

StaticLayout 并不是一个 View 或者 ViewGroup ,而是 android. text. Layout 的子类,它是纯粹用来绘制文字的。StaticLayout 支持换行,它既可以为文字设置宽度上限来让文字自动换行,也会在 \n 处主动换行。
当文本为非单行文本,且非 Spannable 的时候,就会使用 StaticLayout,内部并不会监听 span 的变化,因此效率上会比 DynamicLayout 高,只需一次布局的创建即可,但其实内部也能显示 SpannableString,只是不能在 span 变化之后重新进行布局而已。
构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public StaticLayout(CharSequence source, TextPaint paint,
                    int width,
                    Alignment align, float spacingmult, float spacingadd,
                    boolean includepad) {
    this(source, 0, source.length(), paint, width, align,
         spacingmult, spacingadd, includepad);
}
public StaticLayout(CharSequence source, int bufstart, int bufend,
                    TextPaint paint, int outerwidth,
                    Alignment align,
                    float spacingmult, float spacingadd,
                    boolean includepad) {
    this(source, bufstart, bufend, paint, outerwidth, align,
         spacingmult, spacingadd, includepad, null, 0);
}
public StaticLayout(CharSequence source, int bufstart, int bufend,
        TextPaint paint, int outerwidth,
        Alignment align,
        float spacingmult, float spacingadd,
        boolean includepad,
        TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    this(source, bufstart, bufend, paint, outerwidth, align,
            TextDirectionHeuristics.FIRSTSTRONG_LTR,
            spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
}
public StaticLayout(CharSequence source, int bufstart, int bufend,
    TextPaint paint, int outerwidth,
    Alignment align, TextDirectionHeuristic textDir,
    float spacingmult, float spacingadd,
    boolean includepad,
    TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
    // ...
}

参数说明:

1
2
3
4
5
6
7
8
9
10
11
12
CharSequence source 需要分行的字符串
int bufstart 需要分行的字符串从第几的位置开始
int bufend 需要分行的字符串到哪里结束
TextPaint paint 画笔对象
int outerwidth 是文字区域的宽度文字到达这个宽度后就会自动换行
Alignment align  是文字的对齐方向有ALIGN_CENTER ALIGN_NORMAL ALIGN_OPPOSITE 三种
float spacingmult 是行间距的倍数通常情况下填 1 就好;(相对行间距相对字体大小1.5f表示行间距为1.5倍的字体高度。)
float spacingadd 是行间距的额外增加值通常情况下填 0 就好;(在基础行距上添加多少
boolean includepad 是指是否在文字上下添加额外的空间来避免某些过高的字符的绘制出现越界
TextUtils.TruncateAt ellipsize 从什么位置开始省略
int ellipsizedWidth 超过多少开始省略
int maxLines 最大行数

案例:

1
2
3
4
5
6
7
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    mStaticLayout = new StaticLayout(TEXT, textPaint, canvas.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false);
    mStaticLayout.draw(canvas);
}

DynamicLayout 多行,动态文本

当文本为 Spannable 的时候,TextView 就会使用它来负责文本的显示,在内部设置了 SpanWatcher,当检测到 span 改变的时候,会进行 reflow,重新计算布局。

StaticLayout 的用途

  1. 文中高频度大量 textview 刷新优化。
  2. 一个 textview 显示大量的文本,比如一些阅读 app
  3. 在控件上画文本,比如一个 ImageView 中心画文本。
  4. 一些排版效果, 比如多行文本文字居中对齐等。

仿小红书实现的文本展开/收起的功能(文本折叠展开效果)

https://github.com/MrTrying/ExpandableText-Example/tree/master

https://zhuanlan.zhihu.com/p/87509956

Ref

本文由作者按照 CC BY 4.0 进行授权