在Android的TextView中真正实现文本顶部对齐

4

我正在尝试在安卓系统中显示一个TextView,使得视图中的文本顶部对齐:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Create container layout
    FrameLayout layout = new FrameLayout(this);

    // Create text label
    TextView label = new TextView(this);
    label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 25);     // 25 pixels tall
    label.setGravity(Gravity.TOP + Gravity.CENTER);        // Align text top-center
    label.setPadding(0, 0, 0, 0);                          // No padding
    Rect bounds = new Rect();
    label.getPaint().getTextBounds("gdyl!", 0, 5, bounds); // Measure height
    label.setText("good day, world! "+bounds.top+" to "+bounds.bottom);
    label.setTextColor      (0xFF000000);                  // Black text
    label.setBackgroundColor(0xFF00FFFF);                  // Blue background

    // Position text label
    FrameLayout.LayoutParams layoutParams = 
            new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
                                                            // also 25 pixels tall
    layoutParams.setMargins(50, 50, 0, 0);
    label.setLayoutParams(layoutParams);

    // Compose screen
    layout.addView(label);
    setContentView(layout);
}

这段代码输出以下图片:

enter image description here

需要注意的事项:

  • 蓝色框的高度是25像素,正如所请求的一样
  • 文本边界也被报告为25像素,就像所请求的一样(6-(-19)= 25)
  • 文本不从标签顶部开始,但以上方留有一些填充,忽略了setPadding()
  • 这导致文本在底部被剪切,即使框实际上已经足够高

我怎样才能让TextView在框的最上方开始呈现文本?

对于可能的答案,我有两个限制:

  • 我确实需要将文本与顶部对齐,因此如果有一些通过底部对齐或将其垂直居中的技巧,我无法使用它,因为我有一些情况下TextView比所需的高。
  • 我有点强迫症,所以如果可能的话,我想坚持使用早期的Android API调用(最好是1,但绝对不高于7)。

我建议你看一下这个链接:https://dev59.com/5msz5IYBdhLWcg3w9soQ。我认为你的textSize是25像素,但是你的textView高度超过了25(我尝试使用32像素,它可以完美地居中整个布局)。 - Sinan Kozak
2个回答

11

TextViews使用抽象类android.text.Layout在画布上绘制文本:

canvas.drawText(buf, start, end, x, lbaseline, paint);

垂直偏移量lbaseline的计算方式为行底部减去字体下沉:

int lbottom = getLineTop(i+1);
int lbaseline = lbottom - getLineDescent(i);

两个名为getLineTopgetLineDescent的调用函数是抽象的,但可以在BoringLayout中找到一个简单的实现(自己想象... :),它只返回mBottommDesc的值。这些值通过其init方法计算如下:
if (includepad) {
    spacing = metrics.bottom - metrics.top;
} else {
    spacing = metrics.descent - metrics.ascent;
}

if (spacingmult != 1 || spacingadd != 0) {
    spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
}

mBottom = spacing;

if (includepad) {
    mDesc = spacing + metrics.top;
} else {
    mDesc = spacing + metrics.ascent;
}

这里,“includepad”是一个布尔值,用于指定文本是否应包含额外的填充以容纳超出指定上升高度的字形。它可以通过TextView的setIncludeFontPadding方法进行设置(正如@ggc所指出的)。
如果将includepad设置为true(默认值),则文本的位置是根据字体度量的top-field确定的基线。否则,文本的基线来自descent-field
因此,从技术上讲,这意味着我们只需要关闭IncludeFontPadding,但不幸的是,这会产生以下结果:

enter image description here

由于字体报告其上升值为-23.2,而边界框报告其顶部值为-19。我不知道这是字体中的“错误”还是它本应如此。不幸的是,即使您尝试以某种方式将报告的屏幕分辨率240dpi与72dpi的字体点定义相结合,FontMetrics也没有提供任何与边界框报告的19相匹配的值,因此没有“官方”方法来解决此问题。
但是,当然可以利用可用信息来破解解决方案。有两种方法可以做到这一点:
  • with IncludeFontPadding left alone, i.e. set to true:

    double top = label.getPaint().getFontMetrics().top;
    label.setPadding(0, (int) (top - bounds.top - 0.5), 0, 0);
    

    i.e. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's top-value. Result:

    enter image description here

  • with IncludeFontPadding set to false:

    double ascent = label.getPaint().getFontMetrics().ascent;
    label.setPadding(0, (int) (ascent - bounds.top - 0.5), 0, 0);
    label.setIncludeFontPadding(false);
    

    i.e. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's ascent-value. Result:

    enter image description here

请注意,将IncludeFontPadding设置为false并没有什么神奇之处。两个版本都应该可以工作。它们产生不同结果的原因是字体度量的浮点值转换为整数时略有不同的舍入误差。恰好在这种特殊情况下,使用IncludeFontPadding设置为false看起来更好,但对于不同的字体或字号,情况可能不同。很可能很容易调整上部填充的计算方法,以产生与BoringLayout使用的计算相同的精确舍入误差。我还没有这样做,因为我宁愿使用“无错误”的字体,但如果我找到时间,我可能会稍后添加它。然后,IncludeFontPadding设置为false或true就真正无关紧要了。

1
@androiddeveloper 我猜你需要重写setTextSize方法,并让它在新的大小上快速执行getTextBounds,以获取最大的顶部边界,然后根据此设置填充。并确保至少调用一次此setTextSize方法(也许在一个新的构造函数中)。 - Markus A.
1
顺便说一下:如果你这样做,会有一些字母(可能是中文字符之类的)无法适应你的TextView,并且会被切掉顶部(也许还有底部)!所以请确保你用于测试结果的所有字母都是你计划在应用程序中使用的,这可能比你最初想象的要多得多,特别是如果你为用户提供编辑字段。例如,有些人肯定会在他们最喜欢的角色名称中使用Umlauts或类似的字符。 - Markus A.
1
@androiddeveloper 这里存在两个问题:一方面,字体度量的上升和下降应该描述标准字符超出和低于基线的程度。但由于某种原因,这似乎与我获取的普通字符的边界框不匹配。对我来说,这似乎是字体(或Android文本库)中的“错误”,因为我希望 |ascent| + |descent| 的和能够等于所指定的字体大小,但它并不相等。另一方面,字体指定了一个顶部和底部,这使得一些字符有更多的空间。 - Markus A.
1
@androiddeveloper 我个人会尝试寻找一个没有这个问题的字体,如果我必须使用有这个问题的字体,我会确保检查所有我期望使用的字符,以确保它们不会被裁剪。但是,老实说,最简单的解决方案可能是将您的TextView向上移动20像素,使其高度增加40像素,并在顶部填充中添加20像素。这样,您仍然可以可靠地定位文本,而且不会出现任何裁剪问题。唯一无法做到的是为标签添加背景颜色... - Markus A.
1
@androiddeveloper 另外,它可能会增加标签的区域,以捕获触摸事件...顺便说一句:另一个问题:如果您在文本上使用阴影层来制作阴影或模糊发光效果,则阴影也需要适合 TextView 的尺寸以避免剪切! - Markus A.
显示剩余7条评论

1
如果您的TextView在其他布局中,请确保它们之间有足够的空间。您可以在父视图底部添加填充来查看是否可以显示完整文本。对我有用!
例如:您在FrameLayout中有一个textView,但FrameLayout太小了,无法完全显示textView。添加填充到FrameLayout,看看是否有效。
编辑:更改此行
     FrameLayout.LayoutParams layoutParams = 
            new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);

对于这行代码

    FrameLayout.LayoutParams layoutParams = 
            new FrameLayout.LayoutParams(300, 50, Gravity.LEFT + Gravity.TOP);

这将使盒子变大,并且同样让足够的空间显示您的文本。
或者添加这一行。
     label.setIncludeFontPadding(false);

这将移除周围的字体填充并使文本可见。但是,在您的情况下不起作用的唯一事情是它不会完全显示像“g”这样的字母,因为它们会在下方...也许您需要稍微更改框的大小或文本的大小(例如增加2-3),如果您真的想让它起作用。

不确定我是否理解你的回答...正如您所看到的,该标签没有被任何其他元素遮挡,因为我设置了其完整高度为25像素...只是它将文本定位在其中的方式使其无法适应标签内部。(请参见示例代码) - Markus A.
但是你至少尝试过我的答案吗?我告诉你它对我有效,试一试不会有任何损失。 - ggc
当然,我可以只是把标签变大,但这只解决了这个特定的情况,需要增加多少大小将取决于确切的字体、大小等。因此,虽然您的答案肯定会修复我发布的示例,但我希望找到一种方法,通过消除其上方的填充来实际适应文本所需的尺寸。例如,您的解决方案将使蓝色框变大,这不是我在设计中想要的。 - Markus A.
好的,我终于明白你要什么了。你需要添加这个:label.setIncludeFontPadding(false); 这将去除周围的字体填充并让文本可见。但是在你的情况下唯一不起作用的是它不能完全显示像“g”这样在行下面的字母...好吧,看看能否解决这个问题! - ggc
这似乎回答了问题,请考虑将其标记为已回答。 - ggc
不幸的是,那也不能可靠地工作。我花了一些时间来想出一个解决方案...你可以在下面看到它。但还是谢谢你的帮助。+1 - Markus A.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接