Auto Scale TextView Text to Fit within Bounds

后端 未结 30 2878
囚心锁ツ
囚心锁ツ 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: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 
     *
     * 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);
        }
    }
    

提交回复
热议问题