如何垂直对齐文本?

51

目标:在纯画布上运行的 Android >= 1.6。

假设我想编写一个函数,它将绘制一个(宽度,高度)的大红色矩形,然后在内部绘制黑色的Hello World 文字。我希望文本在矩形的视觉中心。那么让我们尝试:

void drawHelloRectangle(Canvas c, int topLeftX, 
        int topLeftY, int width, int height) {
    Paint mPaint = new Paint();
    // height of 'Hello World'; height*0.7 looks good
    int fontHeight = (int)(height*0.7);

    mPaint.setColor(COLOR_RED);
    mPaint.setStyle(Style.FILL);
    c.drawRect( topLeftX, topLeftY, topLeftX+width, topLeftY+height, mPaint);

    mPaint.setTextSize(fontHeight);
    mPaint.setColor(COLOR_BLACK);
    mPaint.setTextAlign(Align.CENTER);
    c.drawText( "Hello World", topLeftX+width/2, ????, mPaint);
}

现在我不知道应该在标有????的drawText参数中放些什么,也就是说,我不知道如何垂直对齐文本。

类似这样的写法

???? = topLeftY + height/2 + fontHeight/2 - fontHeight/8;

看起来大致上可以工作,但肯定有更好的方法。


21
每当你在问题标题中提到 Android,一只小猫就会死亡,请不要这样做。谢谢。 - Octavian Helm
1
9年过去了,这个问题和被采纳的答案仍然具有相关性。 - 0101100101
9个回答

124

将图形居中在cxcy上的示例:

private final Rect textBounds = new Rect(); //don't new this up in a draw method

public void drawTextCentred(Canvas canvas, Paint paint, String text, float cx, float cy){
  paint.getTextBounds(text, 0, text.length(), textBounds);
  canvas.drawText(text, cx - textBounds.exactCenterX(), cy - textBounds.exactCenterY(), paint);
}

为什么height()/2f不能起到与exactCentre()相同的作用?

exactCentre() = (top + bottom) / 2f

height()/2f = (bottom - top) / 2f

只有当top0时,这两个公式才能得出相同的结果。对于某些字体的所有大小或其他字体的某些大小,可能会出现这种情况,但并非适用于所有字体的所有大小。


1
这应该是被接受的答案。它不仅是所有答案中最简单的一个,而且它是唯一一个真正垂直居中文本的方法,正确地考虑到升高和降低(不是一般性地,而是实际文本中存在的)。 - Gábor
有人能告诉我为什么textBounds.height() / 2的效果与预期不同吗?(至少对我来说是这样。)我发现了这个答案,用centerY()替换了height(),现在它就有效了... - bwoogie
3
  1. 首先,该代码没有考虑文本的对齐方式(例如,如果对齐方式为中心,则不起作用)。
  2. 由于此方法仅翻译文本边界的中心位置,当我们实际绘制文本时,它将不适合边界(未考虑文本的上升和下降)。
- Alex
只需按照上述描述的方法操作,不要同时设置paint.setTextAlign(Align.Center),这样就可以正常工作了。 - Peter Mitrano
抱歉问一个愚蠢的问题。这里的cx和cy是什么意思? - Sundaravel
显示剩余10条评论

31
textY = topLeftY + height/2 - (mPaint.descent() + mPaint.ascent()) / 2

从“基线”到“中心”的距离应为-(mPaint.descent() + mPaint.ascent()) / 2


这个对我没用,但是被采纳的答案完美地解决了问题。也许如果字体更小,这种方法的效果会更好。 - spaaarky21
1
我在这个问题中尝试了几种技术,发现你的总体来说是最好的。与被接受的答案不同,这个答案提供了始终如一的结果,而不管你的文本是什么。而且,与被接受的答案不同,你的答案以大写字母中点为中心,这是我认为大多数人会期望的。 - Sam

23

根据steelbytes的回答,更新后的代码将类似于:

void drawHelloRectangle(Canvas c, int topLeftX, int topLeftY, int width, int height) {
    Paint mPaint = new Paint();
    // height of 'Hello World'; height*0.7 looks good
    int fontHeight = (int)(height*0.7);

    mPaint.setColor(COLOR_RED);
    mPaint.setStyle(Style.FILL);
    c.drawRect( topLeftX, topLeftY, topLeftX+width, topLeftY+height, mPaint);

    mPaint.setTextSize(fontHeight);
    mPaint.setColor(COLOR_BLACK);
    mPaint.setTextAlign(Align.CENTER);
    String textToDraw = new String("Hello World");
    Rect bounds = new Rect();
    mPaint.getTextBounds(textToDraw, 0, textToDraw.length(), bounds);
    c.drawText(textToDraw, topLeftX+width/2, topLeftY+height/2+(bounds.bottom-bounds.top)/2, mPaint);
}

17

在Y位置绘制文本意味着文本基线将在距离原点向下Y像素的位置结束。当您想要在尺寸为(width, height)的矩形中居中文本时,需要执行以下操作:

paint.setTextAlign(Paint.Align.CENTER);  // centers horizontally
canvas.drawText(text, width / 2, (height - paint.ascent()) / 2, paint);

请记住,上升高度是负的(这解释了减号)。

这不考虑下降高度,而通常您想要的是考虑下降高度(上升高度通常是大写字母距离基线的高度)。


我刚试了一下这个答案,但发现文本并没有像你建议的那样相对于大写字母居中。相反,我还需要使用descent()来实现这种行为。 - Sam

9
使用mPaint.getTextBounds(),您可以查询绘制文本时文本的大小,然后使用该信息计算您想要绘制的位置。

2
getTextBounds() 属于 Paint,而不是 Canvas。 - Melllvar
4
记住,当你说垂直居中的文字时,你可能是指字符的上升部分周围,而不是基线。当你使用Align.CENTER绘制时,Y坐标将以字符基线为中心。 - Cameron Lowell Palmer
@CameronLowellPalmer,您能详细说明一下吗? - Peterdk
1
@Peterdk 我建议你自己尝试一下,但你正在测量的是绘制文本的边界。这意味着如果文本向下延伸到基线以下,并且你正在对几个单独绘制的单词进行排列,那么你将得到不符合预期的文本对齐方式。“grow”一词向下延伸到基线以下,“hi”一词则不会。因此,如果你找到这两个单词的TextBounds中心,然后除以2来找到高度,你将得到一个不理想的结果。 - Cameron Lowell Palmer

6
public static PointF getTextCenterToDraw(String text, RectF region, Paint paint) {
    Rect textBounds = new Rect();
    paint.getTextBounds(text, 0, text.length(), textBounds);
    float x = region.centerX() - textBounds.width() * 0.4f;
    float y = region.centerY() + textBounds.height() * 0.4f;
    return new PointF(x, y);
}

使用方法:

PointF p = getTextCenterToDraw(text, rect, paint);
canvas.drawText(text, p.x, p.y, paint);

1

当我试图解决我的问题时,我偶然发现了这个问题,@Weston的答案对我很有效。

在Kotlin的情况下:

private fun drawText(canvas: Canvas) {
    paint.textSize = 80f
    val text = "Hello!"
    val textBounds = Rect()
    paint.getTextBounds(text, 0, text.length, textBounds);
    canvas.drawText(text, cx- textBounds.exactCenterX(), cy - textBounds.exactCenterY(), paint);
    //in case of another Rect as a container:
    //canvas.drawText(text, containerRect.exactCenterX()- textBounds.exactCenterX(), containerRect.exactCenterY() - textBounds.exactCenterY(), paint);
}

1
这是一个SkiaSharp C#扩展方法,供寻找的人使用。
public static void DrawTextCenteredVertically(this SKCanvas canvas, string text, SKPaint paint, SKPoint point)
{
    var textY = point.Y + (((-paint.FontMetrics.Ascent + paint.FontMetrics.Descent) / 2) - paint.FontMetrics.Descent);
    canvas.DrawText(text, point.X, textY, paint);
}

0
private final Rect textBounds = new Rect();//No need o create again

public void drawTextCentred(Canvas canvas, Paint paint, String text, float cx, float cy, float desiredWidth) {
    final float testTextSize = 48f;
    paint.setTextSize(testTextSize);
    Rect bounds = new Rect();
    paint.getTextBounds(text, 0, text.length(), bounds);
    float desiredTextSize = testTextSize * desiredWidth / bounds.width();
    paint.setTextSize(desiredTextSize);
    paint.getTextBounds(text, 0, text.length(), textBounds);
    paint.setTextAlign(Paint.Align.CENTER);
    canvas.drawText(text, cx, cy - textBounds.exactCenterY(), paint);
}

请不要仅仅发布代码作为答案,还需要解释你的代码是如何解决问题的。带有解释的答案通常更有帮助、质量更高,并且更容易获得赞同。 - Tyler2P

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