Search code examples
androidandroid-canvasandroid-graphicsstaticlayout

Measuring tight bounds box of multiline text


I'm drawing a multiline text on a canvas using StaticLayout, and I want to measure the most tight bounds box around the text before drawing, (the text could be in different sizes, fonts, styles etc...), I want something like that:

Size measureText(String text, float size, Font font, etc...)

and I want it to return the most tight bounds box around the text, i.e. (if we are talking about the pixels of the text):

(leftest_pixel - rightest_pixel, highest_pixel - lowest_pixels)

If the text was single line, I could do:

Paint paint = new Paint();
...
paint.getTextBounds(text, 0, size, rect);

But since the text could have multiple lines I must take in consider the linespacing and the glyphs descent and all the other font parameters... so the next option will be to use StaticLayout with maximalLineWidth (in order to break the lines), but StaticLayout doesn't calculate the most tight box, it will add a bit paddings in the top and in the bottom (because it basically multiple the number of lines by the max line height):

for example, the green box is the result of measuring with StaticLayout and the red box is the box I want to receive:

enter image description here enter image description here enter image description here

How can I do it? Thanks.


Solution

  • Build the StaticLayout then determine the bounds of each individual line using methods from TextPaint. The boundaries for the entire multi-line text is the top boundary of the top line, the bottom boundary of the last line and the left-most and right-most boundaries of all the lines.

    Here is a sample custom view that demonstrate this concept. The layout is simply the custom view with a width of 200dp and a height of wrap_content.

    enter image description here

    enter image description here

    MyStaticText

    public class MyStaticText extends View {
        private final String mText = "This is some longer text to test multiline layout.";
        private TextPaint mTextPaint;
        private StaticLayout mStaticLayout;
        private final Paint mRectPaint = new Paint();
    
        public MyStaticText(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
            mTextPaint = new TextPaint();
            mTextPaint.setAntiAlias(true);
            mTextPaint.setTextSize(FONT_SIZE * getResources().getDisplayMetrics().density);
            mTextPaint.setColor(Color.BLACK);
    
            mRectPaint.setStyle(Paint.Style.STROKE);
            mRectPaint.setStrokeWidth(2f);
            mRectPaint.setColor(Color.RED);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthAskedFor = MeasureSpec.getSize(widthMeasureSpec);
            if (widthMode == MeasureSpec.EXACTLY) {
                mStaticLayout = new StaticLayout(mText, mTextPaint, widthAskedFor, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false);
            } else {
                throw new RuntimeException("View width must be exactly specified.");
            }
    
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int height;
            if (heightMode == MeasureSpec.AT_MOST) {
                height = mStaticLayout.getHeight() + getPaddingTop() + getPaddingBottom();
            } else {
                throw new RuntimeException("View height must be 'wrap_content'.");
            }
            setMeasuredDimension(widthAskedFor, height);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            mStaticLayout.draw(canvas);
    
            // Start with bounds of first line.
            Rect textBounds = getLineBounds(0);
    
            // Check bounds of last line since it will be the bottom of the bounding rectangle.
            Rect lineBounds = getLineBounds(mStaticLayout.getLineCount() - 1);
            if (lineBounds.bottom > textBounds.bottom) {
                textBounds.bottom = lineBounds.bottom;
            }
    
            // Now check all lines for min left bound and max right bound.
            for (int line = 0; line < mStaticLayout.getLineCount(); line++) {
                lineBounds = getLineBounds(line);
                if (lineBounds.left < textBounds.left) {
                    textBounds.left = lineBounds.left;
                }
                if (lineBounds.right > textBounds.right) {
                    textBounds.right = lineBounds.right;
                }
            }
            canvas.drawRect(textBounds, mRectPaint);
        }
    
        private Rect getLineBounds(int line) {
            int firstCharOnLine = mStaticLayout.getLineStart(line);
            int lastCharOnLine = mStaticLayout.getLineVisibleEnd(line);
            String s = mText.substring(firstCharOnLine, lastCharOnLine);
    
            // bounds will store the rectangle that will circumscribe the text.
            Rect bounds = new Rect();
    
            // Get the bounds for the text. Top and bottom are measured from the baseline. Left
            // and right are measured from 0.
            mTextPaint.getTextBounds(s, 0, s.length(), bounds);
            int baseline = mStaticLayout.getLineBaseline(line);
            bounds.top = baseline + bounds.top;
            bounds.bottom = baseline + bounds.bottom;
    
            return bounds;
        }
    
        private static final int FONT_SIZE = 48;
    }
    

    Here is a demo app that has a more generalized solution.