Draw text fitting in some specified Rectangle - android

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

Related

Android Layer-list Badge

I followed this article : http://javarticles.com/2015/09/android-icon-badge-example-using-layer-list-drawable.html . My code is almost exactley the same , with a little tweaking in the positioning part.Regardless what i try it never draws above the first layer drawable height.
I am trying to use this on an ActionBarSerlock drawer toggle button(yuh i know ABS is #Deprecated) or also known as hamburger, but how am i to set the circle to draw outside the bounds of the first image?I always get something like this:
I want the circle to draw fully and not be cut by the height of the first image.
I used the LayerDrawable method setLayerInset() .Took me some time to play around , now it looks like this:
mLayerDrawable.setLayerInset(0, 0, vSeperator, hSeperator, 0);
mLayerDrawable.setLayerInset(1, 0, 0, 0, vSeperator);
Where the vSeprator/hSeparator are density independent calculated values:
float density = getResources().getDisplayMetrics().density;
int vSeperator = (int) (10 * density + 0.5f);
int hSeperator = (int) (10 * density + 0.5f);
Hint: I'm shifting both images in equal of size but opposite directions

Android OpenGL ES - Very Weird Pixel Color from glReadPixels - Switched Image?

I am trying to return the pixel color by TouchEvents in OpenGL ES on Android. I created 2 Triangles as a rectangle. So I put the shown Image on it. The black area is not a part of the Triangles.
If I click on the black area, it always gives me the correct number 0 0 0 -1 (black). If i click on the white area it also gives me always the correct number -1 -1 -1 -1 (white). But when I click on the green rectangle, it gives me 0 0 0 -1 (black). After that I am clicking on the black rectangle next to him and it gives me the color of the neighbor(green).
The image switched in the "background", but I don't know how this is possible
Used this Tut: http://androidblog.reindustries.com/a-real-open-gl-es-2-0-2d-tutorial-part-1/
My Code for Color Picking:
public void onDrawFrame(GL10 gl) {
if (clicked) {
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glDisable(GL10.GL_FOG);
gl.glDisable(GL10.GL_BLEND);
ByteBuffer ss = ByteBuffer.allocate(4);
ss.order(ByteOrder.nativeOrder());
gl.glFlush();
gl.glReadPixels(touchXint, touchYint, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ss);
byte b[] = new byte[4];
ss.get(b);
String key = "" + b[0] + " " + b[1] + " " + b[2] + " " + b[3];
Log.d("Color: ", key);
clicked = false;
ss.rewind();
gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glEnable(GL10.GL_FOG);
gl.glEnable(GL10.GL_BLEND);
}
}
You appear to have a problem with coordinate systems. In many window/UI systems, including Android, the coordinates you receive as part of events are relative to the top/left corner of the window.
On the other hand, the window coordinate system used by OpenGL for specifying the pixel rectangle for glReadPixels() has its origin in the bottom/left corner. You can see that in the documentation, where x and y are described by:
Specify the window coordinates of the first pixel that is read from the frame buffer. This location is the lower left corner of a rectangular block of pixels.
To accommodate for these different coordinate systems, you have to invert the y-coordinate of your touch input before passing it to glReadPixels(). Instead of touchYint, use windowHeight - 1 - touchYint.
It's not the real answer, why your problem is occuring, but it seems like your X coordinate is inverted. I mean, the horizontal 0 is swapped with screenWidth. Just to confirm it you can try something like
gl.glReadPixels(screenWidth - touchXint, touchYint, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ss);
If it works, then you should search the problem in obtaining you touchXint coordinate.
Edit:
In this case the screen is oriented horizontally, so the horizontal axis is Y. The direction of Y axis in OpenGL coordinate system and Android coordinate system is inverted. Therefore, to get the right Y coordinate for the glReadPixels call, the following transform needs to be applied: y = (screenHeight-touchYint).

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, ...

Android: Measure Text Height on a Canvas

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

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.

Categories

Resources