Auto Scale TextView Text to Fit within Bounds

后端 未结 30 2659
囚心锁ツ
囚心锁ツ 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 05:58

    Providing this version of top answer rewritten on C# for those who codes on Xamarin.Android. Worked for me well.

     /**
     *               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.
     */
    
    
    using System;
    using Android.Content;
    using Android.Runtime;
    using Android.Text;
    using Android.Util;
    using Android.Widget;
    using Java.Lang;
    
    namespace App.GuestGuide.Droid.Controls
    {
        public class OnTextResizeEventArgs : EventArgs
        {
            public TextView TextView { get; set; }
            public float OldSize { get; set; }
            public float NewSize { get; set; }
        }
    
        /// <inheritdoc />
        /// <summary>
        /// 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.
        /// </summary>
        public class AutoResizeTextView : TextView
        {
            /// <summary>
            /// Minimum text size for this text view
            /// </summary>
            public static float MIN_TEXT_SIZE = 10;
    
            /// <summary>
            /// Our ellipse string
            /// </summary>
            private const string Ellipsis = "...";
    
    
            private float _mMaxTextSize;
    
            private float _mMinTextSize = MIN_TEXT_SIZE;
    
            /// <summary>
            /// Register subscriber to receive resize notifications
            /// </summary>
            public event EventHandler<OnTextResizeEventArgs> OnTextResize;
    
            /// <summary>
            /// Flag for text and/or size changes to force a resize
            /// </summary>
            private bool _needsResize;
    
            /// <summary>
            /// Text size that is set from code. This acts as a starting point for resizing
            /// </summary>
            private float _textSize;
    
            /// <summary>
            /// Text view line spacing multiplier
            /// </summary>
            private float _spacingMult = 1.0f;
    
            /// <summary>
            /// Text view additional line spacing
            /// </summary>
            private float _spacingAdd;
    
            /// <summary>
            /// Add ellipsis to text that overflows at the smallest text size
            /// </summary>
            public bool ShouldAddEllipsis { get; set; }
    
            /// <inheritdoc />
            /// <summary>
            /// Override the set text size to update our internal reference values
            /// </summary>
            public override float TextSize
            {
                get => base.TextSize;
                set
                {
                    base.TextSize = value;
                    _textSize = TextSize;
                }
            }
    
            /// <summary>
            /// Temporary upper bounds on the starting text size
            /// </summary>
            public float MaxTextSize
            {
                get => _mMaxTextSize;
                // Set the upper text size limit and invalidate the view
                set
                {
                    _mMaxTextSize = value;
                    RequestLayout();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// Lower bounds for text size
            /// </summary>
            public float MinTextSize
            {
                get => _mMinTextSize;
                //Set the lower text size limit and invalidate the view
                set
                {
                    _mMinTextSize = value;
                    RequestLayout();
                    Invalidate();
                }
            }
    
            public AutoResizeTextView(Context context) : this(context, null)
            {
            }
    
            public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0)
            {
            }
    
            public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
            {
                _textSize = TextSize;
            }
    
            public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
            {
                _textSize = TextSize;
            }
    
            protected AutoResizeTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
            {
                _textSize = TextSize;
            }
    
            /// <inheritdoc />
            /// <summary>
            /// When text changes, set the force resize flag to true and reset the text size.
            /// </summary>
            /// <param name="text"></param>
            /// <param name="start"></param>
            /// <param name="lengthBefore"></param>
            /// <param name="lengthAfter"></param>
            protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
            {
                _needsResize = true;
                // Since this view may be reused, it is good to reset the text size
                ResetTextSize();
            }
    
            /// <inheritdoc />
            /// <summary>
            /// If the text view size changed, set the force resize flag to true
            /// </summary>
            /// <param name="w"></param>
            /// <param name="h"></param>
            /// <param name="oldw"></param>
            /// <param name="oldh"></param>
            protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
            {
                if (w != oldw || h != oldh)
                {
                    _needsResize = true;
                }
            }
    
            public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
            {
                base.SetTextSize(unit, size);
                _textSize = TextSize;
            }
    
            /// <inheritdoc />
            /// <summary>
            /// Override the set line spacing to update our internal reference values
            /// </summary>
            /// <param name="add"></param>
            /// <param name="mult"></param>
            public override void SetLineSpacing(float add, float mult)
            {
                base.SetLineSpacing(add, mult);
                _spacingMult = mult;
                _spacingAdd = add;
            }
    
            /// <summary>
            /// Reset the text to the original size
            /// </summary>
            public void ResetTextSize()
            {
                if (_textSize > 0)
                {
                    base.SetTextSize(ComplexUnitType.Px, _textSize);
                    _mMaxTextSize = _textSize;
                }
            }
    
            /// <inheritdoc />
            /// <summary>
            /// Resize text after measuring
            /// </summary>
            /// <param name="changed"></param>
            /// <param name="left"></param>
            /// <param name="top"></param>
            /// <param name="right"></param>
            /// <param name="bottom"></param>
            protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
            {
                if (changed || _needsResize)
                {
                    var widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
                    var heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
                    ResizeText(widthLimit, heightLimit);
                }
    
                base.OnLayout(changed, left, top, right, bottom);
            }
    
            /// <summary>
            /// Resize the text size with default width and height
            /// </summary>
            public void ResizeText()
            {
                var heightLimit = Height - PaddingBottom - PaddingTop;
                var widthLimit = Width - PaddingLeft - PaddingRight;
                ResizeText(widthLimit, heightLimit);
            }
    
            /// <summary>
            /// Resize the text size with specified width and height
            /// </summary>
            /// <param name="width"></param>
            /// <param name="height"></param>
            public void ResizeText(int width, int height)
            {
                ICharSequence text = null;
    
                if (!string.IsNullOrEmpty(Text))
                {
                    text = new Java.Lang.String(Text);
                }
    
                // 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 || _textSize == 0)
                {
                    return;
                }
    
                if (TransformationMethod != null)
                {
                    text = TransformationMethod.GetTransformationFormatted(text, this);
                }
    
                // Get the text view's paint object
                var textPaint = Paint;
                // Store the current text size
                var oldTextSize = textPaint.TextSize;
                // If there is a max text size set, use the lesser of that and the default text size
                var targetTextSize = _mMaxTextSize > 0 ? System.Math.Min(_textSize, _mMaxTextSize) : _textSize;
    
                // Get the required text height
                var textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
    
                // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
                while (textHeight > height && targetTextSize > _mMinTextSize)
                {
                    targetTextSize = System.Math.Max(targetTextSize - 2, _mMinTextSize);
                    textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
                }
    
                // If we had reached our minimum text size and still don't fit, append an ellipsis
                if (ShouldAddEllipsis && targetTextSize == _mMinTextSize && textHeight > height)
                {
                    // Draw using a static layout
                    // modified: use a copy of TextPaint for measuring
                    var paint = new TextPaint(textPaint);
                    // Draw using a static layout
                    var layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, false);
    
                    // Check that we have a least one line of rendered text
                    if (layout.LineCount > 0)
                    {
                        // Since the line at the specific vertical position would be cut off,
                        // we must trim up to the previous line
                        var lastLine = layout.GetLineForVertical(height) - 1;
                        // If the text would not even fit on a single line, clear it
                        if (lastLine < 0)
                        {
                            Text = string.Empty;
                        }
                        // Otherwise, trim to the previous line and add an ellipsis
                        else
                        {
                            var start = layout.GetLineStart(lastLine);
                            var end = layout.GetLineEnd(lastLine);
                            var lineWidth = layout.GetLineWidth(lastLine);
                            var ellipseWidth = textPaint.MeasureText(Ellipsis);
    
                            // Trim characters off until we have enough room to draw the ellipsis
                            while (width < lineWidth + ellipseWidth)
                            {
                                lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1));
                            }
    
                            Text = text.SubSequence(0, end) + Ellipsis;
                        }
                    }
                }
    
                // Some devices try to auto adjust line spacing, so force default line spacing
                // and invalidate the layout as a side effect
                SetTextSize(ComplexUnitType.Px, targetTextSize);
                SetLineSpacing(_spacingAdd, _spacingMult);
    
                var notifyArgs = new OnTextResizeEventArgs
                {
                    TextView = this,
                    NewSize = targetTextSize,
                    OldSize = oldTextSize
                };
    
                // Notify the listener if registered
                OnTextResize?.Invoke(this, notifyArgs);
    
                // Reset force resize flag
                _needsResize = false;
            }
    
            /// <summary>
            /// Set the text size of the text paint object and use a static layout to render text off screen before measuring
            /// </summary>
            /// <param name="source"></param>
            /// <param name="paint"></param>
            /// <param name="width"></param>
            /// <param name="textSize"></param>
            /// <returns></returns>
            private int GetTextHeight(ICharSequence source, TextPaint paint, 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)
                // Update the text paint object
                var paintCopy = new TextPaint(paint)
                {
                    TextSize = textSize
                };
    
                // Measure using a static layout
                var layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true);
    
                return layout.Height;
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-21 05:59

    Warning, bug in Android Honeycomb and Ice Cream Sandwich

    Androids versions: 3.1 - 4.04 have a bug, that setTextSize() inside of TextView works only for the 1st time (1st invocation).

    Bug is described here: http://code.google.com/p/android/issues/detail?id=22493 http://code.google.com/p/android/issues/detail?id=17343#c9

    workaround is to add new line character to text assigned to TextView before changing size:

    final String DOUBLE_BYTE_SPACE = "\u3000";
    textView.append(DOUBLE_BYTE_SPACE);
    

    I use it in my code as follow:

    final String DOUBLE_BYTE_SPACE = "\u3000";
    AutoResizeTextView textView = (AutoResizeTextView) view.findViewById(R.id.aTextView);
    String fixString = "";
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1
       && android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {  
        fixString = DOUBLE_BYTE_SPACE;
    }
    textView.setText(fixString + "The text" + fixString);
    

    I add this "\u3000" character on left and right of my text, to keep it centered. If you have it aligned to left then append to the right only. Of course it can be also embedded with AutoResizeTextView widget, but I wanted to keep fix code outside.

    0 讨论(0)
  • 2020-11-21 05:59

    Since I've been looking for this forever, and I found a solution a while ago which is missing here, I'm gonna write it here, for future reference also.

    Note: this code was taken directly from Google Android Lollipop dialer a while back, I don't remember If changes were made at the time. Also, I don't know which license is this under, but I have reason to think it is Apache 2.0.

    Class ResizeTextView, the actual View

    public class ResizeTextView extends TextView {
    
    private final int mOriginalTextSize;
    private final int mMinTextSize;
    private final static int sMinSize = 20;
    public ResizeTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mOriginalTextSize = (int) getTextSize();
        mMinTextSize = (int) sMinSize;
    }
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
    }
    

    This ResizeTextView class could extend TextView and all its children as I undestand, so EditText as well.

    Class ViewUtil with method resizeText(...)

    /*
    * Copyright (C) 2012 The Android Open Source Project
    *
    * Licensed under the Apache License, Version 2.0 (the "License");
    * you may not use this file except in compliance with the License.
    * You may obtain a copy of the License at
    *
    *      http://www.apache.org/licenses/LICENSE-2.0
    *
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    * See the License for the specific language governing permissions and
    * limitations under the License.
    */
    
    import android.graphics.Paint;
    import android.util.TypedValue;
    import android.widget.TextView;
    
    public class ViewUtil {
    
        private ViewUtil() {}
    
        public static void resizeText(TextView textView, int originalTextSize, int minTextSize) {
            final Paint paint = textView.getPaint();
            final int width = textView.getWidth();
            if (width == 0) return;
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
            float ratio = width / paint.measureText(textView.getText().toString());
            if (ratio <= 1.0f) {
                textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                        Math.max(minTextSize, originalTextSize * ratio));
            }
        }
    }
    

    You should set your view as

    <yourpackage.yourapp.ResizeTextView
                android:layout_width="match_parent"
                android:layout_height="64dp"
                android:gravity="center"
                android:maxLines="1"/>
    

    Hope it helps!

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

    Here's an enumeration of what else I've found for anyone still searching:

    1) Here's a solution that recursively re-paints the textview until it fits. This means literally watching your text shrink into place, but at least it fits when it's done. The code will need some tweaking to implement, but it's mostly there.

    2) You can try hacking together a custom solution like this, or dunni's class in this, which is what I did using the getPaint().measureText(str) to search for the right size, but it got a lot messier since I need it to wrap only on whitespace...

    3) You can keep searching- I've tried more alternatives than I can count. Ted's advice on StaticLayout hasn't paid off for me but maybe there's something there; I tried using the StaticLayout.getEllipsis(line) to determine if text was going off screen, to no effect. See my (presently un-answered) post about that here.

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

    My implementation is a bit more complex, but comes with the following goodies:

    • takes the available width and available height into account
    • works with single line and multiline labels
    • uses ellipsis in case the minimum font size is hit
    • since the internal text representation is changed, remembers the originally set text in a separate variable
    • ensures that the canvas is always only as big as it needs to be, while it uses all the available height of the parent
    /**
     * 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.
     * 
     * Based on the original work from Chase Colburn
     * &lt;http://stackoverflow.com/a/5535672/305532>
     *
     * @author Thomas Keller &lt;me@thomaskeller.biz>
     */
    public class AutoResizeTextView extends TextView {
    
        // in dip
        private static final int MIN_TEXT_SIZE = 20;
    
        private static final boolean SHRINK_TEXT_SIZE = true;
    
        private static final char ELLIPSIS = '\u2026';
    
        private static final float LINE_SPACING_MULTIPLIER_MULTILINE = 0.8f;
    
        private static final float LINE_SPACING_MULTIPLIER_SINGLELINE = 1f;
    
        private static final float LINE_SPACING_EXTRA = 0.0f;
    
        private CharSequence mOriginalText;
    
        // temporary upper bounds on the starting text size
        private float mMaxTextSize;
    
        // lower bounds for text size
        private float mMinTextSize;
    
        // determines whether we're currently in the process of measuring ourselves,
        // so we do not enter onMeasure recursively
        private boolean mInMeasure = false;
    
        // if the text size should be shrinked or if the text size should be kept
        // constant and only characters should be removed to hit the boundaries
        private boolean mShrinkTextSize;
    
        public AutoResizeTextView(Context context) {
            this(context, null);
            init(context, null);
        }
    
        public AutoResizeTextView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
            init(context, attrs);
        }
    
        public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init(context, attrs);
        }
    
        private void init(Context context, AttributeSet attrs) {
            // the current text size is used as maximum text size we can apply to
            // our widget
            mMaxTextSize = getTextSize();
            if (attrs != null) {
                TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoResizeTextView);
                mMinTextSize = a.getFloat(R.styleable.AutoResizeTextView_minFontSize, MIN_TEXT_SIZE);
                mShrinkTextSize = a.getBoolean(R.styleable.AutoResizeTextView_shrinkTextSize, SHRINK_TEXT_SIZE);
                a.recycle();
            }
        }
    
        @Override
        public void setTextSize(float size) {
            mMaxTextSize = size;
            super.setTextSize(size);
        }
    
        /**
         * Returns the original, unmodified text of this widget
         * 
         * @return
         */
        public CharSequence getOriginalText() {
            // text has not been resized yet
            if (mOriginalText == null) {
                return getText();
            }
            return mOriginalText;
        }
    
        @Override
        public void setText(CharSequence text, BufferType type) {
            if (!mInMeasure) {
                mOriginalText = text.toString();
            }
            super.setText(text, type);
        }
    
        @SuppressLint("DrawAllocation")
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            mInMeasure = true;
            try {
                int availableWidth = MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft()
                        - getCompoundPaddingRight();
                int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - getCompoundPaddingTop()
                        - getCompoundPaddingBottom();
    
                // Do not resize if the view does not have dimensions or there is no
                // text
                if (mOriginalText == null || mOriginalText.length() == 0 || availableWidth <= 0) {
                    return;
                }
    
                TextPaint textPaint = getPaint();
    
                // start with the recorded max text size
                float targetTextSize = mMaxTextSize;
                String originalText = mOriginalText.toString();
                String finalText = originalText;
    
                Rect textSize = getTextSize(originalText, textPaint, targetTextSize);
                boolean textExceedsBounds = textSize.height() > availableHeight || textSize.width() > availableWidth;
                if (mShrinkTextSize && textExceedsBounds) {
                    // check whether all lines can be rendered in the available
                    // width / height without violating the bounds of the parent and
                    // without using a text size that is smaller than the minimum
                    // text size
                    float heightMultiplier = availableHeight / (float) textSize.height();
                    float widthMultiplier = availableWidth / (float) textSize.width();
                    float multiplier = Math.min(heightMultiplier, widthMultiplier);
                    targetTextSize = Math.max(targetTextSize * multiplier, mMinTextSize);
    
                    // measure again
                    textSize = getTextSize(finalText, textPaint, targetTextSize);
                }
    
                // we cannot shrink the height further when we hit the available
                // height, but we can shrink the width by applying an ellipsis on
                // each line
                if (textSize.width() > availableWidth) {
                    StringBuilder modifiedText = new StringBuilder();
                    String lines[] = originalText.split(System.getProperty("line.separator"));
                    for (int i = 0; i < lines.length; i++) {
                        modifiedText.append(resizeLine(textPaint, lines[i], availableWidth));
                        // add the separator back to all but the last processed line
                        if (i != lines.length - 1) {
                            modifiedText.append(System.getProperty("line.separator"));
                        }
                    }
                    finalText = modifiedText.toString();
    
                    // measure again
                    textSize = getTextSize(finalText, textPaint, targetTextSize);
                }
    
                textPaint.setTextSize(targetTextSize);
                boolean isMultiline = finalText.indexOf('\n') > -1;
                // do not include extra font padding (for accents, ...) for
                // multiline texts, this will prevent proper placement with
                // Gravity.CENTER_VERTICAL
                if (isMultiline) {
                    setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_MULTILINE);
                    setIncludeFontPadding(false);
                } else {
                    setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_SINGLELINE);
                    setIncludeFontPadding(true);
                }
    
                // according to
                // <http://code.google.com/p/android/issues/detail?id=22493>
                // we have to add a unicode character to trigger the text centering
                // in ICS. this particular character is known as "zero-width" and
                // does no harm.
                setText(finalText + "\u200B");
    
                int measuredWidth = textSize.width() + getCompoundPaddingLeft() + getCompoundPaddingRight();
                int measuredHeight = textSize.height() + getCompoundPaddingTop() + getCompoundPaddingBottom();
    
                // expand the view to the parent's height in case it is smaller or
                // to the minimum height that has been set
                // FIXME: honor the vertical measure mode (EXACTLY vs AT_MOST) here
                // somehow
                measuredHeight = Math.max(measuredHeight, MeasureSpec.getSize(heightMeasureSpec));
                setMeasuredDimension(measuredWidth, measuredHeight);
            } finally {
                mInMeasure = false;
            }
        }
    
        private Rect getTextSize(String text, TextPaint textPaint, float textSize) {
            textPaint.setTextSize(textSize);
            // StaticLayout depends on a given width in which it should lay out the
            // text (and optionally also split into separate lines).
            // Therefor we calculate the current text width manually and start with
            // a fake (read: maxmimum) width for the height calculation.
            // We do _not_ use layout.getLineWidth() here since this returns
            // slightly smaller numbers and therefor would lead to exceeded text box
            // drawing.
            StaticLayout layout = new StaticLayout(text, textPaint, Integer.MAX_VALUE, Alignment.ALIGN_NORMAL, 1f, 0f, true);
            int textWidth = 0;
            String lines[] = text.split(System.getProperty("line.separator"));
            for (int i = 0; i < lines.length; ++i) {
                textWidth = Math.max(textWidth, measureTextWidth(textPaint, lines[i]));
            }
            return new Rect(0, 0, textWidth, layout.getHeight());
        }
    
        private String resizeLine(TextPaint textPaint, String line, int availableWidth) {
            checkArgument(line != null && line.length() > 0, "expected non-empty string");
            int textWidth = measureTextWidth(textPaint, line);
            int lastDeletePos = -1;
            StringBuilder builder = new StringBuilder(line);
            while (textWidth > availableWidth && builder.length() > 0) {
                lastDeletePos = builder.length() / 2;
                builder.deleteCharAt(builder.length() / 2);
                // don't forget to measure the ellipsis character as well; it
                // doesn't matter where it is located in the line, it just has to be
                // there, since there are no (known) ligatures that use this glyph
                String textToMeasure = builder.toString() + ELLIPSIS;
                textWidth = measureTextWidth(textPaint, textToMeasure);
            }
            if (lastDeletePos > -1) {
                builder.insert(lastDeletePos, ELLIPSIS);
            }
            return builder.toString();
        }
    
        // there are several methods in Android to determine the text width, namely
        // getBounds() and measureText().
        // The latter works for us the best as it gives us the best / nearest
        // results without that our text canvas needs to wrap its text later on
        // again.
        private int measureTextWidth(TextPaint textPaint, String line) {
            return Math.round(textPaint.measureText(line));
        }
    }
    

    [revised on 2012-11-21]

    • fixed the placement of the ellipsis (off-by-one error)
    • reworked text size calculation; now always the full text including line breaks is measured, to fix problems when the addition of the height of two single measured lines just didn't lead to the same result as the measurement of the height of the text as a whole
    • instead of looping to find the smallest available text size, just calculate it after the first measurement
    0 讨论(0)
  • 2020-11-21 06:04

    Actually a solution is in Google's DialogTitle class... though it's not as effective as the accepted one, it's a lot simpler and is easy to adapt.

    public class SingleLineTextView extends TextView {
    
      public SingleLineTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setSingleLine();
        setEllipsize(TruncateAt.END);
      }
    
      public SingleLineTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setSingleLine();
        setEllipsize(TruncateAt.END);
      }
    
      public SingleLineTextView(Context context) {
        super(context);
        setSingleLine();
        setEllipsize(TruncateAt.END);
      }
    
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
        final Layout layout = getLayout();
        if (layout != null) {
          final int lineCount = layout.getLineCount();
          if (lineCount > 0) {
            final int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
            if (ellipsisCount > 0) {
    
              final float textSize = getTextSize();
    
              // textSize is already expressed in pixels
              setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1));
    
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            }
          }
        }
      }
    
    }
    
    0 讨论(0)
提交回复
热议问题