TextView
is the vital widget in Android. It supports tons of capabilities related to a text and also other things like compound drawables, graphical backgrounds, font auto-sizing, break strategies, ellipsize, spannables, etc
TextView
是Android中至关重要的小部件。 它支持与文本相关的大量功能,还支持其他功能,例如复合画图,图形背景,字体自动调整大小,中断策略,椭圆尺寸,跨度等
But the major feature of the TextView
is to render a text itself and seems this feature works sometimes badly or to be more accurate a bit clumsy. The case happens when a TextView
needs to render some long text which takes more than one line.
但是TextView
的主要功能 是为了渲染文本本身,似乎此功能有时效果很差,或者更准确一些。 当TextView
发生这种情况 需要呈现一些长文本,该文本需要多于一行。
为什么会这样? 为什么会出现这种怪异的差距? (Why does this happen? Why such a weird gap appears?)
In short, the TextView
thinks: well, there is not enough the maximum allowed by parent width to render the entire text so I will be maximally wide and multi-line. And because for the “Incomprehensibilities” word, there is no room on the first line it goes to the second line leaving quite huge gap on the right side.
简而言之, TextView
认为:很好,父级宽度所允许的最大值不足以呈现整个文本,因此我将最大程度地变宽和多行。 而且因为“ 不可理解性”一词在第一行没有空间,所以它转到第二行,在右侧留有相当大的空隙。
有一天,设计人员提供了要实现的原型: (One day a designer provides a prototype to be implemented:)
- an icon should precede a text 图标应位于文本之前
- the text should be aligned to view start 文本应对齐以查看开始
- entire construction should be centered on the screen 整个结构应在屏幕上居中
But because TextView is multiline and takes maximum allowed by parent width the result will be like this:
但是由于TextView是多行的,并且采用了父级宽度允许的最大值,因此结果将如下所示:
Designer is not satisfied, QAs report bugs. Developer tries to fix it but no luck. Making the text centered is also looking badly.
设计人员不满意,QA报告错误。 开发人员尝试修复它,但是没有运气。 使文本居中也看起来很糟糕。
Google and StackOverflow help to find people who suffer due to the same issue but there is no robust solution so far.
Google和StackOverflow可以帮助您找到因同一问题而受苦的人,但到目前为止还没有可靠的解决方案。
让我们深入研究TextView内部 (Let’s dive into TextView internals)
The decision to be maximally wide was wrong and the reason why a TextView
makes such a decision is android.text.Layout
. The subclasses of this class are used internally in the TextView
and actually not TextView
itself but Layout
is responsible for text-related features. And actually, TextView
delegates drawing on the canvas to Layout
. When TextView
measures itself it takes into consideration the width provided by Layout
. Layout
has methods: getWidth()
returns the whole width which includes a useless gap whereas getLineWidth(int line)
returns specified line width. So during TextView measuring instead of relying on getWidth()
better to define the largest line width using getLineWidth(int line)
and rely on it.
最大宽度的决定是错误的,也是TextView
的原因 做出这样的决定是android.text.Layout
。 此类的子类在TextView
内部使用 而不是TextView
本身,但Layout
负责与文本相关的功能。 实际上, TextView
将画布上的绘图委托给Layout
。 当TextView
测量自己时,它会考虑Layout
提供的宽度。 Layout
有方法: getWidth()
返回整个宽度,其中包括无用的间隙,而getLineWidth(int line)
返回指定的线宽。 因此在TextView测量期间,而不是依赖于getWidth()
最好使用getLineWidth(int line)
定义最大的线宽 并依靠它。
该写代码了 (It’s time to code)
Let’s extend AppCompatTextView
(to be compatible :) ) and override onMeasure
:
让我们扩展AppCompatTextView
(以使其兼容:))并覆盖onMeasure
:
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (layout == null || layout.lineCount < 2) return
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
val uselessPaddingWidth = layout.width - maxLineWidth
val width = measuredWidth - uselessPaddingWidth
val height = measuredHeight
setMeasuredDimension(width, height)
}
private fun getMaxLineWidth(layout: Layout): Float {
return (0 until layout.lineCount)
.map { layout.getLineWidth(it) }
.max()
?: 0.0f
}
We also should keep in mind that text may also be right-aligned, centered or RTL at all. So it not enough just to override onMeasure
but it is also needed to affect the drawing process.
我们还应该记住,文本也可能全部右对齐,居中或RTL 。 因此仅覆盖onMeasure
还不够 但也需要影响绘制过程。
Because the TextView
in order to support all it’s features has pretty long and complex onDraw
method, I decided that it will take too much time to write my own onDraw
with desired alterations. But all ingenious is simple! I came up with an idea to pass shifted canvas to the original onDraw
method. Because I am able to control the shift I am able to control the text drawing start coordinate. Only one thing is important here is just to calculate an appropriate shifting depending on the text alignment.
因为TextView
为了支持其所有功能,所以onDraw
相当长且复杂 方法,我决定用所需的更改来编写自己的onDraw
将花费太多时间。 但是所有的创意都很简单 ! 我想出了一个方法,将平移后的画布传递给原始的onDraw
方法。 因为我能够控制平移,所以我能够控制文本绘图的起始坐标。 这里只有一件事很重要,那就是根据文本对齐方式计算适当的偏移量。
该想法的说明性示例(请单击示例): (Illustrative examples of the idea (do clicks on the examples):)
Using such a workaround we loose compound drawables drawing, background drawing features (they will work but everything will be shifted so those capabilities are unusable) although actually it is not a problem. We could achieve the same result by wrapping an ImageView
and extended TextView
into some ViewGroup
(as most of android beginners do :).
使用这种解决方法,我们可以放松复合可绘制图形,背景图形的功能(它们可以工作,但是所有功能都会改变,因此这些功能将无法使用),尽管实际上这不是问题。 我们可以通过将ImageView
和TextView
封装到某些ViewGroup
来实现相同的结果(就像大多数android初学者一样:)。
关于文字对齐方式,LTR,RTL和Canvas的几句话 (Few words about text alignments, LTR, RTL and Canvas)
Canvas’ 0;0 coordinate is always on the top-left side. So even RTL texts get drawn from left to right from the Canvas point of view. That’s why we can summarize that for us it does not matter whether a text is RTL or LTR. For us is important whether it is left-aligned or centered or right-aligned and depending on this make appropriate canvas shifting before drawing. For mixed alignment, I decided not to do anything. So I introduced an enum:
画布的0; 0坐标始终位于左上方。 因此,从Canvas的角度来看,甚至RTL文本也从左到右绘制。 这就是为什么我们可以为我们总结一下,无论文本是RTL还是LTR都不重要。 对我们来说,重要的是它是左对齐还是居中还是右对齐,并根据此在绘制之前进行适当的画布移动。 对于混合对齐,我决定不做任何事情。 所以我介绍了一个枚举:
private enum class ExplicitLayoutAlignment {
LEFT, CENTER, RIGHT, MIXED
}
… and wrote a function that determines the layout explicit alignment.
…并编写了一个确定布局显式对齐的函数。
private fun Layout.getExplicitAlignment(): ExplicitLayoutAlignment {
...
}
一切准备就绪,可随时绘制! (Everything is ready to be drawn!)
I did override onDraw
and… Here came up with another issue to fight with.
我确实重写了onDraw
然后…这里提出了另一个要解决的问题。
很高兴认识您Canvas.clipRect()! (Nice to meet you Canvas.clipRect()!)
This method defines the boundaries within which the drawing will be done. Everything outside the bounds will not be drawn. The sad here is that inside the original onDraw
the TextView
sets Canvas.clipRect()
so even if I’d set Canvas.clipRect()
before calling the super.onDraw()
it would not bring any effect. So I dived into original onDraw
and explored what affects the calculation of the Canvas.clipRect()
arguments and… Compound drawables and their paddings may help! Anyway, the solution does not support compound drawable feature. By controlling the compoundPaddingRight
I control the clipRect width! That’s exactly what I need! So I override getCompoundPaddingRight
and in order not repeat myself I introduced drawTranslatedHorizontally()
function:
此方法定义了将在其中完成绘制的边界。 超出边界的所有内容均不会绘制。 这里的难过的是,原来里面onDraw
的TextView
设置Canvas.clipRect()
所以即使我设置Canvas.clipRect()
调用之前super.onDraw()
它不会带来任何影响。 因此,我深入研究了原始的onDraw
并探讨了哪些因素会影响Canvas.clipRect()
的计算 参数和…复合可绘制对象及其填充可能会有所帮助! 无论如何,该解决方案不支持复合可绘制功能。 通过控制compoundPaddingRight
我控制clipRect的宽度! 那正是我所需要的! 所以我重写了getCompoundPaddingRight
为了不重复自己,我介绍了drawTranslatedHorizontally()
功能:
private var extraPaddingRight: Int? = nullprivate fun drawTranslatedHorizontally(
canvas: Canvas,
xTranslation: Int,
drawingAction: (Canvas) -> Unit
) {
extraPaddingRight = xTranslation
canvas.save()
canvas.translate(xTranslation.toFloat(), 0f)
drawingAction.invoke(canvas)
extraPaddingRight = null
canvas.restore()
}override fun getCompoundPaddingRight(): Int {
return extraPaddingRight ?: super.getCompoundPaddingRight()
}
最后onDraw! (And finally onDraw!)
override fun onDraw(canvas: Canvas) {
if (layout == null || layout.lineCount < 2) return super.onDraw(canvas)
val explicitLayoutAlignment = layout.getExplicitAlignment()
if (explicitLayoutAlignment == ExplicitLayoutAlignment.MIXED) return super.onDraw(canvas)
val layoutWidth = layout.width
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
if (layoutWidth == maxLineWidth) return super.onDraw(canvas)
when (explicitLayoutAlignment) {
ExplicitLayoutAlignment.RIGHT -> {
drawTranslatedHorizontally(
canvas,
-1 * (layoutWidth - maxLineWidth)
) { super.onDraw(it) }
return
}
ExplicitLayoutAlignment.CENTER -> {
drawTranslatedHorizontally(
canvas,
-1 * (layoutWidth - maxLineWidth) / 2
) { super.onDraw(it) }
return
}
else -> return super.onDraw(canvas)
}
}
As you see I do not shift canvas before drawing in the following cases:
如您所见,在以下情况下,在绘制之前我不会移动画布:
- text is single-line 文字为单行
if the largest line width equal to
Layout
width如果最大线宽等于
Layout
宽度- if there are lines with different alignment 如果有对齐方式不同的线
- if the text is left-aligned 如果文字左对齐
And that’s actually it!
就是这样!
摘要 (Summary)
The solution is pretty simple and looks like it does not depend on the appcompat/androidx library version. The entire glist is here.
该解决方案非常简单,看起来不依赖于appcompat / androidx库版本。 整个名单都在这里 。
In our Plus500 android team, we develop our app supporting more than 30 languages including RTL languages like Hebrew and Arabic, supporting any screen sizes, tablets, and accessibility features like increased system UI scale and system font scale. That’s why each and every TextView
is considered as potentially short and at the same time as potentially extremely long so they may take much more space than we expect. The solution in our case is really essential because there are lots of cases when the layout is built around texts and accurate TextView
measurements are required.
在我们的Plus500安卓团队中,我们开发了支持30多种语言(包括RTL语言,如希伯来语和阿拉伯语)的应用程序,支持任何屏幕尺寸,平板电脑和辅助功能,例如增加的系统UI比例和系统字体比例。 这就是每个TextView
的原因 被认为可能很短,而同时又可能很长,因此它们占用的空间可能比我们预期的大得多。 在我们的案例中,该解决方案非常重要,因为在很多情况下,布局都是围绕文本构建的,因此需要精确的TextView
测量。
I hope I saved somebody’s time and the implementation and article were useful and interesting. If you have any other ideas to improve the implementation or to achieve the same behavior without introducing a custom view welcome to share your thoughts. Cheers!
我希望我可以节省一些时间,并且实现和文章非常有用且有趣。 如果您有其他想法可以改进实现或实现相同的行为而又不引入自定义视图,欢迎您分享您的想法。 干杯!
翻译自: https://medium.com/@mxdiland/android-textview-multiline-problem-61f8c3499bbb