Auto Scale TextView Text to Fit within Bounds

后端 未结 30 2868
囚心锁ツ
囚心锁ツ 2020-11-21 05:49

I\'m looking for an optimal way to resize wrapping text in a TextView so that it will fit within its getHeight and getWidth bounds. I\'m not simply looking for

相关标签:
30条回答
  • 2020-11-21 06:10

    At google IO conference in 2017, google introduced autoSize property of TextView

    https://youtu.be/fjUdJ2aVqE4

    <android.support.v7.widget.AppCompatTextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/my_text"
            app:autoSizeTextType="uniform"
            app:autoSizeMaxTextSize="10sp"
            app:autoSizeMinTextSize="6sp"
            app:autoSizeStepGranularity="1sp"/>
    
    0 讨论(0)
  • 2020-11-21 06:13

    I needed a specific solution. I have got an edittext and textview in my layout. The textview is fixed height and width. When the user starts to type in the edittext, the text should immediately appear in the textview. The text in the textfield should auto - resize to fit the textview. So I updated Chase's solution to work for me. So when the text changes in the textview, resizing starts. The difference between mine and Chase's soluton: resizing is done even if the user DELETE some chars. I hope it can help someone.

    public class TextFitTextView extends TextView {
    
    // Minimum text size for this text view
    public static final float MIN_TEXT_SIZE = 10;
    
    // Maximum text size for this text view - if it is 0, then the text acts
    // like match_parent
    public static final float MAX_TEXT_SIZE = 0;
    
    // Our ellipse string
    private static final String mEllipsis = "...";
    
    // Text size that is set from code. This acts as a starting point for
    // resizing
    private float mTextSize;
    
    // Lower bounds for text size
    private float mMinTextSize = MIN_TEXT_SIZE;
    
    // Max bounds for text size
    private float mMaxTextSize = MAX_TEXT_SIZE;
    
    // Text view line spacing multiplier
    private float mSpacingMult = 1.0f;
    
    // Text view additional line spacing
    private float mSpacingAdd = 0.0f;
    
    // Add ellipsis to text that overflows at the smallest text size
    private boolean mAddEllipsis = true;
    
    // Add ellipsis to text that overflows at the smallest text size
    private int heightLimit;
    private int widthLimit;
    
    // Default constructor override
    public TextFitTextView(Context context) {
        this(context, null);
    }
    
    // Default constructor when inflating from XML file
    public TextFitTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    // Default constructor override
    public TextFitTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mTextSize = getTextSize();
    }
    
    /**
     * When text changes resize the text size.
     */
    @Override
    protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
        // if we are adding new chars to text
        if (before <= after && after != 1) {
            resizeText(true);
            // now we are deleting chars
        } else {
            resizeText(false);
        }
    }
    
    /**
     * Override the set text size to update our internal reference values
     */
    @Override
    public void setTextSize(float size) {
        super.setTextSize(size);
        mTextSize = getTextSize();
    }
    
    /**
     * Override the set text size to update our internal reference values
     */
    @Override
    public void setTextSize(int unit, float size) {
        super.setTextSize(unit, size);
        mTextSize = getTextSize();
    }
    
    /**
     * Override the set line spacing to update our internal reference values
     */
    @Override
    public void setLineSpacing(float add, float mult) {
        super.setLineSpacing(add, mult);
        mSpacingMult = mult;
        mSpacingAdd = add;
    }
    
    /**
     * Set the lower text size limit and invalidate the view
     * 
     * @param minTextSize
     */
    public void setMinTextSize(float minTextSize) {
        mMinTextSize = minTextSize;
        requestLayout();
        invalidate();
    }
    
    /**
     * Return lower text size limit
     * 
     * @return
     */
    public float getMinTextSize() {
        return mMinTextSize;
    }
    
    /**
     * Set flag to add ellipsis to text that overflows at the smallest text size
     * 
     * @param addEllipsis
     */
    public void setAddEllipsis(boolean addEllipsis) {
        mAddEllipsis = addEllipsis;
    }
    
    /**
     * Return flag to add ellipsis to text that overflows at the smallest text
     * size
     * 
     * @return
     */
    public boolean getAddEllipsis() {
        return mAddEllipsis;
    }
    
    /**
     * Get width and height limits
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (widthLimit == 0 && heightLimit == 0) {
            widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
            heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
        }
        super.onLayout(changed, left, top, right, bottom);
    }
    
    /**
     * Resize the text size with specified width and height
     * 
     * @param width
     * @param height
     */
    public void resizeText(boolean increase) {
        CharSequence text = getText();
        // Do not resize if the view does not have dimensions or there is no
        // text
        if (text == null || text.length() == 0 || heightLimit <= 0 || widthLimit <= 0 || mTextSize == 0) {
            return;
        }
    
        // Get the text view's paint object
        TextPaint textPaint = getPaint();
    
        // Get the required text height
        int textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
    
    
        // If the text length is increased 
        // Until we either fit within our text view or we had reached our min
        // text size, incrementally try smaller sizes
        if (increase) {
            while (textHeight > heightLimit && mTextSize > mMinTextSize) {
                mTextSize = Math.max(mTextSize - 2, mMinTextSize);
                textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
            }
        } 
    //      text length has been decreased
        else {
    //          if max test size is set then add it to while condition
            if (mMaxTextSize != 0) {
                while (textHeight < heightLimit && mTextSize <= mMaxTextSize) {
                    mTextSize = mTextSize + 2;
                    textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
                }
            } else {
                while (textHeight < heightLimit) {
                    mTextSize = mTextSize + 2;
                    textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
                }
            }
            mTextSize = textHeight > heightLimit ? mTextSize - 2 : mTextSize;
        }
    
        // If we had reached our minimum text size and still don't fit, append
        // an ellipsis
        if (mAddEllipsis && mTextSize == mMinTextSize && textHeight > heightLimit) {
            // Draw using a static layout
            TextPaint paint = new TextPaint(textPaint);
            StaticLayout layout = new StaticLayout(text, paint, widthLimit, Alignment.ALIGN_NORMAL, mSpacingMult,
                    mSpacingAdd, false);
            // Check that we have a least one line of rendered text
            if (layout.getLineCount() > 0) {
                // Since the line at the specific vertical position would be cut
                // off,
                // we must trim up to the previous line
                int lastLine = layout.getLineForVertical(heightLimit) - 1;
                // If the text would not even fit on a single line, clear it
                if (lastLine < 0) {
                    setText("");
                }
                // Otherwise, trim to the previous line and add an ellipsis
                else {
                    int start = layout.getLineStart(lastLine);
                    int end = layout.getLineEnd(lastLine);
                    float lineWidth = layout.getLineWidth(lastLine);
                    float ellipseWidth = paint.measureText(mEllipsis);
    
                    // Trim characters off until we have enough room to draw the
                    // ellipsis
                    while (widthLimit < lineWidth + ellipseWidth) {
                        lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString());
                    }
                    setText(text.subSequence(0, end) + mEllipsis);
                }
            }
        }
    
        // Some devices try to auto adjust line spacing, so force default line
        // spacing
        // and invalidate the layout as a side effect
        setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
        setLineSpacing(mSpacingAdd, mSpacingMult);
    
    }
    
    // Set the text size of the text paint object and use a static layout to
    // render text off screen before measuring
    private int getTextHeight(CharSequence source, TextPaint originalPaint, int width, float textSize) {
        // Update the text paint object
        TextPaint paint = new TextPaint(originalPaint);
        paint.setTextSize(textSize);
        // Measure using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd,
                true);
        return layout.getHeight();
    }
    
    }
    
    0 讨论(0)
  • 2020-11-21 06:13

    I combined some of the above suggestions to make one that scales up and down, with bisection method. It also scales within the width.

    /**
     *               DO WHAT YOU WANT TO PUBLIC LICENSE
     *                    Version 2, December 2004
     *
     * Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
     *
     * Everyone is permitted to copy and distribute verbatim or modified
     * copies of this license document, and changing it is allowed as long
     * as the name is changed.
     *
     *            DO WHAT YOU WANT TO PUBLIC LICENSE
     *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
     *
     *  0. You just DO WHAT YOU WANT TO.
     */
    
    import android.content.Context;
    import android.text.Layout.Alignment;
    import android.text.StaticLayout;
    import android.text.TextPaint;
    import android.util.AttributeSet;
    import android.util.TypedValue;
    import android.widget.TextView;
    
    /**
     * Text view that auto adjusts text size to fit within the view. If the text
     * size equals the minimum text size and still does not fit, append with an
     * ellipsis.
     * 
     * @author Chase Colburn
     * @since Apr 4, 2011
     */
    public class AutoResizeTextView extends TextView {
    
        // Minimum text size for this text view
        public static final float MIN_TEXT_SIZE = 10;
    
        // Minimum text size for this text view
        public static final float MAX_TEXT_SIZE = 128;
    
        private static final int BISECTION_LOOP_WATCH_DOG = 30;
    
        // Interface for resize notifications
        public interface OnTextResizeListener {
            public void onTextResize(TextView textView, float oldSize, float newSize);
        }
    
        // Our ellipse string
        private static final String mEllipsis = "...";
    
        // Registered resize listener
        private OnTextResizeListener mTextResizeListener;
    
        // Flag for text and/or size changes to force a resize
        private boolean mNeedsResize = false;
    
        // Text size that is set from code. This acts as a starting point for
        // resizing
        private float mTextSize;
    
        // Temporary upper bounds on the starting text size
        private float mMaxTextSize = MAX_TEXT_SIZE;
    
        // Lower bounds for text size
        private float mMinTextSize = MIN_TEXT_SIZE;
    
        // Text view line spacing multiplier
        private float mSpacingMult = 1.0f;
    
        // Text view additional line spacing
        private float mSpacingAdd = 0.0f;
    
        // Add ellipsis to text that overflows at the smallest text size
        private boolean mAddEllipsis = true;
    
        // Default constructor override
        public AutoResizeTextView(Context context) {
            this(context, null);
        }
    
        // Default constructor when inflating from XML file
        public AutoResizeTextView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        // Default constructor override
        public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            mTextSize = getTextSize();
        }
    
        /**
         * When text changes, set the force resize flag to true and reset the text
         * size.
         */
        @Override
        protected void onTextChanged(final CharSequence text, final int start,
                final int before, final int after) {
            mNeedsResize = true;
            // Since this view may be reused, it is good to reset the text size
            resetTextSize();
        }
    
        /**
         * If the text view size changed, set the force resize flag to true
         */
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            if (w != oldw || h != oldh) {
                mNeedsResize = true;
            }
        }
    
        /**
         * Register listener to receive resize notifications
         * 
         * @param listener
         */
        public void setOnResizeListener(OnTextResizeListener listener) {
            mTextResizeListener = listener;
        }
    
        /**
         * Override the set text size to update our internal reference values
         */
        @Override
        public void setTextSize(float size) {
            super.setTextSize(size);
            mTextSize = getTextSize();
        }
    
        /**
         * Override the set text size to update our internal reference values
         */
        @Override
        public void setTextSize(int unit, float size) {
            super.setTextSize(unit, size);
            mTextSize = getTextSize();
        }
    
        /**
         * Override the set line spacing to update our internal reference values
         */
        @Override
        public void setLineSpacing(float add, float mult) {
            super.setLineSpacing(add, mult);
            mSpacingMult = mult;
            mSpacingAdd = add;
        }
    
        /**
         * Set the upper text size limit and invalidate the view
         * 
         * @param maxTextSize
         */
        public void setMaxTextSize(float maxTextSize) {
            mMaxTextSize = maxTextSize;
            requestLayout();
            invalidate();
        }
    
        /**
         * Return upper text size limit
         * 
         * @return
         */
        public float getMaxTextSize() {
            return mMaxTextSize;
        }
    
        /**
         * Set the lower text size limit and invalidate the view
         * 
         * @param minTextSize
         */
        public void setMinTextSize(float minTextSize) {
            mMinTextSize = minTextSize;
            requestLayout();
            invalidate();
        }
    
        /**
         * Return lower text size limit
         * 
         * @return
         */
        public float getMinTextSize() {
            return mMinTextSize;
        }
    
        /**
         * Set flag to add ellipsis to text that overflows at the smallest text size
         * 
         * @param addEllipsis
         */
        public void setAddEllipsis(boolean addEllipsis) {
            mAddEllipsis = addEllipsis;
        }
    
        /**
         * Return flag to add ellipsis to text that overflows at the smallest text
         * size
         * 
         * @return
         */
        public boolean getAddEllipsis() {
            return mAddEllipsis;
        }
    
        /**
         * Reset the text to the original size
         */
        public void resetTextSize() {
            if (mTextSize > 0) {
                super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
                // mMaxTextSize = mTextSize;
            }
        }
    
        /**
         * Resize text after measuring
         */
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            if (changed || mNeedsResize) {
                int widthLimit = (right - left) - getCompoundPaddingLeft()
                        - getCompoundPaddingRight();
                int heightLimit = (bottom - top) - getCompoundPaddingBottom()
                        - getCompoundPaddingTop();
                resizeText(widthLimit, heightLimit);
            }
            super.onLayout(changed, left, top, right, bottom);
        }
    
        /**
         * Resize the text size with default width and height
         */
        public void resizeText() {
    
            // Height and width with a padding as a percentage of height
            int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
            int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
            resizeText(widthLimit, heightLimit);
        }
    
        /**
         * Resize the text size with specified width and height
         * 
         * @param width
         * @param height
         */
        public void resizeText(int width, int height) {
            CharSequence text = getText();
    
            // Do not resize if the view does not have dimensions or there is no
            // text
            if (text == null || text.length() == 0 || height <= 0 || width <= 0
                    || mTextSize == 0) {
                return;
            }
    
            // Get the text view's paint object
            TextPaint textPaint = getPaint();
    
            // Store the current text size
            float oldTextSize = textPaint.getTextSize();
    
            // Bisection method: fast & precise
            float lower = mMinTextSize;
            float upper = mMaxTextSize;
            int loop_counter = 1;
            float targetTextSize = (lower + upper) / 2;
            int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
            int textWidth = getTextWidth(text, textPaint, width, targetTextSize);
    
            while (loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) {
                targetTextSize = (lower + upper) / 2;
                textHeight = getTextHeight(text, textPaint, width, targetTextSize);
                textWidth = getTextWidth(text, textPaint, width, targetTextSize);
                if (textHeight > (height) || textWidth > (width))
                    upper = targetTextSize;
                else
                    lower = targetTextSize;
                loop_counter++;
            }
    
            targetTextSize = lower;
            textHeight = getTextHeight(text, textPaint, width, targetTextSize);
    
            // If we had reached our minimum text size and still don't fit, append
            // an ellipsis
            if (mAddEllipsis && targetTextSize == mMinTextSize
                    && textHeight > height) {
                // Draw using a static layout
                // modified: use a copy of TextPaint for measuring
                TextPaint paintCopy = new TextPaint(textPaint);
                paintCopy.setTextSize(targetTextSize);
                StaticLayout layout = new StaticLayout(text, paintCopy, width,
                        Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
                // Check that we have a least one line of rendered text
                if (layout.getLineCount() > 0) {
                    // Since the line at the specific vertical position would be cut
                    // off,
                    // we must trim up to the previous line
                    int lastLine = layout.getLineForVertical(height) - 1;
                    // If the text would not even fit on a single line, clear it
                    if (lastLine < 0) {
                        setText("");
                    }
                    // Otherwise, trim to the previous line and add an ellipsis
                    else {
                        int start = layout.getLineStart(lastLine);
                        int end = layout.getLineEnd(lastLine);
                        float lineWidth = layout.getLineWidth(lastLine);
                        float ellipseWidth = paintCopy.measureText(mEllipsis);
    
                        // Trim characters off until we have enough room to draw the
                        // ellipsis
                        while (width < lineWidth + ellipseWidth) {
                            lineWidth = paintCopy.measureText(text.subSequence(
                                    start, --end + 1).toString());
                        }
                        setText(text.subSequence(0, end) + mEllipsis);
                    }
                }
            }
    
            // Some devices try to auto adjust line spacing, so force default line
            // spacing
            // and invalidate the layout as a side effect
            setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
            setLineSpacing(mSpacingAdd, mSpacingMult);
    
            // Notify the listener if registered
            if (mTextResizeListener != null) {
                mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
            }
    
            // Reset force resize flag
            mNeedsResize = false;
        }
    
        // Set the text size of the text paint object and use a static layout to
        // render text off screen before measuring
        private int getTextHeight(CharSequence source, TextPaint originalPaint,
                int width, float textSize) {
            // modified: make a copy of the original TextPaint object for measuring
            // (apparently the object gets modified while measuring, see also the
            // docs for TextView.getPaint() (which states to access it read-only)
            TextPaint paint = new TextPaint(originalPaint);
            // Update the text paint object
            paint.setTextSize(textSize);
            // Measure using a static layout
            StaticLayout layout = new StaticLayout(source, paint, width,
                    Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
            return layout.getHeight();
        }
    
        // Set the text size of the text paint object and use a static layout to
        // render text off screen before measuring
        private int getTextWidth(CharSequence source, TextPaint originalPaint,
                int width, float textSize) {
            // Update the text paint object
            TextPaint paint = new TextPaint(originalPaint);
            // Draw using a static layout
            paint.setTextSize(textSize);
    
            StaticLayout layout = new StaticLayout(source, paint, width,
                    Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
    
            return (int) layout.getLineWidth(0);
        }
    }
    
    0 讨论(0)
  • 2020-11-21 06:13

    I have use code from chase and M-WaJeEh and I found some advantage & disadvantage here

    from chase

    Advantage:

    • it's perfect for 1 line TextView

    Disadvantage:

    • if it's more than 1 line with custom font some of text will disappear

    • if it's enable ellipse, it didn't prepare space for ellipse

    • if it's custom font (typeface), it didn't support

    from M-WaJeEh

    Advantage:

    • it's perfect for multi-line

    Disadvantage:

    • if set height as wrap-content, this code will start from minimum size and it will reduce to smallest as it can, not from the setSize and reduce by the limited width

    • if it's custom font (typeface), it didn't support

    0 讨论(0)
  • 2020-11-21 06:13

    My method is:

    public void changeTextSize(int initialSize, TextView tv) {
    
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        double width = displayMetrics.widthPixels / displayMetrics.xdpi;
        double height = displayMetrics.heightPixels / displayMetrics.ydpi;
    
        Log.i("LOG", "The width of the tested emulator is: " + width);
        Log.i("LOG", "The height of the tested emulator is: " + height);
    
        double scale = Math.min(width / 2.25, height / 4.0); //See the logcat >>> width = 2.25 and heigt = 4.0
        tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, (int) (initialSize * scale));
    
    }
    

    For example:

    changeTextSize(16, findViewById(R.id.myTextView));
    changeTextSize(12, findViewById(R.id.myEditText));
    
    0 讨论(0)
  • 2020-11-21 06:16

    I found the following to work nicely for me. It doesn't loop and accounts for both height and width. Note that it is important to specify the PX unit when calling setTextSize on the view.

    Paint paint = adjustTextSize(getPaint(), numChars, maxWidth, maxHeight);
    setTextSize(TypedValue.COMPLEX_UNIT_PX,paint.getTextSize());
    

    Here is the routine I use, passing in the getPaint() from the view. A 10 character string with a 'wide' character is used to estimate the width independent from the actual string.

    private static final String text10="OOOOOOOOOO";
    public static Paint adjustTextSize(Paint paint, int numCharacters, int widthPixels, int heightPixels) {
        float width = paint.measureText(text10)*numCharacters/text10.length();
        float newSize = (int)((widthPixels/width)*paint.getTextSize());
        paint.setTextSize(newSize);
    
        // remeasure with font size near our desired result
        width = paint.measureText(text10)*numCharacters/text10.length();
        newSize = (int)((widthPixels/width)*paint.getTextSize());
        paint.setTextSize(newSize);
    
        // Check height constraints
        FontMetricsInt metrics = paint.getFontMetricsInt();
        float textHeight = metrics.descent-metrics.ascent;
        if (textHeight > heightPixels) {
            newSize = (int)(newSize * (heightPixels/textHeight));
            paint.setTextSize(newSize);
        }
    
        return paint;
    }
    
    0 讨论(0)
提交回复
热议问题