Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw text fitting in some specified Rectangle

Using a canvas, I want to draw some short label text (1-2 characters) which fits into some specified rectangle. For some other reasons, the scaling I use is such that the dimensions of this recangle are small, i.e. about 1.

The problem I'm facing is to calculate the optimal (as large as possible so that the text still fits) text size to use with Paint.setTextSize prior to drawing the text (which I do using Canva.drawText()). For that I can either use the Paint.Fontmetrics object to get some general font dimensions as floats or getTextBounds(String text, int start, int end, Rect bounds) to get the bounding box of the text as an integer rectangle. Due to the scaling I use, the integer bounding box from the latter is to imprecise to calculate the optimal text size for my purpose.

What I would need is some method to get the bounding box of the text with higher precision (i.e. like getStringBounds(String str, Graphics context) in java.awt.FontMetrics), but I found no suitable method.

like image 568
Christian Gawron Avatar asked Feb 26 '23 11:02

Christian Gawron


2 Answers

I now was also busy with this problem.

Unfortunately I didn't manage to install the native source yet, but I'm pretty sure that text measurements (both getTextBounds() and measureText()) differ heftily from the real text output, especially for small font sizes (as to be seen in Moritz' screenshot). I suppose that the measurement methods use a float width for a char, and the real text output uses an int (presumably due to performance). This will of course fail, if you need an absolutely exact size (e.g for autosize). I experimented a bit with a monospace font. This Image shows some of these experiments:

Android Autoscale experiments.

The text scale in these experiments was set by auto scaling it.

In the top row, I created boxes with an integer increment. These match the android text output. You see, the last cipher doesn't match the screen!

In the 2nd row, the boxes were created with a float increment. The boxes match the screen better, but they don't match the android text output!

In the last row, the boxes were incremented by a float increment and the text was output char by char by the same increment. Both boxes and chars fit perfect.

In my conclusion, it is currently not possible to set an Android text output to an exact width. The problem seems not to be the measurement methods, which are exact enough. The problem seems to be, that you can not set an exact text width by setTextSize().

Try the following to reproduce this:

    float width = 100; // define a width which should be achieved
    m_textPaint.setTextSize( 100 ); // set a text size surely big enough
    Rect r = new Rect();
    String s = new String("This is a test text");
    m_textPaint.getTextBounds( s, 0, s.length(), r ); // measure the text with a random size
    float fac = width / r.width(); // compute the factor, which will scale the text to our target width
    Log.i( "MyTestOutput", "current text width:" + r.width() );
    Log.i( "MyTestOutput", "wanted text width:" + width );
    Log.i( "MyTestOutput", "factor:" + fac );
    Log.i( "MyTestOutput", "expected result:" + (float) r.width() * fac );
    m_textPaint.setTextSize( m_textPaint.getTextSize() * fac );
    m_textPaint.getTextBounds( s, 0, s.length(), r ); // now final measurement: whats the real width?
    Log.i( "MyTestOutput", "real result:" + r.width() );

I get an output of:

05-26 12:18:18.420: I/MyTestOutput(23607): current text width:1125

05-26 12:18:18.425: I/MyTestOutput(23607): wanted text width:100.0

05-26 12:18:18.425: I/MyTestOutput(23607): factor:0.08888889

05-26 12:18:18.425: I/MyTestOutput(23607): expected result:100.0

05-26 12:18:18.430: I/MyTestOutput(23607): real result:94

So in my opinion, the only way to achieve very exactly autoscaled text, is to a) autoscale it by measuring and setting text size b) output it char by char by a self computed increment.

Unfortunately, this works only for monospaced fonts. For other fonts, it will be more complicated, but also possible.

like image 83
user2328447 Avatar answered Mar 06 '23 02:03

user2328447


I did that just some days ago: See my blog.

You would use a StaticLayout:

// bmp1 is a source bitmap (in that case 44x44px big).
// density is the screen density, you will need to retrieve it yourself as well.

canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.overlay_44x44), new Matrix(), null);

// Initialize using a simple Paint object.
final TextPaint tp = new TextPaint(WHITE);

// Save the canvas state, it might be re-used later.
canvas.save();

// Create a padding to the left & top.
canvas.translate(4*density, 4*density);

// Clip the bitmap now.
canvas.clipRect(new Rect(0, 0, bmp1.getWidth(),(int) (bmp1.getHeight() - (6*density))));

// Basic StaticLayout with mostly default values
StaticLayout sl = new StaticLayout(message, tp, bmp1.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
sl.draw(canvas);

// Restore canvas.
canvas.restore();
like image 30
Sebastian Roth Avatar answered Mar 06 '23 02:03

Sebastian Roth