51

Target: Android >= 1.6 on a pure Canvas.

Suppose I want to write a function that will draw a (width, height) large red rectangle and then draw a black Hello World text inside. I want the text to be visually in the center of the rectangle. So let's try:

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);
}

Now I don't know what to put in drawText's argument marked by ????, i.e. I don't know how to vertically align the text.

Something like

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

appears to work more or less ok, but there must be a better way.

weston
  • 54,145
  • 21
  • 145
  • 203
Leszek
  • 1,181
  • 1
  • 10
  • 21

9 Answers9

124

Example to centre on cx and cy:

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);
}

Why doesn't height()/2f work the same?

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

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

These would only yield the same result when top is 0. This may be the case for some fonts at all sizes, or other fonts at some sizes, but not for all fonts at all sizes.

weston
  • 54,145
  • 21
  • 145
  • 203
  • 1
    This should be the accepted answer. Not only it's the simplest one of them all, it's the only one that really, optically centers the text vertically, properly taking ascents and descents into account (not generally but the actual ones present in the actual text to be displayed). – Gábor Oct 10 '14 at 10:22
  • Been through a lot of hoops and loops to crack this - and then its as simple as this. This is _the_ answer you are looking for! – slott Nov 18 '14 at 11:50
  • Can someone tell me why textBounds.height() / 2 doesn't work as expected? (At least not for me.) I found this answer and replaced height() with centerY() and now it works... – bwoogie Apr 13 '15 at 20:34
  • 3
    1. First the code does not take into account the alignment of the text (for example it will not work if align is CENTER) 2. As this method simply translate the center of the text's bounds when we actually paint the text it will not fit the bounds (not considering ascent and descent of the text) – Alex Nov 30 '15 at 14:22
  • @Alex it does what it does, I'm sorry if it doesn't fit your requirements. "when we actually paint the text it will not fit the bounds" it fits the bounds, we just moved the centre. – weston Nov 30 '15 at 16:02
  • @weston If you take the initial bounds of the text and translate them by the computed x and y then the updated bounds will not coincide with the actual bounds of the painted text. – Alex Nov 30 '15 at 16:10
  • @Alex this is code for finding the position to draw some text so that it's centre is the centre you specify. I'm not sure what you are after. It doesn't update the bounds by the way, it draws the text at an offset. – weston Nov 30 '15 at 16:12
  • @weston You're right. I was thinking about centering based on font metrics instead of actual text (for example in cases where you want to align several separate strings so their baseline coincide - kinda find the center of the font instead of actual text) but for a single string your idea works nice. – Alex Nov 30 '15 at 16:55
  • @alex's first point is valid, particularly because this question is about aligning text vertically only. Better to use cx directly (don't use exactCenterX) and use the built in Paint.setTextAlign(Align) options directly. – Nick Sep 19 '16 at 01:55
  • @Nick I don't follow. Perhaps you should provide your own answer, this one isn't changing. – weston Mar 22 '17 at 12:18
  • make sure you don't **also** set paint.setTextAlign(Align.Center) do only the approach described above and it will work – Peter Mitrano Jul 11 '17 at 22:58
  • This doesn't account for the text baseline and ascent. For example, if you render "ay", it will centre relative to the "y", which looks weird. Also, the text centre isn't consistent, depending on the text itself, so if you compare "Aa" with "aa", the latter will come out slightly lower. – Sam Apr 13 '19 at 21:43
  • @Sam You're not the first to point that out. It does what it does and it never promised to do anything special with the baseline. If that doesn't fit your requirements there are plenty of other answers here. But this one is not changing. – weston Apr 13 '19 at 21:58
  • Sorry for the silly question. But what is mean by cx and cy here? – Sundaravel Apr 18 '21 at 10:58
  • @Sundaravel they are the coordinates you want to center the text on. Center x and center y. – weston Apr 26 '21 at 00:09
31
textY = topLeftY + height/2 - (mPaint.descent() + mPaint.ascent()) / 2

The distance from "baseline" to "center" should be -(mPaint.descent() + mPaint.ascent()) / 2

AVEbrahimi
  • 17,993
  • 23
  • 107
  • 210
QQchurch
  • 319
  • 3
  • 2
  • This didn't work for me but the accepted answer worked perfectly. Maybe the result this approach looks better if the font is smaller. – spaaarky21 Dec 22 '17 at 22:36
  • 1
    I tried a few techniques in this question and found yours is the best overall. Unlike the accepted answer, this one provides consistent results regardless of what your text is. And also unlike the accepted answer, yours centres relative to a capital letter's mid-point, which I think is what most people would expect. – Sam Apr 13 '19 at 21:46
23

Based on steelbytes' response, the updated code would look something like:

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);
}
WarrenFaith
  • 57,492
  • 25
  • 134
  • 150
gauravjain0102
  • 257
  • 2
  • 3
17

Since drawing text at Y means that the baseline of the text will end up Y pixels down from the origin, what you need to do when you want to center text within a rectangle of (width, height) dimensions is:

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

Keep in mind that the ascent is negative (which explains the minus sign).

This does not take the descent into account, which is usually what you want (the ascent is generally the height of caps above the baseline).

Pierre-Luc Paour
  • 1,725
  • 17
  • 21
  • I just tried this answer but found that the text wasn't centred relative to capital letters like you suggested. Instead, I had to also use [`descent()`](https://stackoverflow.com/a/10255500/238753) to get this behaviour. – Sam Apr 13 '19 at 21:44
9

using mPaint.getTextBounds() you can ask how big the text will be when drawn, then using that info you can calc where you want to draw it.

Dalmas
  • 26,409
  • 9
  • 67
  • 80
SteelBytes
  • 6,905
  • 1
  • 26
  • 28
  • 2
    getTextBounds() belongs to Paint, not Canvas – Melllvar Jun 12 '11 at 00:59
  • 4
    Remember that when you say vertically centered text you probably mean around the ascending portion of the character, not the baseline. When you draw using Align.CENTER the Y-coordinate will center on the character baseline. – Cameron Lowell Palmer Apr 22 '12 at 18:38
  • @CameronLowellPalmer Could you elaborate a bit more on that? – Peterdk Jun 15 '13 at 19:31
  • 1
    @Peterdk I recommend you try this for yourself, but what you are measuring is the bounds of drawn text. That means if the text descends below the baseline and you are lining up several, separately drawn words, you are going to have text that doesn't line up the way you expect.The word 'grow' descends below the baseline, the word 'hi' does not. So if you find the center of the TextBounds of those two words and then divide by 2 to find the height you will get an undesirable result. – Cameron Lowell Palmer Jun 20 '13 at 21:55
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);
}

Usage:

PointF p = getTextCenterToDraw(text, rect, paint);
canvas.drawText(text, p.x, p.y, paint);
Exterminator13
  • 2,152
  • 1
  • 23
  • 28
1

I stumbled on this question when trying to solve my issue, and @Weston's answer works fine with me.

In case of 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);
}
N. Osil
  • 494
  • 7
  • 13
1

Here is a SkiaSharp C# extension method for anyone looking for it

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);
}
Benoit Jadinon
  • 1,082
  • 11
  • 21
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);
}
Emre Hamurcu
  • 111
  • 1
  • 6
  • Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Tyler2P Mar 10 '21 at 09:05