androidcanvastextandroid-custom-viewstaticlayout

How is StaticLayout used in Android?


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.


Solution

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

    enter image description here

    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