显示文本信息可能是许多Android应用程序中最基本,最重要的部分。 本文将讨论TextView。 从“ Hello World”开始的每个开发人员都经常遇到此用户界面元素。 有时,在处理文本时,您必须考虑实现各种设计解决方案或在渲染屏幕时提高性能。
我将讨论TextView设备以及使用它的一些技巧。 关键提示来自过去的Google I / O报告。
引擎盖下的TextView
为了在Android中渲染文本,引擎盖下使用了一大堆不同的库。 它们可以分为两个主要部分-Java代码和本机代码:
Java代码本质上是应用程序开发人员可以使用的Android SDK的一部分,并且可以将其中的新功能移植到支持库中。
TextView核心本身是用C ++编写的,这限制了从新版本的操作系统向实现该功能的支持库的移植。 核心是以下库:
- Minikin用于按音节测量文本,换行符和单词的长度。
- ICU提供Unicode支持。
- HarfBuzz为Unicode字符找到字体中的相应图形元素(字形)。
- FreeType生成字形的位图。
- Skia是绘制2D图形的引擎。
测量文本长度和换行符
如果将行传递到在TextView内部使用的Minikin库,则它确定的第一件事是该行包含哪些字形:
从本示例中可以看到,将带有字形的Unicode字符匹配并不总是一对一的:这里,一次3个字符将对应一个ffi字形。 另外,值得注意的是,可以在各种系统字体中找到必要的字形。
仅在系统字体中找到字形可能会导致困难,尤其是如果图标或表情符号是通过字符显示的,并且应该将来自不同字体的字符合并在一行中。 因此,从Android Q(29)开始 ,可以创建应用程序随附的自己的字体列表。 此列表将用于搜索字形:
textView.typeface = TypeFace.CustomFallbackBuilder( FontFamily.Builder( Font.Builder(assets, “lato.ttf”).build() ).build() ).addCustomFallback( FontFamily.Builder( Font.Builder(assets, “kosugi.ttf”).build() ).build() ).build()
现在,使用CustomFallbackBuilder
当将字符与字形匹配时,SDK将按顺序CustomFallbackBuilder
指定的字体系列,如果找不到,则搜索将继续使用系统字体(并通过setSystemFallback()
方法可以指定首选的系统字体系列)。 CustomFallbackBuilder
对字体系列的数量有限制-您最多可以添加64种字体。
Minikin库将字符串拆分为单词并测量单个单词。 为了加快工作速度,从Lollipop(21)开始 ,使用系统LRU单词缓存。 这样的缓存可带来巨大的性能提升:对一个缓存的单词的Paint.measureText()
调用将平均花费其首次计算其大小的时间的3%。
如果文本不适合指定的宽度,则Minikin会在文本中排列换行符和单词。 从棉花糖(23)开始,您可以通过为TextView指定breakStrategy
和breakStrategy
特殊属性来控制其行为。
使用breakStrategy=simple
值时breakStrategy=simple
库将简单地按顺序排列连字符,并遍历文本:一旦行不再适合,则将连字符放在最后一个单词的前面。
在balanced
值时balanced
库将尝试进行换行,以使行的宽度对齐。
high_quality
的行为几乎与balanced
相同,但有一些区别(其中之一:倒数第二行,连字符不仅可以是单独的单词,而且可以是音节的单词)。
hyphenationFrequency
属性使hyphenationFrequency
可以控制音节自动换行的策略。 值none
不会自动断字, normal
会使断字的频率降低,而full
会使用最大的单词数。
文本呈现性能取决于所选标志(在Android P(28)上测得):
鉴于性能受到很大影响,从版本Q(29)和AppCompat 1.1.0开始,Google开发人员决定默认关闭连字符。 如果自动换行在应用程序中很重要,则现在需要显式启用它。
使用自动换行时,必须考虑到操作系统中当前选择的语言会影响库的操作。 根据语言,系统将选择带有传输规则的特殊词典。
文字样式
有几种在Android中设置文字样式的方法:
- 适用于整个TextView元素的单一样式 。
- 多种样式(多种样式) -可以同时在段落或单个字符级别将多种样式应用于文本。 有几种方法可以做到这一点:
单一样式意味着在TextView标记中使用XML样式或XML属性。 在这种情况下,系统将按以下顺序应用资源中的值:TextAppearance,主题(主题),默认样式(Default样式),应用程序中的样式,并且最高优先级是View属性的值。
使用资源是一个相当简单的解决方案,但是不幸的是,它不允许您将样式应用于文本的某些部分。
HTML标签是另一种简单的解决方案,它提供的功能包括使单个单词加粗,斜体,甚至突出显示列表中带有文本点的列表。 开发人员所需要做的只是调用Html.fromHtml()
方法,该方法会将标记的文本转换Html.fromHtml()
标记的文本。 但是此解决方案功能有限,因为它只能识别html标记的一部分,并且不支持CSS样式。
val text = "My text <ul><li>bullet one</li><li>bullet two</li></ul>" myTextView.text = Html.fromHtml(text)
可以将各种样式化TextView的方法组合在一起,但值得记住的是特定方法的优先级,这会影响最终结果:
另一种方法-在画布上绘制文本-使开发人员可以完全控制文本输出:例如,您可以沿曲线绘制文本。 但是,根据要求,这种解决方案可能很难实施,超出了本文的范围。
跨度
TextView使用跨度来微调样式。 使用跨度,您可以更改字符范围的颜色,使文本成为链接的一部分,更改文本的大小,在段落前面绘制点等。
可以区分以下几类跨度:
- 字符跨度 -在字符串的字符级别应用。
- 外观影响 -请勿更改文字大小。
- 度量标准影响 -调整文本大小。
- 段落跨度 -在段落级别应用。
Android框架具有接口和抽象类,这些接口和抽象类具有在onMeasure()
和呈现TextView期间调用的方法,这些方法可跨度访问TextPaint
和Canvas
等较低级别的对象。 使用跨度,Android框架检查此对象实现的接口以调用必要的方法。
android框架定义了大约20个跨度,因此在制作自己的跨度之前,最好检查一下SDK是否合适。
外观与指标影响范围
跨度的第一类影响字符串中字符的外观:字符颜色,背景颜色,带下划线或划线的字符等。 这些范围实现UpdateAppearance
接口并继承自CharacterStyle
类,该类提供对TextPaint
对象的访问。
影响跨度的度量标准会影响文本和布局的大小,因此,使用这种跨度不仅需要重绘TextView,还需要调用onMeasure()
/ onLayout()
。 这些范围通常是从MetricAffectingSpan
类继承的,而该类是从上述CharacterStyle
继承的。
字符与段落影响跨度
段落跨度会影响整个文本块:它可以更改对齐方式,缩进甚至在段落的开头插入一个点。 此类跨度应从ParagraphStyle
类继承,并从ParagraphStyle
的开头到结尾完全插入到文本中。 如果范围不正确,则跨度将不起作用。
在Android上,段落被认为是用换行符( \n
)分隔的文本的一部分。
编写跨度
在编写自己的跨度时,您需要确定跨度将影响什么,以便选择要从哪个类继承:
- 在字符级别上影响文本->
CharacterStyle
- 在段落级别影响文本->
ParagraphStyle
- 影响文本视图→
UpdateAppearance
- 影响文字大小-
UpdateLayout
这是更改字体的跨度示例:
class CustomTypefaceSpan(private val font: Typeface?) : MetricAffectingSpan() { override fun updateMeasureState(textPaint: TextPaint) = update(textPaint) override fun updateDrawState(textPaint: TextPaint) = update(textPaint) fun update(textPaint: TextPaint) { textPaint.apply { val old = typeface val oldStyle = old?.style ?: 0 val font = Typeface.create(font, oldStyle) typeface = font
想象一下,我们想创建自己的跨度以突出显示代码块,为此,我们将编辑先前的跨度-添加字体后,我们还将添加文本背景色的更改:
class CodeBlockSpan(private val font: Typeface?) : MetricAffectingSpan() { … fun update(textPaint: TextPaint) { textPaint.apply {
将跨度应用于文本:
但是,您可以通过组合两个跨度来获得完全相同的结果:从Android框架中获取我们之前的CustomTypefaceSpan
和BackgroundColorSpan
:
这两种解决方案将有所不同。 事实是,自写跨度无法实现Parcelable
接口,这与系统接口不同。
当通过Intent或剪贴板传输风格化的行时,如果跨度为自写标记,则不会保存。 当使用框架的跨度时,标记将保留。
在文本中使用跨度
框架中有两个用于样式化文本的接口: Spannable
和Spannable
(分别具有不变的标记和可变的标记)和三个实现: SpannedString
(不变的文本), SpannableString
(不变的文本)和SpannableStringBuilder
(可变文本)。
例如, SpannableStringBuilder
用于需要更改文本的EditText
中。
您可以使用以下方法向行添加新的跨度:
setSpan(Object what, int start, int end, int flags)
跨度通过第一个参数传递,然后指示文本中的索引范围。 最后一个参数是可以控制的,插入新文本时跨度的行为是什么:跨度是否会扩展到在起点或终点插入的文本(如果在中间插入新文本,则无论标志值如何,该跨度都会自动应用于该文本) 。
上面列出的类不仅在语义上有所不同,而且在内部排列方面也有所不同: SpannedString
和SpannableString
使用数组存储跨度,而SpannableStringBuilder
使用间隔树 。
如果根据跨度的数量测试渲染文本的速度,您将得到以下结果:当连续使用多达250个跨度时, SpannableString
和SpannableStringBuilder
工作速度大致相同,但是如果标记元素超过250,则SpannableString
启动输。 因此,如果任务是将样式应用于某些文本,则在选择类时,应遵循语义要求:线条和样式是否可变。 但是,如果标记需要超过250个跨度,则应始终将SpannableStringBuilder
为SpannableStringBuilder
。
检查文本中的跨度
该任务会定期出现,以检查跨度线是否具有特定跨度。 在Stackoverflow上,您可以找到以下代码:
fun <T> hasSpan(spanned: Spanned, clazz: Class<T>): Boolean { val spans: Array<out T> = spanned.getSpans(0, spanned.length, clazz) return spans.isNotEmpty() }
这样的解决方案可以工作,但是效率很低:您必须遍历所有范围,检查每个范围是否属于传递的类型,将结果收集到数组中,最后只检查数组是否为空。
一个更有效的解决方案是使用nextSpanTransition()
方法:
fun <T> hasSpan(spanned: Spanned, clazz: Class<T>): Boolean { val limit = spanned.length return spanned.nextSpanTransition(0, limit, clazz) < limit }
各种语言资源中的文本标记
当您想借助各种字符串资源中的标记来突出显示特定单词时,可能会出现这样的任务。 例如,我们需要在英语版本中突出显示“文本”一词,在西班牙语中突出显示“ texto”一词 :
<string name="title">Best practices for text in Android</string> <string name=”title”>Texto en Android: mejores prácticas</string>
例如,如果您需要简单的内容以粗体突出显示该单词,则可以使用常用的html标签( <b>
)。 在用户界面中,您只需要在TextView中设置字符串资源:
textView.setText(R.string.title)
但是,如果您需要更复杂的内容(例如更改字体),则无法再使用html。 解决方案是使用特殊的<annotation>
。 该标记允许您在xml文件中定义任何键值对。 当我们从资源中提取字符串时,这些标签会自动转换为Annotation
范围,并在文本中带有相应的键和值。 之后,您可以解析文本中的注释列表并应用必要的跨度。
假设我们需要使用CustomTypefaceSpan
更改字体。
添加一个标签,并为其定义一个“字体”键和一个值-我们要使用的字体类型为“ title_emphasis” :
<string name="title">Best practices for <annotation font=”title_emphasis”>text</annotation> in Android</string> <string name=”title”><annotation font=”title_emphasis”>Texto</annotation> en Android: mejores prácticas</string>
从资源中拉出字符串,使用“ font”键找到注释并排列跨度:
上面已经提到,Android框架之外的跨度无法实现Parcelable
,而是通过Intent进行传输。 但这不适用于实现Parcelable
注释。 因此,您可以通过Intent传递带注释的字符串,并通过安排跨度以完全相同的方式进行解析。
文本如何放置在TextView中
TextView不仅可以显示文本,还可以显示图片。 您还可以在文本前面设置各种缩进。 在幕后,这样做可以使TextView创建一个子类Layout,它直接负责显示文本。 这是一个具有三个实现的抽象类;通常,除非您编写自己的控件,否则不必直接使用它们:
- BoringLayout用于简单的文本,不支持换行符 ,RTL等,但是它是最轻量的。 如果文本符合所有限制,则TextView将使用它。
- 在其他情况下,在TextView中使用StaticLayout 。
- DynamicLayout用于EditText中的可变文本。
布局有许多方法可以让您知道所显示文本的各种参数:行的坐标,基线,行中文本的开头和结尾的坐标等。 (更多详细信息可以在文档中找到)
这样的方法可能非常有用。 例如,一些开发人员面临着将部分文本提取为圆角矩形,并试图通过不适用于解决此问题的跨度找到其解决方案的任务。
但是Layout类的方法可以解决。 这是一个示例解决方案:
使用注释,我们选择应在矩形中圈出的单词。
然后为所有文本环绕情况创建4个可绘制资源,这些资源应放在矩形中:
接下来,如上所述,我们在文本中找到了所需的注释。 现在,我们有了此类注释的开始和结束的索引。 通过Layout方法,您可以找到带注释的文本开始和结束的行号:
val startLine = layout.getLineForOffset(spanStartIndex) val endLine = layout.getLineForOffset(spanEndIndex)
接下来,您必须绘制一个或多个矩形。 考虑一个简单的情况,即文本的带注释的部分显示在一行上,那么我们只需要一个带有四个圆角的矩形。 定义其坐标并绘制:
... if (startLine == endLine) { val lineTop = layout.getLineTop(startLine)
从该示例中可以看到,布局在显示的文本上存储了许多有用的信息,这可以帮助实现各种非标准任务。
TextView性能
与任何View一样,TextView在显示时经历三个阶段: onMeasure()
, onLayout()
和onDraw()
。 同时,与其他两种方法不同, onMeasure()
花费的时间最多:此时,重新创建Layout类并计算文本大小。 因此,更改文本大小(例如,更改字体)需要进行大量工作。 更改文本的颜色将更加轻巧,因为它只需要调用onDraw()
。 如上所述,系统具有计算出的大小的全局字缓存。 如果单词已经在高速缓存中,则onMeasure()
调用onMeasure()
将会花费完整计算所需时间的11-16%。
文字加速
2015年,Instagram开发人员使用全局缓存加快了照片评论的显示速度。 想法是在将文本显示在屏幕上之前先对其进行虚拟绘制,从而“预热”系统缓存。 当需要显示文本时,由于文本已经过测量并且位于缓存中,因此用户可以更快地看到它。
从Android P(28)开始 ,Google开发人员已在API中添加了预先在后台线程中执行文本大小测量阶段的功能PrecomputedText
(以及从Android I(14)开始的API的PrecomputedTextCompat
)。 使用新的API,90%的工作将在后台线程中完成。
一个例子:
显示大文字
如果您需要显示大文本,则不要立即将其传输到TextView。 否则,该应用程序可能会停止平稳运行或完全冻结,因为它将在主线程上进行大量工作以显示巨大的文本,用户甚至可能无法滚动到末尾。 解决方案是将文本拆分为多个部分(例如段落),然后在RecyclerView中显示各个部分。 为了进一步提高速度,您可以使用PrecomputedText预计算文本块的大小。
为了促进PrecomputedText在RecyclerView中的嵌入,Google开发人员为PrecomputedTextCompat.getTextFuture()
和AppCompatTextView.setTextFuture()
制作了特殊的方法:
fun onBindViewHolder(vh: ViewHolder, position: Int) { val data = getData(position) vh.textView.setTextSize(...) vh.textView.setFontVariationSettings(...)
RecyclerView , , , .
, getTextFuture()
(, ), , , getTextFuture()
, , TextView.
, TextView
TextView.setText()
:
if (type == SPANNABLE || movementMethod != null) { text = spannableFactory.newSpannable(spannable)
span' TextView, setText()
, .
, . TextView , -, . , . , , TextView spannableFactory
:
class MySpannableFactory : Spannable.Factory() { override fun newSpannable(source: CharSequence): Spannable { return source as? Spannable ?: super.newSpannable(source) } } textView.spannableFactory = MySpannableFactory()
textView.setText(spannable, BufferType.SPANNABLE)
, .
Google span' RecyclerView, .
TextView, span, setText()
. TextView span. TextView spannable- span', :
val spannable = textView.getText() as Spannable val span = CustomTypefaceSpan(span) spannable.setSpan(span, ...)
span, TextView, TextView . , invalidate()
, – requestLayout()
:
val spannable = textView.getText() as Spannable val span = CustomTypefaceSpan(span) spannable.setSpan(span, ...) span.setTypeface(anotherTypeface) textView.requestLayout()
autoLink
TextView . autoLink
. autoLink=”web”
TextView URL URLSpan
. , SDK setText()
:
spannable = new SpannableString(string); Matcher m = pattern.matcher(text); while (...) {
UI , autoLink=”web”
RecyclerView. . LinkifyCompat
:
autoLink
map
– ( all
). . , WebView, ! SDK Linkify.gatherMapLinks()
, :
while ((address = WebView.findAddress(string)) != null) { ... }
WebView TODO SDK:
public static String findAddress(String addr) {
? Smart Linkify, Android P (28) , , . :
Linkify, . toolbar , Google .
Smart Linkify : , .
Magnifier
Android P (28) , – Magnifier, . .
TextView, EditText WebView, : API .
结论
Android , , :
- , Google I/O'19 “Best Practices for Using Text in Android” .
有用的链接
报告书