The story begins with a product requirement to make a little red book text folding function, and that leads to the next series of things. But after the implementation, I also know a lot about TextView interception, the specific effects are as follows:

To sum up a few points to pay attention to when implementing:

  1. According to “… After a certain number of lines are intercepted, it is displayed directly at the end of the last line
  2. Fold up appears on the next line of the entire text and is right-aligned
  3. Animation effects that unfold and collapse

If the generalization isn’t perfect, please also point out that you can skip to the end of this article and look at the ExpandableTextView code

Text interception

Referring to a number of articles, many implementations cut the largest line of text and add a button to the next line of text. This doesn’t work, so you can PASS it.

TextView set Android :maxLines and ellipSize to End, but… Replace with… Unfortunately, the system does not provide direct replacement… The API.

However, as you can see from the source code of the TextView that deals with android: EllipSize property, StaticLayout is used as a tool class to help us achieve this effect. StaticLayout is an Android utility class that handles text newlines.

There are BoringLayout, StaticLayout and DynamicLayout utility classes

  • BoringLayoutIs used for single-line display
  • StaticLayoutIt’s for text that can’t be changed
  • DynamicLayoutIs for editable text and updates itself.

Now we need to know how to use StaticLayout, there are three constructors we can use directly, right

public StaticLayout(CharSequence source, TextPaint paint,
                    int width,
                    Alignment align, float spacingmult, float spacingadd,
                    boolean includepad) {
    this(source, 0, source.length(), paint, width, align,
         spacingmult, spacingadd, includepad);
}

public StaticLayout(CharSequence source, int bufstart, int bufend,
                    TextPaint paint, int outerwidth,
                    Alignment align,
                    float spacingmult, float spacingadd,
                    boolean includepad) {
    this(source, bufstart, bufend, paint, outerwidth, align,
         spacingmult, spacingadd, includepad, null.0);
}

public StaticLayout(CharSequence source, int bufstart, int bufend,
        TextPaint paint, int outerwidth,
        Alignment align,
        float spacingmult, float spacingadd,
        boolean includepad,
        TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    this(source, bufstart, bufend, paint, outerwidth, align,
            TextDirectionHeuristics.FIRSTSTRONG_LTR,
            spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
}
Copy the code

Before you use it, you should know a little about the function of the parameters in the method

  • CharSequence source: string that requires a line
  • Int bufstart: the first position in the string that requires a line
  • Int buza: Where does the string that needs branches end
  • TextPaint paint: Paintbrush object
  • Int outerWidth: The width of the layout, which is automatically wrapped when characters exceed the width, i.e. the width of the content to display
  • Alignment align: Indicates the AlignmentALIGN_CENTER,ALIGN_NORMAL,ALIGN_OPPOSITEThree kinds of
  • Float spacingmult: multiple of line spacing, equivalent toandroid:lineSpacingMultiplier
  • Float spacingAdd: Additional line spacing, equivalent toandroid:lineSpacingExtra
  • Boolean incluDEPad: Whether to include padding
  • TruncateAt ellipsize: ellipse position,TruncateAtIs aenum, there areSTART,MIDDLE,END,MARQUEE(Running lantern) andEND_SMALLBut it was hidden
  • Int ellipsizedWidth: Position to start ellipsis

All we need to do is use the constructor with the fewest arguments

private Layout createStaticLayout(SpannableStringBuilder spannable) {
    int contentWidth = initWidth - getPaddingLeft() - getPaddingRight();
    return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
            getLineSpacingMultiplier(), getLineSpacingExtra(), false);
}
Copy the code

Once we get the text’s StaticLayout object, we can use the getLineCount() method to know if the text will exceed our maxLines, Use the getLineEnd(int line) method to find the position of the last character of the last line in the text. The key codes are as follows:

Layout layout = createStaticLayout(tempText);
mExpandable = layout.getLineCount() > maxLines;
if(mExpandable){int endPos = layout.getLineEnd(maxlines-1); mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos)); SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);if(mOpenSuffixSpan ! = null) { tempText2.append(tempText2); } Layout = createStaticLayout(tempText2);while (tempLayout.getLineCount() > maxLines) {
        int lastSpace = mCloseSpannableStr.length() - 1;
        if (lastSpace == -1) {
            break;
        }
        mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
        tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
        if(mOpenSuffixSpan ! = null) { tempText2.append(mOpenSuffixSpan); } tempLayout = createStaticLayout(tempText2); } mCLoseHeight = templayout.getheight () + getPaddingTop() + getPaddingBottom(); mCloseSpannableStr.append(ELLIPSIS_STRING);if (mOpenSuffixSpan != null) {
        mCloseSpannableStr.append(mOpenSuffixSpan);
    }
}
Copy the code

In this way, the problem of text interception is solved. The mCloseSpannableStr in the code is the text object that needs to be displayed after being folded. In consideration of the possibility of expressions or pictures in the text, SpannableStringBuilder is used as the text object.

“Fold up” right alignment

For packed text, use SpannableString and set new AlignmentSpan.Standard(layout.alignment.ALIGN_OPPOSITE) to display the packed text as right-aligned. You need to add ‘\n’ between the original text and the enclosing text.

private void updateCloseSuffixSpan(a) {
    if (TextUtils.isEmpty(mCloseSuffixStr)) {
        mCloseSuffixSpan = null;
        return;
    }
    mCloseSuffixSpan = new SpannableString(mCloseSuffixStr);
    mCloseSuffixSpan.setSpan(new ForegroundColorSpan(mCloseSuffixColor), 0, mCloseSuffixStr.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    if (mCloseInNewLine) {
        AlignmentSpan alignmentSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE);
        mCloseSuffixSpan.setSpan(alignmentSpan, 0, mCloseSuffixStr.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); }}Copy the code

Animation effects

Animations are relatively easy to perform by changing the height of the TextView in the applyTransformation method

class ExpandCollapseAnimation extends Animation {
    private final View mTargetView;// Animate the view
    private final int mStartHeight;// Start height of animation execution
    private final int mEndHeight;// The height after the animation ends

    ExpandCollapseAnimation(View target, int startHeight, int endHeight) {
        mTargetView = target;
        mStartHeight = startHeight;
        mEndHeight = endHeight;
        setDuration(400);
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        // Calculate the height that should be displayed each time, change the height of the execution view, implement the animation
        mTargetView.getLayoutParams().height = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight); mTargetView.requestLayout(); }}Copy the code

The height of the expanded and folded TextView is retrieved from getHeight() of the StaticLayout text handler. There is also the TextView height and text update before and after the animation, the specific code is as follows:

/** Perform the unfold animation */
private void executeOpenAnim(a) {
    // Create an unfolding animation
    if (mOpenAnim == null) {
        mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight);
        mOpenAnim.setFillAfter(true);
        mOpenAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE);
                setText(mOpenSpannableStr);
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                // After the animation ends, the textView sets the state to expand
                getLayoutParams().height = mOpenHeight;
                requestLayout();
                animating = false;
            }

            @Override
            public void onAnimationRepeat(Animation animation) {}}); }if (animating) {
        return;
    }
    animating = true;
    clearAnimation();
    // Perform the animation
    startAnimation(mOpenAnim);
}

/** Perform the fold animation */
private void executeCloseAnim(a) {
    // Create a collapse animation
    if (mCloseAnim == null) {
        mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight);
        mCloseAnim.setFillAfter(true);
        mCloseAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {}@Override
            public void onAnimationEnd(Animation animation) {
                animating = false;
                ExpandableTextView.super.setMaxLines(mMaxLines);
                setText(mCloseSpannableStr);
                getLayoutParams().height = mCLoseHeight;
                requestLayout();
            }

            @Override
            public void onAnimationRepeat(Animation animation) {}}); }if (animating) {
        return;
    }
    animating = true;
    clearAnimation();
    // Perform the animation
    startAnimation(mCloseAnim);
}
Copy the code

The final result

Directions for use

methods instructions
initWidth(int width) Initialize theExpandableTextWidth must be insetOriginalText()Before the call
setMaxLines(int maxLines) Sets the maximum number of rows to display
setOpenSuffix(String openSuffix) Set up theNeed to openIs displayed. The default isan
setOpenSuffixColor(@ColorInt int openSuffixColor) Set up theNeed to openDisplays the text color of the text
setCloseSuffix(String closeSuffix) Set up theNeed to pack upIs displayed. The default isPack up
setCloseSuffixColor(@ColorInt int closeSuffixColor) Set up theNeed to pack upDisplays the text color of the text
setCloseInNewLine(boolean closeInNewLine) Set up theNeed to pack upWhen folded up the text is another line
setOpenAndCloseCallback(OpenAndCloseCallback callback) Set the expanded & folded click Callback
setCharSequenceToSpannableHandler(CharSequenceToSpannableHandler handler) Set text conversion toSpannableThe preprocessing callback can handle special text styles

Finally finished

After adding animation effects, use setMovementMethod (LinkMovementMethod getInstance ()); Add click events for expansion and collapse, resulting in incorrect animation for collapse. TextView’s scrollY value was changed during the animation execution, so we can call the setScrollY(0) method of TextView in applyTransformation. No problem has been found in this way.

Also consider ExpandableTextView should not handle emoji expressions and so on some special text, so provides CharSequenceToSpannableHandler extension interface, can be extended to handle the display of the text.

The above isExpandableTextViewThe whole implementation process and ideas, share, if there is a better way to welcome the discussion in the comments

Expandabletext-example

Android TextView can be customized to expand the fold TextView, expand the fold button to follow the text content to achieve the view all and fold function