Android: Measure Text Height on a Canvas - android

I am currently working on rendering a Bitmap, that I then want to send to a mobile printer. However, I am struggling with measuring the height of my text, so I can advance the y position appropriately.
My basic bitmap/canvas/paint configuration is this (Font Size is 16 and the dimensions of the bitmap are 200x400 (width x height):
public MyRenderer() {
// Initialize bitmap
bitmap = Bitmap.createBitmap(200, 400, Bitmap.Config.ARGB_8888);
// Initialize canvas
canvas = new Canvas(bitmap);
// Initialize brush (Paint instance)
brush = new Paint();
brush.setTextSize(16);
brush.setTypeface(Typeface.SANS_SERIF);
brush.setColor(Color.BLACK);
brush.setStyle(Paint.Style.FILL);
brush.setAntiAlias(true);
brush.setTextAlign(Align.LEFT);
}
So far so good, now what I want to do is: If I use the Paint's method drawText I need to supply the x and y coordinates. As for x that's zero (assuming left aligned text) but as for y, I'd have to calculate the height of each text I print and add it up, so I can keep track of my current y position.
And this is where it gets odd: I am using the following method to determine the height of a text (using the Paint objected that I initialized previously - it's called "brush"):
public int measureHeight(String text) {
Rect result = new Rect();
// Measure the text rectangle to get the height
brush.getTextBounds(text, 0, text.length(), result);
return result.height();
}
The above method returns the following values for the following texts:
"Hello World" returns a height of 12
"A camera instance can be used to compute 3D transformations and generate a matrix." returns a height of 16
"Introducing Android Design: The place to learn about principles, building blocks, and patterns for creating world-class Android user interfaces. Whether you're a UI professional or a developer playing that role, these docs show you how to make good design decisions, big and small." returns a height of 16
It makes sense to me, that number 2 and 3 return a greater height than number 1 but if one line has a height of 12 (as number one does) - it makes no sense, that multiple lines have a height of 16 ?
Am I missing something here? There is a convenience method for measuring the width of a text (using an instance of paint and call measureText("myText") which works perfectly, however I am quite at a loss, when it comes to the height, as the above given results don't make any sense to me.
EDIT
I am aware, that getTextBounds probably does no auto-wrapping of multi-lined text, and that's ok, I already wrote a method for splitting text, but even if it just measures one line, the above given length values still seem unlikely.

I think it is because the "p" in "compute" extends below the baseline whereas "Hello World" only contains letters that are above the baseline.
Since the line distance should not depend on what specific letters your text happens to consist of you are probably looking for Paint.FontMetrics which can be obtained via Paint.getFontMetrics(). Compute descent - ascent + leading to get the recommended baseline distance (because ascent has a negative value).

There is a small error in the accepted answer. If you want the text height, you should use
Paint.FontMetrics fm = mTextPaint.getFontMetrics();
float textHeight = fm.descent - fm.ascent;
And if you want the line height, you should use
float lineHeight = fm.bottom - fm.top + fm.leading;
Leading is optional interline spacing, so if you need to get the line hight you can include it. But if you just want the text height, then you can leave it off.
Note
I've never actually seen leading be anything else than 0, and as far as I can tell it even seems to be ignored in the TextView source code (and its associated Layout, StaticLayout, etc.). Please correct me if I'm wrong. So it is probably safe it leave it out of line hight calulations, but I'm not completely sure about that.
See also
Getting text height from getTextBounds vs FontMetrics vs StaticLayout
Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics

Related

Auto set rectangle length on canvas as per string length

I have an array of strings and I have drawn those strings on screen using android graphics library (Canvas, Paint). Firstly, a transparent rectangle is drawn then on this rectangle I paint the strings, giving the impression of text with border line and filled.
The problem is some strings are long and some are short, how can I modify the rectangle so that it is as long and as wide as the string is? (Like WRAP_CONTENT in textView)
Currently I am using this method
canvas.drawRect(_x-10, _y-10, _x+620, _y+30, rectanglePaint);
canvas.drawText(placeName, _x, _y, textPaint); //text
If there is a better way then please do let me know.
Note: It will be used in an AR app so the text will be moving from left to right and vice versa as the mobile is moving. _x, _y for place name works perfectly in 2nd line, I want text should remain highlighted, no matter how the mobile is moving.
Paint has a measure text method that will give the width of the text if it were to be drawn with that paint. Its get font metrics will probably give you some data that you can calculate the height from. The sizing, positioning, wrapping and rendering of text is a very complicated problem and I would advise using a child TextView (with a background) in your ViewGroup rather than trying to roll your own if you're doing anything complicated.

How can I really center my text horizontally?

I have a Piano app with some round labels on it with the note names.
These note names need to be completely centered on the label.
I finally figured out how to do that vertically using ascent and descent. However, I am still not getting the horizontal alignment perfectly ok.
It is mainly about the rendering of single characters. Whatever I do, some of the characters are off. It's slightly, but it's there.
Notice how the E is slightly off to the right for example, and the B a bit to the left.
I am rendering the labels in onDraw(), not with custom views. I also tried with a TextView the size of the labels and using Gravity.CENTER, but this gave the same results. Also note that I tried Align.Center too.
Code:
usedPaint.setTextAlign(Align.LEFT); //Also tried it with Center
Rect textBounds = new Rect();
usedPaint.getTextBounds(infoText, 0, infoText.length(), textBounds);
float textWidth = usedPaint.measureText(infoText);
canvas.drawText(infoText, circleX - (0.5F * textWidth), circleY - ((usedPaint.descent() + usedPaint.ascent())/2),
usedPaint);
canvas.drawRect((circleX - (0.5F* textWidth)), circleY - (0.5F * textBounds.height()), circleX + (0.5F * textWidth), circleY + (0.5F * textBounds.height()), otherPaint);
The drawn box's width is the result of measureText. At the E character you see that somehow it measures some whitespace on the left, making the character drift off to the right.
This is using the standard font on Android 4.0.3. Using a custom TTF font results in the same kind of issues, but different for each character.
I am wondering what else I can do? (Besides getting over it ;) )
with center align just change canvas.drawText(infoText,
circleX - (0.5F * textWidth), ... to canvas.drawText(infoText,
circleX, ...

How to add ellipses to text StaticLayout when it exceeds its height?

I'm programmatically creating StaticLayouts with varying strings, and then applying each to a bitmap that is then added to a canvas. The SL displays up to two lines before getting cut off by the fixed height that I defined in the SL constructor. But there is no ellipses to indicate that the text is longer than what is shown.
From http://developer.android.com/reference/android/text/StaticLayout.html I see that the third constructor has truncation arguments at the end:
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)
I know I'd like TextUtils.TruncateAt.END, but I can't figure out what I should be defining for bufstart, bufend, and ellipsizedWidth. I couldn't find any good examples of this constructor in particular, or if it will even help achieve my goal.
Not 100% familiar with Android, but have had the same issue myself and this is what I've found:
bufstart - index of the first character (in source) to include in the layout
bufend - index of the last character to include in the layout
outerwidth - I think the width of the imaginary box containing all your text (I've just made it really really wide)
ellipsizedwidth - the width of your box your drawing into, text will be trimmed if it exceeds this
Sorry for a not-so-confident answer, but I'm still learning!

Android: Text scale to biggest possible without wrapping

I am programming an app that shows a lot of verses/poems so text wrapping is not an option for me. I would like the text to be as big as possible (doesn't have to recalculate each time a new text is shown, should just allow the biggest text to fit on the screen) without extending screen size. It should not visually scale or take longer for the text to appear.
Is this possible?
Thanks
I would suggest a simple search for the best point size using the largest text that you need to fit. This can be done once at start-up. (Well, maybe twice—once for landscape and once for portrait). The first step would be to initialize a Paint with the typeface you want to use for display. Then call this function to
public void setBestTextSize(String longestText, int targetWidth, Paint paint) {
float size = paint.getTextSize(); // initial size
float w = paint.meaasureText(longestText);
size = targetWidth * size / w;
paint.setTextSize(size);
// test if we overshot
w = paint.measureText(longestText);
while (w > targetWidth) {
--size;
paint.setTextSize(size);
w = paint.measureText(longestText);
}
A binary search in the loop might be theoretically faster, but this should do pretty well since text width does scale approximately linearly with font size and the first step before the loop should get the size pretty close.
An alternative approach, which deals nicely with view size changes, is shown in this thread.

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.
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:
.
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.
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();

Categories

Resources