Draw Text in Deep

作者: eclipse_xu | 来源:发表于2019-12-11 09:48 被阅读0次

    Android系统提供了Textview来提供文字的显示,但很多时候开发者还需要使用Canvas来绘制Text,这时候,canvas.drawText()就不像Textview的使用这么简单了,需要掌握文字的测量以及渲染的流程。

    Paint.FontMetrics

    FontMetrics是文字测量的重要方法,它提供了下面这些变量,来展示文字测量的相关参数:

    • baseline:字符绘制基线
    • ascent:字符最高点到baseline的距离
    • top:字符最高点到baseline的最大距离
    • descent:字符最低点到baseline的距离
    • bottom:字符最低点到baseline的最大距离
    • leading:行间距,即前一行的descent与下一行的ascent之间的距离,单行则为0(注意不是行距)

    要注意的是,这些参数都是以baseline为基准,所以在baseline之上的参数均为负值,baseline之下的参数才为正值,且这些值是距离,而非坐标。或者可以理解为baseline.y = 0的时候的坐标值。

    top要大于ascent,原因是需要为拉丁语等带符号的语言留出位置

    由这些参数,可以定义下面的这些与渲染有关的参数。

    • 字体的高度

      可以通过descent + Math.abs(ascent)计算得到。

    • 行间距(leading)

      TextView的行间距调整设置是通过setLineSpacing(add, mult)方法,在xml中,可以通过lineSpacingExtra和lineSpacingMultiplier来设置,在Paint自定义绘制Text中,可以使用Paint.fontMetrics中的leading属性设置

    • 行高

      即字符所在行的高度 = ascent + descent + leading,即字符的高度 + 行间距,可以通过descent+Math.abs(ascent) + leading得到。如果在TextView中,可以直接通过getLineHeight()方法获取。

    • 字符间距(kerning)

      对于textView和Paint绘制的Text,可以分别使用各自类中的getLetterSpacing()和setLetterSpacing()方法获取和设置字符间距,对于TextView还可以在布局文件中使用属性letterSpacing进行定义。(注意以上的方法和属性是在API 21引入的,对于之前的版本,只能通过SpannableString类及相应的方法来间接调整。)

    通过下面这张图,大家可以非常清楚的了解FontMetrics。

    file

    文本测量

    文本的测量是非常复杂,因为要适配全球几百种语言不同的排版,除了前面提到的FontMetrics,Android的渲染API还提供了很多测量文本的API。

    getFontSpacing()

    这个API用于获取推荐的行距。即两行文字间的baseline的距离。

    这个值是系统根据文本的字体和字号自动计算的。当你使用drawText一行行绘制文字的时候,可以在换行的时候获取下一行的baseline坐标。

    如果使用StaticLayout进行多行文本的绘制,则不需要通过这个API来获取行距

    这里有一点需要注意的是,getFontSpacing所获取的行距,与FontMetrics获取的bottom + abs(top) + leading行距是不一样的,这主要是因为这两个API的计算方式不同,系统推荐使用getFontSpacing来获取多行文本绘制时的行距。

    getTextBounds()

    获取文字的实际显示范围。这个API返回的是当前绘制文字的最小矩形,即能完全包裹文字的矩形范围。

    measureText()

    与getTextBounds不同,measureText返回的是文字的实际占用位置,即理论上文字应该占用的区域。

    getTextWidths()

    这个API返回的数组中,包含了每个字符的实际宽度,在排版中,这个宽度也叫“advance width”。它们累加的和,即为measureText返回的长度。

    如果所选字体为等宽字体,则每个字符的宽度是相同的,如果非等宽字体,则不同字符的宽度是不同的。

    文字渲染Layout

    在Android中,文字渲染的基类是Layout类,它包含了文字测量、渲染和布局的所有功能,Layout类有几个子类:

    • BoringLayout
    • StaticLayout
    • DynamicLayout

    一般来说,如果待渲染文本是属于Spannable的文本对象,则使用动态布局DynamicLayout,否则,使用isBoring判断是不是单纯的单行布局,如果是则使用BoringLayout,其他情况使用StaticLayout。

    BoringLayout用于绘制仅一行文本的场景,它比较重要的地方是,它提供了一个静态方法isBoring来判断一段文字是否能在一行放下,这对于布局渲染是非常有帮助的。

    /**
     * Returns null if not boring; the width, ascent, and descent if boring.
     */
    val boring = BoringLayout.isBoring(drawText, textPaint)
    

    StaticLayout

    StaticLayout的使用场景为多行文本的渲染和SpannableString的渲染。

    SpannableString是不能通过Paint.getTextBounds或者是Paint.measureText来测量的

    StaticLayout的基本使用如下所示。

    val spannable = SpannableString(drawText)
    spannable.setSpan(RelativeSizeSpan(2f), 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    val staticLayout = StaticLayout(
        spannable, textPaint, width, Layout.Alignment.ALIGN_NORMAL,
        1F, 0F, true
    )
    val width = staticLayout.getLineWidth(0)
    val height = staticLayout.height
    Log.d("xys", "line width $width height $height")
    staticLayout.draw(canvas)
    

    Demo如图所示。

    file

    如果是API26+,可以使用新的API构造StaticLayout,代码如下所示。

    // API 26+
    val staticLayout = StaticLayout.Builder
            .obtain(text, start, end, textPaint, width)
            .build()
    

    通过StaticLayout.Builder可以设置一些API26+的额外参数,例如alignment、textDirection、lineSpacing、justificationMode等,其中justificationMode用于多行文本的两边对齐显示。

    关于StaticLayout这里有一篇比较好的文章推荐给大家。

    https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a

    TextPaint与Paint

    TextPaint是Paint的子类,与Paint的使用基本一致,但大多用于StaticLayout或者是用于测量计算时使用。

    TextPaint的示例代码如下所示。

    String text = "This is some text."
    
    TextPaint myTextPaint = new TextPaint();
    mTextPaint.setAntiAlias(true);
    mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
    mTextPaint.setColor(0xFF000000);
    
    float width = mTextPaint.measureText(text);
    float height = -mTextPaint.ascent() + mTextPaint.descent();
    

    TextAlign

    TextAlign设置的是文本的对齐方式,一共有三种,LEFT、CETNER和RIGHT,默认值为LEFT,它的作用是在绘制的时候确定绘制的方向,例如设置为LEFT,那么文本绘制的时候,就是从baseline的StartX开始向右绘制文本,如果是CENTER,那么就是从StartX开始,向两边开始绘制文字,同理,RIGHT为StartX向左开始绘制文本,这里要注意的是,TextAlign确定的是方向,而非在显示区域内的对齐方式,它的一个作用是帮助开发者进行居中的绘制,例如设置Paint的TextAlign为CENTER,drawText的时候起点x = canvas.getWidth() / 2即可。文本会根据基准线的中点开始向左右开始绘制文字,最终自然就变成了居中显示了。如果你设定了RIGHT,那么从baseline的StartX的右边开始绘制。

    通过下面这个例子,可以很清楚的了解这一原理。

    file

    文本的居中绘制

    Android中文本的绘制都是使用baseline进行定位的,通过fontMetrics和已知的区域坐标,是可以推算出文字的其它关键坐标的,所以,文本在任意区域的任意位置绘制问题,其实就是一个坐标运算的问题,根据已知变量和fontMetrics的相关参数,来计算baseline的距离,下面就是文本垂直居中的推算过程。

    文本的descent:
    descentY = baselineY + fontMetrics.descent;
    文本的字体高度:
    fontHeight = fontMetrics.descent- fontMetrics.ascent
    当文本垂直居中时的bottom距离应该为:
    descentY=1/2 * height + 1/2 * fontHeight

    baselineY = 1/2 * height - 1/2 * ( fontMetrics.ascent + fontMetrics.descent )
    此时求得baseline的值,即cavans.drawText()里的y的坐标。

    file

    breakText

    这个API与BoringLayout中的isBoring方法有些类似,主要是对文中进行一行的测量。

    breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)
    这个方法让我们可以设置一个最大宽度,在不超过这个宽度的范围内返回实际测量值,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)。
    这个方法在一些自定义文本绘制的场景下比较常用,例如阅读类APP的文字排版,需要在换行的时候动态折断或生成一行新的字符串。

    基本使用方式如下所示。

    measuredCount = paint.breakText(text, 0, text.length(), true, showWidth, measuredWidth);
    canvas.drawText(text, 0, measuredCount, paint);
    

    通过上面的方法,就得到了当前这一行可以容纳text文本中的多少个字符,如果showWidth不够展示全部的字符,text文本则会被截断,measuredCount就是该截断的位置。

    其它

    canvas中还有很多其它关于绘制文本的API,都是样式上的参数,这里不详细解释,例如:

    • textScaleX
    • letterSpacing(API 21+)
    • textSkewX

    这些都是一些设置文本样式的API,大家自己在Demo中设置下就知道样式了。

    整个文章的演示Demo上传到GitHub了,大家可以自己在手机上测试下,加深对文本渲染的了解,地址如下所示。

    https://github.com/xuyisheng/TextMatrix
    欢迎大家关注我的微信公众号——Android群英传

    相关文章

      网友评论

        本文标题:Draw Text in Deep

        本文链接:https://www.haomeiwen.com/subject/rgfogctx.html