Centering Single-Line Text in a Canvas

Suppose I want a custom View that draws a circle, then a number centered inside of it. Your first attempt will probably look something like this:

The problem is that while you can easily set a horizontal alignment for your TextPaint (via Paint.Align), the vertical alignment is tricky. That's because Canvas.drawText() starts drawing at the baseline of your set Y-coordinate, instead of the center.

If you only knew the height of the text, then you could center it yourself - but getting the height is tricky! TextPaint.getTextBounds() doesn't work quite right because it gives you the minimal bounding rectangle, not the height that the TextPaint draws. For example, if your text has no ascenders/descenders, then the measured height is smaller than it will draw (since it will still account for the possibility of them).

The way I've found to get the height of the TextPaint is to use ascent() and descent(). These measure the size above/below the text's baseline. Combined, they add up to the total height of the drawn text. You can then use some math to center the draw on the baseline - here's a version of onDraw() that does it correctly*:

protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);

  Paint paint = new Paint();
  paint.setColor(Color.BLACK);

  TextPaint textPaint = new TextPaint();
  textPaint.setColor(Color.WHITE);
  textPaint.setTextAlign(Paint.Align.CENTER);
  float textHeight = textPaint.descent() - textPaint.ascent();
  float textOffset = (textHeight / 2) - textPaint.descent();

  RectF bounds = new RectF(0, 0, getWidth(), getHeight());
  canvas.drawOval(bounds, paint);
  canvas.drawText("42", bounds.centerX(), bounds.centerY() + textOffset, textPaint);
}

And the finished product is here:

Note that all advice in this post is about a single line of text. If you're handling multi-line text then the solution is more complex because you have to handle how many lines the text will render onto. If I ever try to tackle that I'll write that up as well.


* In a real-world example, you wouldn't want to instantiate your Paints in onDraw(); this is done for brevity's sake.