64

I want to draw text on canvas of certain width using .drawtext

For example, the width of the text should always be 400px no matter what the input text is.

If input text is longer it will decrease the font size, if input text is shorter it will increase the font size accordingly.

Kara
  • 6,115
  • 16
  • 50
  • 57
Badal
  • 3,738
  • 4
  • 33
  • 60

3 Answers3

124

Here's a much more efficient method:

/**
 * Sets the text size for a Paint object so a given string of text will be a
 * given width.
 * 
 * @param paint
 *            the Paint to set the text size for
 * @param desiredWidth
 *            the desired width
 * @param text
 *            the text that should be that width
 */
private static void setTextSizeForWidth(Paint paint, float desiredWidth,
        String text) {

    // Pick a reasonably large value for the test. Larger values produce
    // more accurate results, but may cause problems with hardware
    // acceleration. But there are workarounds for that, too; refer to
    // http://stackoverflow.com/questions/6253528/font-size-too-large-to-fit-in-cache
    final float testTextSize = 48f;

    // Get the bounds of the text, using our testTextSize.
    paint.setTextSize(testTextSize);
    Rect bounds = new Rect();
    paint.getTextBounds(text, 0, text.length(), bounds);

    // Calculate the desired size as a proportion of our testTextSize.
    float desiredTextSize = testTextSize * desiredWidth / bounds.width();

    // Set the paint for that size.
    paint.setTextSize(desiredTextSize);
}

Then, all you need to do is setTextSizeForWidth(paint, 400, str); (400 being the example width in the question).

For even greater efficiency, you can make the Rect a static class member, saving it from being instantiated each time. However, this may introduce concurrency issues, and would arguably hinder code clarity.

Michael Scheper
  • 6,514
  • 7
  • 63
  • 76
  • 3
    Aw, shucks! :-) Glad it helped. – Michael Scheper Mar 03 '15 at 13:23
  • can I know from where you chose the 400? cuz I tried your method but some of my text becomes out of the canvas, like a letter or two. – Ahmed Ekri Mar 03 '15 at 19:52
  • I trried bitmap.getwidth(), it is better but still, it is not effecient. – Ahmed Ekri Mar 03 '15 at 19:52
  • 400 is the width in the Original Poster's example. You should set `desiredWidth` to the width of your canvas. If the result isn't accurate, try increasing the size of `testTextSize`. – Michael Scheper Mar 03 '15 at 20:43
  • is it just me or text size doesnt seem to make linear effect on the rect size... which makes this approach not work – Ofek Ron Jun 17 '16 at 12:09
  • @OfekRon: I'm not sure what you mean, but the size of the text will only be proportional to the text length if you're using a fixed-width font. – Michael Scheper Jun 17 '16 at 14:12
  • I think to prevent a text to be out of `Canvas` bounds it's better to `Math.floor` the `desiredTextSize`. In this case the text can have a smaller size than the desired one but, on the other hand, it will be fully visible. – Stan Mots Nov 10 '16 at 19:31
  • @StanMots: Thanks for the thought, but I'm pretty sure non-integer text sizes are valid. So assuming your `desiredWidth` and `text` are correct, I don't see how this code could result in text that's too large. Has your experience proven otherwise? – Michael Scheper Nov 11 '16 at 16:43
  • @MichaelScheper, no, I didn't experience such issue. That was mostly in response to the AhmedEkri comment. I just thought since the float values in some cases cannot be represented exactly in memory (see [link](https://www.securecoding.cert.org/confluence/display/java/NUM04-J.+Do+not+use+floating-point+numbers+if+precise+computation+is+required)) the computation of the text size could produce a slightly incorrect result. – Stan Mots Nov 11 '16 at 18:36
  • @StanMots: Since users can't really see fractions of pixels, using floats in this case is fine. The problem AhmedEkri was having was that my original answer didn't make it clear that the desiredWidth value of 400 came from the question. We've cleared that up, though. Thanks. – Michael Scheper Nov 16 '16 at 01:35
  • This is great, but isn't `static` a bit unnecessary? – Chisko May 31 '18 at 19:23
  • @Chisko: It is, if you don't care about how much memory your program uses. But if you're serious about software engineering, I recommend you read Bloch's _Effective Java_ – Michael Scheper Jul 18 '18 at 20:17
  • @MichaelScheper I've had serious issues with Java since I first started studying it back in 2003. I can see why it is effective to be static (doesn't modify state, right?) but what I've always struggled is: who else is going to use this method if not the class, because of encapsulation? Some specific light on the issue from you would be nice :) – Chisko Jul 19 '18 at 03:24
  • 1
    @Chisko: Sure, and since I've been coding Java since 1997, I guess it's appropriate. ☺ When a method is `static`, there'll only ever be one copy of it per JVM, no matter how many instances of the class there are. But for non-static methods, there's one copy per instance of the class, so you can have multiple copies of the local variables, and I believe the bytecode itself as well. I admit this may be rather academic in your Android app, if you're only creating one instance of whatever class you put this code in, but it's a good habit to get into. I highly recommend Bloch's book for more info. – Michael Scheper Jul 24 '18 at 14:58
  • 1
    To answer your question more directly: Who else is going to use that method? Other instances of the class. Let's say you code a widget and put `setTextSizeForWidth` in it, and your app has three such widgets. If your widget's class makes this method static, there'll be one copy. If not, there'll be three. The three copies may only use that method one at a time, but new memory for the two `float`s will be allocated each time, and not be freed until garbage collection. Again, minor in this case, but for larger collections of larger objects, it can quickly cause problems. – Michael Scheper Jul 24 '18 at 15:03
  • Great! Maybe could return the expected height of the text as well to help you draw it centered vertically if necessary. (desiredTextSize/testTextSize) * bounds.height() – Cal Aug 26 '22 at 04:37
29

Try this:

/**
 * Retrieve the maximum text size to fit in a given width.
 * @param str (String): Text to check for size.
 * @param maxWidth (float): Maximum allowed width.
 * @return (int): The desired text size.
 */
private int determineMaxTextSize(String str, float maxWidth)
{
    int size = 0;       
    Paint paint = new Paint();

    do {
        paint.setTextSize(++ size);
    } while(paint.measureText(str) < maxWidth);

    return size;
} //End getMaxTextSize()
Badal
  • 3,738
  • 4
  • 33
  • 60
slybloty
  • 6,346
  • 6
  • 49
  • 70
  • 1
    Just out of curiosity, why are you returning ``size / 2``? Guess there's a good reason I can't think of right now but ``size - 1`` made more sense to me :/ – harism Aug 28 '12 at 19:55
  • To tell you the truth I don't remember exactly. I know I tried with `-1` and think I didn't get what I wanted exactly. – slybloty Aug 28 '12 at 20:03
  • this doesn't work.. what i am doing is setting maxwidth to my phone's screen width that is 560px but when i set it it doesn't fit in the screen its very bigger than that. and font size is not changing accordingly. – Badal Aug 28 '12 at 20:05
  • Did you try with `size / 2`? And use `int screenWidth = this.getWindowManager().getDefaultDisplay().getWidth();` to get the screen size. – slybloty Aug 28 '12 at 20:08
  • i am getting screen size with that only. and i tried it with both `size/2` and `size`. i tried using 200 as maxwidth too still text is not fixed under 200px width – Badal Aug 28 '12 at 20:16
  • 2
    ohh this worked now i was passing diffrent string value and drawing diffrent one. my bad. and it worked with `size` not `size/2` please edit your answer. – Badal Aug 28 '12 at 20:29
  • 4
    There is a bug in this. You do not handle the case where str has no characters or only spaces. You iterate forever because of it. Add this as first line... `if (str == null || str.trim().length() == 0) return 0;` – MQS Dec 27 '13 at 05:32
  • The method suggested in this answer is very inefficient, and since it potentially gets called very frequently, it can make your app sluggish on some devices. It will potentially perform a lot better if you use this method instead: http://stackoverflow.com/questions/12166476/android-canvas-drawtext-set-font-size-from-width/21895626#21895626 – Michael Scheper Oct 25 '14 at 00:28
5

Michael Scheper's solution seems nice but it didn't work for me, I needed to get the largest text size that is possible to draw in my view but this approach depends on the first text size you set, Every time you set a different size you'll get different results that can not say it is the right answer in every situation.

So I tried another way:

private float calculateMaxTextSize(String text, Paint paint, int maxWidth, int maxHeight) {
    if (text == null || paint == null) return 0;
    Rect bound = new Rect();
    float size = 1.0f;
    float step= 1.0f;    
    while (true) {
        paint.getTextBounds(text, 0, text.length(), bound);
        if (bound.width() < maxWidth && bound.height() < maxHeight) {
            size += step;
            paint.setTextSize(size);
        } else {
            return size - step;
        }
    }
}

It's simple, I increase the text size until the text rect bound dimensions are close enough to maxWidth and maxHeight, to decrease the loop repeats just change step to a bigger value (accuracy vs speed), Maybe it's not the best way to achieve this but It works.

Community
  • 1
  • 1
Farshad
  • 3,074
  • 2
  • 30
  • 44
  • This has the same efficiency problem that @slybloty's answer has. I also don't understand the problem you experienced with my solution; the whole point is that you'll get different text sizes for different widths. Can you leave a comment with examples of parameters it didn't return the right size for? – Michael Scheper Nov 22 '17 at 18:43
  • Honestly your solution works but not in the way I expected, If I remember right `float desiredTextSize = testTextSize * desiredWidth / bounds.width();` returns an acceptable value but not necessarily max size, There's no doubt that your solution is more efficient but I think results is so dependent to the `desiredWidth` and not exact as I needed, regards – Farshad Nov 22 '17 at 21:43
  • So you're trying to accommodate a maximum height, as well as a maximum width? Try `float desiredTextSize = testTextSize * Math.min(desiredWidth / bounds.width(), desiredHeight / bounds.height())`. In any case, be careful with the integers in your solution; they'll cause your floats to only hold integer values, which might not be as precise as you need. – Michael Scheper Feb 10 '19 at 23:12