I need to build my own custom TextView so I have been learning about StaticLayout to draw text on a canvas. This is preferable to using Canvas.drawText() directly, or so the documentation says. However, the documentation doesn't give any examples for how do it. There is only a vague reference to StaticLayout.Builder being the newer way to do it.
I found an example here but it seems a little dated.
I finally worked though how to do it so I am adding my explanation below.
StaticLayout (similar to DynamicLayout and BoringLayout) is used to layout and draw text on a canvas. It is commonly used for the following tasks:
Measuring how big multiline text would be after being laid out.
Drawing text on a bitmap image.
Making a custom view that handles its own text layout (as opposed to making a composite view with an embedded TextView). TextView itself uses a StaticLayout internally.
Measuring text size
Single line
If you only have a single line of text, you can measure it with Paint or TextPaint.
String text = "This is some text."
TextPaint myTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
mTextPaint.setColor(0xFF000000);
float width = mTextPaint.measureText(text);
float height = -mTextPaint.ascent() + mTextPaint.descent();
Multiline
However, if there is line wrapping and you need the height, then it is better to use a StaticLayout. You provide the width and then you can get the height from the StaticLayout.
String text = "This is some text. This is some text. This is some text. This is some text. This is some text. This is some text.";
TextPaint myTextPaint = new TextPaint();
myTextPaint.setAntiAlias(true);
myTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
myTextPaint.setColor(0xFF000000);
int width = 200;
Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
float spacingMultiplier = 1;
float spacingAddition = 0;
boolean includePadding = false;
StaticLayout myStaticLayout = new StaticLayout(text, myTextPaint, width, alignment, spacingMultiplier, spacingAddition, includePadding);
float height = myStaticLayout.getHeight();
New API
If you want to use the newer StaticLayout.Builder (available from API 23), you can get your layout like this:
StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), myTextPaint, width);
StaticLayout myStaticLayout = builder.build();
You can tack on addition settings using dot notation:
StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), myTextPaint, width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(spacingAddition, spacingMultiplier)
.setIncludePad(includePadding)
.setMaxLines(5);
StaticLayout myStaticLayout = builder.build();
Writing text on an image
I may expand this more in the future, but for now see this post for an example of a method that uses StaticLayout and returns a bitmap.
Making a custom text handling View
Here is an example of a custom view using a StaticLayout. It behaves like a simple TextView. When the text is too long to fit on the screen, it automatically line wraps and increases its height.
Code
MyView.java
public class MyView extends View {
String mText = "This is some text.";
TextPaint mTextPaint;
StaticLayout mStaticLayout;
// use this constructor if creating MyView programmatically
public MyView(Context context) {
super(context);
initLabelView();
}
// this constructor is used when created from xml
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
initLabelView();
}
private void initLabelView() {
mTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
mTextPaint.setColor(0xFF000000);
// default to a single line of text
int width = (int) mTextPaint.measureText(mText);
mStaticLayout = new StaticLayout(mText, mTextPaint, (int) width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false);
// New API alternate
//
// StaticLayout.Builder builder = StaticLayout.Builder.obtain(mText, 0, mText.length(), mTextPaint, width)
// .setAlignment(Layout.Alignment.ALIGN_NORMAL)
// .setLineSpacing(0, 1) // add, multiplier
// .setIncludePad(false);
// mStaticLayout = builder.build();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Tell the parent layout how big this view would like to be
// but still respect any requirements (measure specs) that are passed down.
// determine the width
int width;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthRequirement = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = widthRequirement;
} else {
width = mStaticLayout.getWidth() + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
if (width > widthRequirement) {
width = widthRequirement;
// too long for a single line so relayout as multiline
mStaticLayout = new StaticLayout(mText, mTextPaint, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false);
}
}
}
// determine the height
int height;
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightRequirement = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
height = heightRequirement;
} else {
height = mStaticLayout.getHeight() + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightRequirement);
}
}
// Required call: set width and height
setMeasuredDimension(width, height);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// do as little as possible inside onDraw to improve performance
// draw the text on the canvas after adjusting for padding
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
mStaticLayout.draw(canvas);
canvas.restore();
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/activity_vertical_margin"
tools:context="com.example.layoutpractice.MainActivity">
<com.example.layoutpractice.MyView
android:layout_centerHorizontal="true"
android:background="#color/colorAccent"
android:padding="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
Notes
This, this, and this were useful in learning how to make a custom text handling view.
See Creating a View Class if you would like to add custom attributes that can be set from code or xml.
Here is my explanation for drawing multiline text on canvas.
Declare Paint object. Use TextPaint which is an extension of Paint.
TextPaint textPaint;
Initialize Paint object. Set your own color, size etc.
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
textPaint.setColor(Color.YELLOW);
Add getTextHeight function
private float getTextHeight(String text, Paint paint) {
Rect rect = new Rect();
paint.getTextBounds(text, 0, text.length(), rect);
return rect.height();
}
in your onDraw function put following lines like this
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "This is a lengthy text. We have to render this properly. If layout mess users review will mess. Is that so ? ";
Rect bounds = canvas.getClipBounds();
StaticLayout sl = new StaticLayout(text, textPaint, bounds.width(),
Layout.Alignment.ALIGN_CENTER, 1, 1, true);
canvas.save();
//calculate X and Y coordinates - In this case we want to draw the text in the
//center of canvas so we calculate
//text height and number of lines to move Y coordinate to center.
float textHeight = getTextHeight(text, textPaint);
int numberOfTextLines = sl.getLineCount();
float textYCoordinate = bounds.exactCenterY() -
((numberOfTextLines * textHeight) / 2);
//text will be drawn from left
float textXCoordinate = bounds.left;
canvas.translate(textXCoordinate, textYCoordinate);
//draws static layout on canvas
sl.draw(canvas);
canvas.restore();
}
Courtesy goes to KOC's post
Related
How to draw a filled rectangle with specified bounds and inside that rectangle text to be drawn using Canvas Android ?? I tried
mPaint.setColor(Color.GREEN);
canvas.drawText(mText, x, y, mPaint);
mPaint.setColor(Color.BLACK);
canvas.drawRect(x, y, x + w, y + h, mPaint);
but text is not inside of that rectangle. Can any buddy tell me how to draw a rectangle surrounding specified text with consideration of text size ??
Here i have hardcoded x and y values. You can change them
mpaint= new Paint();
mpaint.setColor(Color.RED);
mpaint.setStyle(Style.FILL);
paint2= new Paint();
paint2.setColor(Color.GREEN);
paint2.setTextSize(50); //set text size
float w = paint2.measureText(s)/2;
float textSize = paint2.getTextSize();
#Override
protected void onDraw(Canvas canvas) {
paint2.setTextAlign(Paint.Align.CENTER);
canvas.drawRect(300-w, 300 - textsize, 300 + w, 300, mpaint);
canvas.drawText(s, 300, 300 ,paint2); //x=300,y=300
}
Edit :
Its bad a idea to call measureText in onDraw. You can do that outside of onDraw.
There is a video on also about performance and why you should avoid allocations in onDraw. https://www.youtube.com/watch?v=HAK5acHQ53E
Resulting snap shot
If you have to center the text inside de rect you have use this code
mpaint= new Paint();
mpaint.setColor(Color.RED);
mpaint.setStyle(Style.FILL);
paint2= new Paint();
paint2.setColor(Color.GREEN);
paint2.setTextSize(50); //set text size
float w = paint2.measureText(s)/2;
float textSize = paint2.getTextSize();
#Override
protected void onDraw(Canvas canvas) {
paint2.setTextAlign(Paint.Align.CENTER);
Rect rect = new Rect(300-w, 300 - textsize, 300 + w, 300);
canvas.drawRect(rect, mpaint);
canvas.drawText(s, rect.centerX(), rect.centerY() ,paint2); // center text inside rect
}
This might be very late for this particular query but I think many will find this answer useful. So, the problem with the Canvas for any CustomView is that, you can get the width for a particular text, but it's not that easy to get the height of the text. Also if you are using canvas.drawText(....) with simple Paint object, you can not draw multi line text. So, use the below code within your onDraw() method.
String displayText = "Hello World";
int mainTextPositionX = getWidth() / 2 ;
int mainTextPositionY = getHeight() / 2;
StaticLayout textStaticLayout;
TextPaint textPaint;
textPaint = new TextPaint();
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setColor(Color.BLUE);
textPaint.setAntiAlias(true);
textPaint.setTextSize(convertDpToPixel(30, context));
textPaint.setTextAlign(Paint.Align.CENTER);
highlightedRectPaint = new Paint();
highlightedRectPaint.setStrokeWidth(12);
highlightedRectPaint.setStyle(Paint.Style.STROKE);
highlightedRectPaint.setColor(Color.RED);
highlightedRectPaint.setAntiAlias(true);
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
textStaticLayout = StaticLayout
.Builder
.obtain(displayText, 0, displayText.length(), textPaint, (int) textPaint.measureText(displayText))
.build();
}else{
textStaticLayout = new StaticLayout(
displayText, textPaint, (int)textPaint.measureText(displayText), Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);
}
Rect highlightedTextBorderRect = new Rect();
highlightedTextBorderRect.top = mainTextPositionY-20;
highlightedTextBorderRect.left = mainTextPositionX-
((int)textPaint.measureText(displayText)/2)-20;
highlightedTextBorderRect.right = mainTextPositionX+
((int)textPaint.measureText(displayText)/2) + 20;
highlightedTextBorderRect.bottom = mainTextPositionY+
(int)textStaticLayout.getHeight()+20;
canvas.save();
canvas.translate(mainTextPositionX, mainTextPositionY);
textStaticLayout.draw(canvas);
canvas.restore();
canvas.drawRect(highlightedTextBorderRect,highlightedRectPaint);
just make sure that, you declare all the objects and variable outside of the draw() method. And this will draw a rectangle outline around the text with multi line support. If you want the rectangle to have a fill, then just use the highlightedRectPaint and change the setStyle(Paint.Style.FILL). Hope that helps.
I am trying to calculate the width of a multiline text paragraph. To my knowledge, the only class that can do this in Android is the StaticLayout (or DynamicLayout) class. When using this class i do no get the proper length of my text snippet but rather the measured the dimensions are sometimes smaller and sometimes greater depending on the text size.
So i am basically looking for a way to reliably measure the width of a multiline text string.
The following image shows how the measured width diverges from the actual text length in various text sizes.
The screenshot is created running the the following code in a custom View:
#Override
protected void onDraw( Canvas canvas ) {
for( int i = 0; i < 15; i++ ) {
int startSize = 10;
int curSize = i + startSize;
paint.setTextSize( curSize );
String text = i + startSize + " - " + TEXT_SNIPPET;
layout = new StaticLayout( text,
paint,
Integer.MAX_VALUE,
Alignment.ALIGN_NORMAL,
1.0f,
0.0f,
true );
float top = STEP_DISTANCE * i;
float measuredWidth = layout.getLineMax( 0 );
canvas.drawRect( 0, top, measuredWidth, top + curSize, bgPaint );
canvas.drawText( text, 0, STEP_DISTANCE * i + curSize, paint );
}
}
You could try using get text bounds.
private int calculateWidthFromFontSize(String testString, int currentSize)
{
Rect bounds = new Rect();
Paint paint = new Paint();
paint.setTextSize(currentSize);
paint.getTextBounds(testString, 0, testString.length(), bounds);
return (int) Math.ceil( bounds.width());
}
private int calculateHeightFromFontSize(String testString, int currentSize)
{
Rect bounds = new Rect();
Paint paint = new Paint();
paint.setTextSize(currentSize);
paint.getTextBounds(testString, 0, testString.length(), bounds);
return (int) Math.ceil( bounds.height());
}
I had a similar issue where the measured text width was off by a bit, once you set a Typeface to the paint this will go away.
So before you use the paint object in the StaticLayout just set the Typeface:
textPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL))
or whatever typeface you want.
Here's a way where you can get the multiline width of any text length. usableButtonWidth can be a given text space. I used usableButtonWidth = width of the button - padding. But any default value can be used.
Using StaticLayout, you can get the height of the text for the usableButtonWidth. Then I just try to reduce the available width by decreasing it by 1px in a loop. If the height increases then we come to know that the previously used width was the maximum permissible.
Return this value as the width of multiline text.
private int getTextWidth(){
if(this.getText().length() != 0){
Paint textPaint = new Paint();
Rect bounds = new Rect();
textPaint.setTextSize(this.getTextSize());
if(mTypeface != null){
textPaint.setTypeface(mTypeface);
}
String buttonText = this.getText().toString();
textPaint.getTextBounds(buttonText, 0, buttonText.length(), bounds);
if(bounds.width() > usableButtonWidth){
TextPaint longTextPaint = new TextPaint();
longTextPaint.setTextSize(this.getTextSize());
if(mTypeface != null){
longTextPaint.setTypeface(mTypeface);
}
StaticLayout staticLayout = new StaticLayout(this.getText(), longTextPaint, usableButtonWidth,
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
int textLineCount = (int)Math.ceil(staticLayout.getHeight() / bounds.height());
int multiLineTextWidth = usableButtonWidth;
while ((int)Math.ceil(staticLayout.getHeight() / bounds.height()) == textLineCount && multiLineTextWidth > 1){
multiLineTextWidth--;
staticLayout = new StaticLayout(this.getText(), longTextPaint, multiLineTextWidth,
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
}
return multiLineTextWidth+1;
} else {
return bounds.width();
}
} else {
return 0;
}
}
I've been struggling with text measuring and scaled canvases.
When the canvas is unscaled, getTextBounds and measureText deliver accurate results. However, when the canvas is scaled both methods do not deliver results that match the actual size of a printed text.
For testing I've created a subclass of View with the following onDraw method:
final float scaling = 0.51f;
final int fontSize = 50;
canvas.scale(scaling, scaling);
font = Typeface.create("Arial", Typeface.NORMAL);
Paint paint = new Paint();
paint.setColor(0xff4444ff);
paint.setTypeface(font);
paint.setTextSize(fontSize);
paint.setAntiAlias(true);
int x = 10;
int y = 100;
final String text = "Lorem ipsum dolor sit amet, consectetur adipisici elit...";
canvas.drawText(text, x, y, paint);
// draw border using getTextBounds
paint.setColor(0xffff0000);
paint.setStyle(Paint.Style.STROKE);
paint.setTypeface(font);
paint.setTextSize(fontSize);
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
bounds.offset(x, y);
paint.setColor(0x80ffff00);
canvas.drawRect(bounds, paint);
// draw border using measureText
float w = paint.measureText(text);
bounds.left = x;
bounds.right = (int) Math.ceil(bounds.left + w);
bounds.top -= 10;
bounds.bottom += 10;
paint.setColor(0x8000ffff);
paint.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 0));
canvas.drawRect(bounds, paint);
for scaling = 0.5 I get the following output:
for scaling = 0.51 the following result is shown:
The yellow solid border marks the rect delivered from getTextBounds, the dashed cyan rect is rendered using the width delivered from measureText.
As you can see, the text with scaling = 0.5 is smaller than the measured dimensions and with scaling=0.51 the drawn text is way bigger than the measured dimension.
Any help is appreciated!
Ok, just found out how to circumvent the issue.
The problem is that the Paint does not know about the Canvas scaling. Therefore measureText and getTextBounds deliver the unscaled result. But since the font size does not scale linearly (however, the drawn rect does ), you have to make up for that effect manually.
So the solution would be:
// paint the text as usual
Paint paint = new Paint();
paint.setTypeface(font);
paint.setTextSize(fontSize);
canvas.drawText(text, x, y, paint);
// measure the text using scaled font size and correct the scaled value afterwards
Paint paint = new Paint();
paint.setTypeface(font);
paint.setTextSize(fontSize * scaling);
float w = paint.measureText(text) / scaling;
Using Mono for Android I had to use display metrics as shown here:
public override System.Drawing.SizeF MeasureString(MyFont f, string text)
{
Rect r = new Rect();
f.DrawingFont.GetTextBounds(text, 0, text.Length, r);
//Manual scaling using DisplayMetrics due to Android
//issues for compatibility with older versions
Android.Util.DisplayMetrics metrics = new Android.Util.DisplayMetrics();
GetDisplay.GetMetrics(metrics);
return new System.Drawing.SizeF(r.Width(), r.Height() * metrics.Density);
}
Where f.DrawingFont is an Androdid.Text.TextPaint GetDisplay is:
private Display GetDisplay()
{
return this.GetSystemService(Android.Content.Context.WindowService).JavaCast<Android.Views.IWindowManager>().DefaultDisplay;
}
And the same method in Java is:
private Display getDisplay() {
return ((WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE)).getDefaultDisplay();
}
I'm developing an Android 2.2 application.
I'm using this method to draw a text in a View:
public void draw(Canvas c)
{
p.setColor(Color.WHITE);
if(name != null)
c.drawText(name, getLeft(), getTop(), p);
}
How can I get height and width of name text?
If I do this (p is a Paint object):
p.getTextBounds(name, 0, name.length(), bounds);
I get With name = 'Loading', bounds = Rect(1, -10 - 42, 3);.
I don't know why I get this strange rectangle.
Any clue?
This is my possible solution:
public class MyView extends ARSphericalView
{
public String name;
public MyView(Context ctx)
{
super(ctx);
inclination = 0;
}
public void draw(Canvas c)
{
/*
p.setColor(Color.WHITE);
if(name != null)
c.drawText(name, getLeft(), getTop(), p);
*/
p.setColor(Color.BLACK);
if(name != null)
{
Rect bounds = new Rect();
c.drawText(name, getLeft(), getTop(), p);
setBackgroundColor(Color.WHITE);
p.getTextBounds(name, 0, name.length(), bounds);
c.drawRect(bounds, p);
}
}
}
But, it doesn't work. I get that strange rectangle.
A text size is measured from its baseline, and has an ascent (upwards, so -y) and descent (downwards, y). The first y-value in your rect (-10) is the ascent, the second the descent (3). Width of the text is 41 (42-1). Height thus is |ascent| + |descent| is 10 + 3 = 13;
Similarly p.getFontMetrics() has top and bottom attributes, which describe the highest ascent and descent the font you are using have. If you want to calculate the height of a text, then its Math.abs(p.ascent) + Math.abs(p.descent) You can also measure the width of a text with p.measureText().
You can use Paint.setTextSize() to set the text size, and then paint it in the Canvas.
I have a View which draws a rectangle with a line of text inside of it. The view uses break text to ensure that no text extends outside of the rectangle; it ignores any text that does. This works fine for some characters, but often Strings made up of 'l's and 'f's extend outside of the rectangle. So, I'm in need of a sanity check here: Is there some obvious flaw in my below code, or is it possible that Paint.breakText(...) is inaccurate?
public void onDraw(Canvas canvas)
{
int MARGIN = 1;
int BORDER_WIDTH = 1;
Paint p = new Paint();
p.setAntiAlias(true);
p.setTextSize(12);
p.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));
RectF rect = getRect();
float maxWidth = rect.width() - MARGIN - BORDER_WIDTH * 2;
String str = getText();
char[] chars = str.toCharArray();
int nextPos = p.breakText(chars, 0, chars.length, maxWidth, null);
str = str.substring(0, nextPos);
float textX = MARGIN + BORDER_WIDTH;
float textY = (float) (Math.abs(p.getFontMetrics().ascent) + BORDER_WIDTH + MARGIN);
canvas.drawText(str, textX, textY, p);
p.setStrokeWidth(BORDER_WIDTH);
p.setStyle(Style.STROKE);
canvas.drawRect(rect, p);
}
This was fixed by: Paint.setSubpixelText(true);
The problem might be how you draw your rectangle. Strokes are not outside of the rectangle, half of the stroke is inside, half is outside.