[Android] Automatically adjust textScaleX of TextView

What i made

There are many experiences where you try to make TextView display a long string in a situation where only a limited space is available, and the string is cut off in the middle and it does not look good. There may be various solutions, but for now we'll focus on a TextView attribute called textScaleX. It is a value that indicates the horizontal expansion / contraction ratio of the character to be displayed. If the value is larger than 1, the character string is expanded, and if it is smaller than 1, the character string is contracted. I created a View that automatically adjusts the value of this textScaleX to compress the length of the string when displaying a string that does not fit in the maximum width of the TextView. Let's also call the class name ExpandableTextView.

Demo image

The background of the ExpandableTextView is grayed out for clarity. expandableTextView_demo.gif

What can i do

--Stretch the character string to be displayed horizontally according to the maximum width of View (textScaleX = (0.0, 1.0]) --OnLayout () and setText () automatically adjust each time the situation changes --Prepare the lower limit of textScaleX, and if the character string does not fit even after being compressed to the minimum magnification, replace it with an alternative character string (cut the end and replace it with "...").

View Width automatic adjustment behavior

Cases for layout_width of LayoutParam. The value of android: layout_width that is statically defined in XML.

Source code

Put it on Github I will.

Implementation description

Confirmed with SDK version = 26

Inherit TextView

I want to implement it easily, so I inherit TextView and play around with it. Changed behavior by overriding some methods.

Control of the displayed character string

Provides control to expand / contract or replace the character string with an alternative character string as needed for the maximum width allowed

ExpandableTextView#updateText



private Paint mTextPaint;           //Object to draw a character
private CharSequence mDisplayedText //The character string actually displayed

private void updateText(CharSequence text, BufferType type, int widthSize){
    mTextPaint.setTextScaleX(1f);
    float length = mTextPaint.measureText(text, 0, text.length());
    int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
    if ( length + padding > widthSize ){
        float scale = (widthSize - padding) / length;
        text =  modifyText(scale, text, widthSize - padding);
    }
    mDisplayedText = text;
    super.setText(text, type);
}

/**
 *Shrinks the string horizontally to fit the specified maximum width.
 *Shrinkage rate{@link #mMinTextScaleX}If it is less than, replace it with an alternative string that fits the maximum width at its minimum ratio.
 * @param scale     rough value, with which text width can be adjusted to maxLength
 * @param text      raw text
 * @param maxLength max length of text
 * @return modified text
 */
private CharSequence modifyText(float scale, CharSequence text, float maxLength){
    //Source code reference
}

Specifically, modifyText () is used to shrink the string or replace it with an alternative string. Note that the length of the string measured by Paint # measureText is

--** Not proportional to the ratio specified by Paint # setTextScaleX ** --The number of characters in the string is ** not proportional to String # length **

Therefore, in order to search for the desired magnification textScaleX, the magnification is changed little by little with an appropriate hit and the best value is adopted. Similarly, in order to determine an alternative character string that fits the character string in the maximum width, it has become a disappointing implementation method of searching while cutting from the end one character at a time. The code is dirty so I won't put it here.

Control the width of View with View # onMeasure (int, int)

This method is called to measure the size of this View when it is placed by the parent View. For more information on the role of onMeasure (), see [Qiita] Understanding onMeasure and onLayout. This time I'm only interested in width, so I'll add a request to measureSpec for width. Since it is troublesome to implement all onMeasure () by yourself, pass it to super # onMeasure () as it is except for the width and throw it to the parent class.

ExpandableTextView#onMeasure


private Paint mTextPaint;           //Object to draw a character
private CharSequence mCurrentText;  //The character string you want to display
private CharSequence mDisplayedText;//The character string actually displayed
private int mMaxWidth;              //Maximum width
private int mRawWidthMeasureSpec;   //Request for the size of this View specified by the parent

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    //Add request only for width
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    CharSequence text = mCurrentText;
    mTextPaint.setTextScaleX(1f);
    float length = mTextPaint.measureText(text, 0, text.length());

    int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
    int requestedWidth = 0;

    switch ( widthMode ){
        case MeasureSpec.EXACTLY:
            //Fixed at the specified width regardless of the character string to be displayed
            requestedWidth = widthSize;
            //Adjust the displayed character string according to the fixed width
            updateText(mCurrentText, BufferType.NORMAL, widthSize);
            break;
        case MeasureSpec.AT_MOST:
            int max = Math.min(widthSize, mMaxWidth);
            if ( length + padding > max ){
                //Compress if it does not fit in the maximum width
                requestedWidth = max;
                float scale = (max - padding) / length;
                CharSequence modified = modifyText(scale, text, max - padding);
                if ( !mDisplayedText.equals(modified) ){
                    mDisplayedText = modified;
                    super.setText(modified, BufferType.NORMAL);
                }
            }else{
                //If it fits, adjust the width of View to the width of the character string
                requestedWidth = (int)Math.ceil(length + padding);
            }
            break;
        case MeasureSpec.UNSPECIFIED:
            //If not specified, decide on your own
            requestedWidth = (int)Math.ceil(length + padding);
            mTextPaint.setTextScaleX(1f);
            mDisplayedText = text;
            break;
    }
    
    mRawWidthMeasureSpec = widthMeasureSpec;

    //Add request as width is fixed
    int calcWidthMeasureSpec = MeasureSpec.makeMeasureSpec(requestedWidth, MeasureSpec.EXACTLY);
   
    // onMeasure()Leave the detailed implementation of
    super.onMeasure(calcWidthMeasureSpec, heightMeasureSpec);
}

Corresponds to changes in the character string to be displayed and the environment

Implementation to behave as intended even when the character string to be displayed by setText () changes. The important point here is when TextView calls layout (). If the size of a normal TextView depends on its contents (when MeasureSpec.AT_MOST is specified by onMeasure (), such as by specifying wrap_content in LayoutParam),

--The character string displayed by setText () changes --SetMaxWidth () changes the maximum width --textScaleX changes with setTextScaleX ()

Call layout () at the timing of to recalculate the size of View. This time in addition to the above

--setMinTextScaleX () changes the minimum value of textScaleX

Implement to call layout () even at the timing of.

The above is the case where the size of View depends on the contents. If the width is specified by MeasureSpec.EXACTLY from the parent, adjust the character string and expansion / contraction ratio displayed by updateText () according to the fixed width.

ExpandableTextView



private CharSequence mCurrentText;  //The character string you want to display
private CharSequence mDisplayedText //The character string actually displayed
private int mMaxWidth;              //Maximum width
private int mRawWidthMeasureSpec;   //Request for the size of this View specified by the parent
private float mMinTextScaleX;       //Minimum value of textScalex

@Override
public void setMaxWidth(int width){
    // super#Since mMaxWidth is private, record it yourself
    if ( width <= 0 ) return;
    if ( width != mMaxWidth ){
        mMaxWidth = width;
        mRequestMeasure = true;
        super.setMaxWidth(width);
        //maxWidth is a setting (wrap) where the width of this View depends on the contents_Only valid for content etc.)
        //In such cases the parent class is layout()Call
    }
}

/**
 *Specify the minimum value of the horizontal scale of the string.
 * @param scale in range of (0,1]
 * @see TextPaint#setTextScaleX(float)
 */
public void setMinTextScaleX(float scale){
    if ( scale <= 0 || scale > 1 ) return;
    if ( mMinTextScaleX != scale ){
        mMinTextScaleX = scale;
        if ( MeasureSpec.getMode(mRawWidthMeasureSpec) == MeasureSpec.EXACTLY ){
            // layout()No need to
            updateText(mCurrentText, BufferType.NORMAL, MeasureSpec.getSize(mRawWidthMeasureSpec));
        }else{
            // layout()And recalculate the size
            requestLayout();
        }
    }
}


//#setText(char[], int, int) <-Cannot be overridden in final
//Other than this is via here
@Override
public void setText(CharSequence text, BufferType type){
    if ( text == null ) return;
    if ( mCurrentText != null && mCurrentText.equals(text) ) return;

    mCurrentText = text;

    if ( MeasureSpec.getMode(mRawWidthMeasureSpec) == MeasureSpec.EXACTLY ){
        //Parent class is layout()No, no need to
        updateText(text, type, MeasureSpec.getSize(mRawWidthMeasureSpec));
    }else{
        //Parent class layout()To do
        super.setText(text, type);
    }
}

Two setTextScaleX () pitfalls

In this implementation, setTextScaleX () is used to specify the expansion / contraction magnification of the character string for the Paint object used for drawing the characters obtained by TextView # getPaint (). There is a note in getPaint () that you shouldn't mess with Paint properties (original: [Use this only to consult the Paint's properties and not to change them.](Https://developer.android.com/reference/android. /widget/TextView.html#getPaint())), textScaleX has the following implementation points.

TextView


public void setTextScaleX(float size) {
    if (size != mTextPaint.getTextScaleX()) {
        mUserSetTextScaleX = true;
        mTextPaint.setTextScaleX(size);
        if (mLayout != null) {
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }
}

private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
    /*Omission*/
    if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);   
    /*Omission*/
}

If you do not specify from setTextScaleX () of TextView, textScaleX = 1.0f will overwrite ***! *** *** I got it at first. Solution: Call TextView # setTextScaleX () at least once.

ExpandableTextView


public class ExpandableTextView extends AppCompatTextView{

    public ExpandableTextView(Context context, AttributeSet attr, int defaultAttr){
        super(context, attr, defaultAttr);

        /*Omission*/

        //You can specify a value different from the current value, so getTextScaleX() / 2f
        super.setTextScaleX(getTextScaleX() / 2f);
    }

Recommended Posts

[Android] Automatically adjust textScaleX of TextView
Automatically adjust the height of Xib files
Definition of Android constants
How to implement one-line display of TextView in Android development
Automatically adjust the height of the WebView dialog with the size of the content