具有正确宽度的Android多行TextView

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发生这种情况 需要呈现一些长文本,该文本需要多于一行。

I added green background to demonstrate the real dimensions of the TextView
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

    整个结构应在屏幕上居中
Image for post

But because TextView is multiline and takes maximum allowed by parent width the result will be like this:

但是由于TextView是多行的,并且采用了父级宽度允许的最大值,因此结果将如下所示:

Image for post

Designer is not satisfied, QAs report bugs. Developer tries to fix it but no luck. Making the text centered is also looking badly.

设计人员不满意,QA报告错误。 开发人员尝试修复它,但是没有运气。 使文本居中也看起来很糟糕。

Image for post

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):)

Left aligned text
左对齐文字
Right aligned text
右对齐文字
Centered text
居中文字

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 :).

使用这种解决方法,我们可以放松复合可绘制图形,背景图形的功能(它们可以工作,但是所有功能都会改变,因此这些功能将无法使用),尽管实际上这不是问题。 我们可以通过将ImageViewTextView封装到某些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然后…这里提出了另一个要解决的问题。

很高兴认识您C​​anvas.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:

此方法定义了将在其中完成绘制的边界。 超出边界的所有内容均不会绘制。 这里的难过的是,原来里面onDrawTextView设置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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值