Android - Expandable TextView with Animation

AndroidAndroid AnimationTextview

Android Problem Overview


I have a TextView which firstly shows a small portion of a long text.

The user can press a "see more" button to expand the TextView and see the rest of that text.

Making tests, I can reach that by simply interchange the value of TextView.setMaxLines between 4 for collapsing and Integer.MAX_VALUE for expanding.

Now, I would like that this behavior would be accompanied by an animation. I know that in this question one solution is almost done, but I tried to implement it and I have no success.

Can someone help me with this?

Android Solutions


Solution 1 - Android

You can check my blog post on ExpandableTexTView:

The idea is, initially the TextView will show a small portion of a long text and when it is clicked, it will show the rest of the text.

So here is the code that how I solved it.

package com.rokonoid.widget;
 
import android.content.Context;
import android.content.res.TypedArray;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
 * User: Bazlur Rahman Rokon
 * Date: 9/7/13 - 3:33 AM
 */
public class ExpandableTextView extends TextView {
    private static final int DEFAULT_TRIM_LENGTH = 200;
    private static final String ELLIPSIS = ".....";
 
    private CharSequence originalText;
    private CharSequence trimmedText;
    private BufferType bufferType;
    private boolean trim = true;
    private int trimLength;
 
    public ExpandableTextView(Context context) {
        this(context, null);
    }
 
    public ExpandableTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
 
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView);
        this.trimLength = typedArray.getInt(R.styleable.ExpandableTextView_trimLength, DEFAULT_TRIM_LENGTH);
        typedArray.recycle();
 
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                trim = !trim;
                setText();
                requestFocusFromTouch();
            }
        });
    }
 
    private void setText() {
        super.setText(getDisplayableText(), bufferType);
    }
 
    private CharSequence getDisplayableText() {
        return trim ? trimmedText : originalText;
    }
 
    @Override
    public void setText(CharSequence text, BufferType type) {
        originalText = text;
        trimmedText = getTrimmedText(text);
        bufferType = type;
        setText();
    }
 
    private CharSequence getTrimmedText(CharSequence text) {
        if (originalText != null && originalText.length() > trimLength) {
            return new SpannableStringBuilder(originalText, 0, trimLength + 1).append(ELLIPSIS);
        } else {
            return originalText;
        }
    }
 
    public CharSequence getOriginalText() {
        return originalText;
    }
 
    public void setTrimLength(int trimLength) {
        this.trimLength = trimLength;
        trimmedText = getTrimmedText(originalText);
        setText();
    }
 
    public int getTrimLength() {
        return trimLength;
    }
}

And add the following line in your attr.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ExpandableTextView">
        <attr name="trimLength" format="integer"/>
    </declare-styleable>
</resources>

Put the following in your main.xml

<?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent">

     <com.rokonoid.widget.ExpandableTextView
         android:id="@+id/lorem_ipsum"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"/>

 </LinearLayout>

And test your activity

package com.rokonoid.widget;
 
import android.app.Activity;
import android.os.Bundle;
 
public class MyActivity extends Activity {
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        String yourText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
                "Ut volutpat interdum interdum. Nulla laoreet lacus diam, vitae " +
                "sodales sapien commodo faucibus. Vestibulum et feugiat enim. Donec " +
                "semper mi et euismod tempor. Sed sodales eleifend mi id varius. Nam " +
                "et ornare enim, sit amet gravida sapien. Quisque gravida et enim vel " +
                "volutpat. Vivamus egestas ut felis a blandit. Vivamus fringilla " +
                "dignissim mollis. Maecenas imperdiet interdum hendrerit. Aliquam" +
                " dictum hendrerit ultrices. Ut vitae vestibulum dolor. Donec auctor ante" +
                " eget libero molestie porta. Nam tempor fringilla ultricies. Nam sem " +
                "lectus, feugiat eget ullamcorper vitae, ornare et sem. Fusce dapibus ipsum" +
                " sed laoreet suscipit. ";
 
        ExpandableTextView expandableTextView = (ExpandableTextView) findViewById(R.id.lorem_ipsum);
        expandableTextView.setText(yourText);
    }
}

Reference: Android – Expandable TextView

Solution 2 - Android

Use an ObjectAnimator.

ObjectAnimator animation = ObjectAnimator.ofInt(yourTextView, "maxLines", tv.getLineCount());
animation.setDuration(200).start();

This will fully expand your TextView over 200 milliseconds. You can replace tv.getLineCount() with however many lines of text you wish to collapse it back down.

----Update----

Here are some convenience methods you can drop in:

private void expandTextView(TextView tv){
    ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", tv.getLineCount());
    animation.setDuration(200).start();
}

private void collapseTextView(TextView tv, int numLines){
    ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", numLines);
    animation.setDuration(200).start();
}

If you're on API 16+, you can use textView.getMaxLines() to easily determine if your textView has been expanded or not.

private void cycleTextViewExpansion(TextView tv){
    int collapsedMaxLines = 3;
    ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", 
        tv.getMaxLines() == collapsedMaxLines? tv.getLineCount() : collapsedMaxLines);
    animation.setDuration(200).start();
}

Notes:

If maxLines has not been set, or you've set the height of your textView in pixels, you can get an ArrayIndexOutOfBounds exception.

The above examples always take 200ms, whether they expand by 3 lines or 400. If you want a consistent rate of expansion, you can do something like this:

int duration = (textView.getLineCount() - collapsedMaxLines) * 10;

Solution 3 - Android

I created an open-source library for this, because I wasn’t satisfied with the other solutions I found on the internet. I’ve put the thing on GitHub, and it’s free to use by anyone.

public class ExpandableTextView extends TextView
{
    // copy off TextView.LINES
    private static final int MAXMODE_LINES = 1;

    private OnExpandListener onExpandListener;
    private TimeInterpolator expandInterpolator;
    private TimeInterpolator collapseInterpolator;

    private final int maxLines;
    private long animationDuration;
    private boolean animating;
    private boolean expanded;
    private int originalHeight;

    public ExpandableTextView(final Context context)
    {
        this(context, null);
    }

    public ExpandableTextView(final Context context, final AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public ExpandableTextView(final Context context, final AttributeSet attrs, final int defStyle)
    {
        super(context, attrs, defStyle);

        // read attributes
        final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView, defStyle, 0);
        this.animationDuration = attributes.getInt(R.styleable.ExpandableTextView_animation_duration, BuildConfig.DEFAULT_ANIMATION_DURATION);
        attributes.recycle();

        // keep the original value of maxLines
        this.maxLines = this.getMaxLines();

        // create default interpolators
        this.expandInterpolator = new AccelerateDecelerateInterpolator();
        this.collapseInterpolator = new AccelerateDecelerateInterpolator();
    }

    @Override
    public int getMaxLines()
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
        {
            return super.getMaxLines();
        }

        try
        {
            final Field mMaxMode = TextView.class.getField("mMaxMode");
            mMaxMode.setAccessible(true);
            final Field mMaximum = TextView.class.getField("mMaximum");
            mMaximum.setAccessible(true);

            final int mMaxModeValue = (int) mMaxMode.get(this);
            final int mMaximumValue = (int) mMaximum.get(this);

            return mMaxModeValue == MAXMODE_LINES ? mMaximumValue : -1;
        }
        catch (final Exception e)
        {
           return -1;
        }
    }

    /**
     * Toggle the expanded state of this {@link ExpandableTextView}.
     * @return true if toggled, false otherwise.
     */
    public boolean toggle()
    {
        if (this.expanded)
        {
            return this.collapse();
        }

        return this.expand();
    }

    /**
     * Expand this {@link ExpandableTextView}.
     * @return true if expanded, false otherwise.
     */
    public boolean expand()
    {
        if (!this.expanded && !this.animating && this.maxLines >= 0)
        {
            this.animating = true;

            // notify listener
            if (this.onExpandListener != null)
            {
                this.onExpandListener.onExpand(this);
            }

            // get original height
            this.measure
            (
                MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
            );

            this.originalHeight = this.getMeasuredHeight();

            // set maxLines to MAX Integer
            this.setMaxLines(Integer.MAX_VALUE);

            // get new height
            this.measure
            (
                MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
            );

            final int fullHeight = this.getMeasuredHeight();

            final ValueAnimator valueAnimator = ValueAnimator.ofInt(this.originalHeight, fullHeight);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
            {
                @Override
                public void onAnimationUpdate(final ValueAnimator animation)
                {
                    final ViewGroup.LayoutParams layoutParams = ExpandableTextView.this.getLayoutParams();
                    layoutParams.height = (int) animation.getAnimatedValue();
                    ExpandableTextView.this.setLayoutParams(layoutParams);
                }
            });
            valueAnimator.addListener(new AnimatorListenerAdapter()
            {
                @Override
                public void onAnimationEnd(final Animator animation)
                {
                    ExpandableTextView.this.expanded = true;
                    ExpandableTextView.this.animating = false;
                }
            });

            // set interpolator
            valueAnimator.setInterpolator(this.expandInterpolator);

            // start the animation
            valueAnimator
                .setDuration(this.animationDuration)
                .start();

            return true;
        }

        return false;
    }

    /**
     * Collapse this {@link TextView}.
     * @return true if collapsed, false otherwise.
     */
    public boolean collapse()
    {
        if (this.expanded && !this.animating && this.maxLines >= 0)
        {
            this.animating = true;

            // notify listener
            if (this.onExpandListener != null)
            {
                this.onExpandListener.onCollapse(this);
            }

            // get new height
            final int fullHeight = this.getMeasuredHeight();

            final ValueAnimator valueAnimator = ValueAnimator.ofInt(fullHeight, this.originalHeight);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
            {
                @Override
                public void onAnimationUpdate(final ValueAnimator animation)
                {
                    final ViewGroup.LayoutParams layoutParams = ExpandableTextView.this.getLayoutParams();
                    layoutParams.height = (int) animation.getAnimatedValue();
                    ExpandableTextView.this.setLayoutParams(layoutParams);
                }
            });
            valueAnimator.addListener(new AnimatorListenerAdapter()
            {
                @Override
                public void onAnimationEnd(final Animator animation)
                {
                    // set maxLines to original value
                    ExpandableTextView.this.setMaxLines(ExpandableTextView.this.maxLines);

                    ExpandableTextView.this.expanded = false;
                    ExpandableTextView.this.animating = false;
                }
            });

            // set interpolator
            valueAnimator.setInterpolator(this.collapseInterpolator);

            // start the animation
            valueAnimator
                .setDuration(this.animationDuration)
                .start();

            return true;
        }

        return false;
    }

    /**
     * Sets the duration of the expand / collapse animation.
     * @param animationDuration duration in milliseconds.
     */
    public void setAnimationDuration(final long animationDuration)
    {
        this.animationDuration = animationDuration;
    }

    /**
     * Sets a listener which receives updates about this {@link ExpandableTextView}.
     * @param onExpandListener the listener.
     */
    public void setOnExpandListener(final OnExpandListener onExpandListener)
    {
        this.onExpandListener = onExpandListener;
    }

    /**
     * Returns the {@link OnExpandListener}.
     * @return the listener.
     */
    public OnExpandListener getOnExpandListener()
    {
        return onExpandListener;
    }

    /**
     * Sets a {@link TimeInterpolator} for expanding and collapsing.
     * @param interpolator the interpolator
     */
    public void setInterpolator(final TimeInterpolator interpolator)
    {
        this.expandInterpolator = interpolator;
        this.collapseInterpolator = interpolator;
    }

    /**
     * Sets a {@link TimeInterpolator} for expanding.
     * @param expandInterpolator the interpolator
     */
    public void setExpandInterpolator(final TimeInterpolator expandInterpolator)
    {
        this.expandInterpolator = expandInterpolator;
    }

    /**
     * Returns the current {@link TimeInterpolator} for expanding.
     * @return the current interpolator, null by default.
     */
    public TimeInterpolator getExpandInterpolator()
    {
        return this.expandInterpolator;
    }

    /**
     * Sets a {@link TimeInterpolator} for collpasing.
     * @param collapseInterpolator the interpolator
     */
    public void setCollapseInterpolator(final TimeInterpolator collapseInterpolator)
    {
        this.collapseInterpolator = collapseInterpolator;
    }

    /**
     * Returns the current {@link TimeInterpolator} for collapsing.
     * @return the current interpolator, null by default.
     */
    public TimeInterpolator getCollapseInterpolator()
    {
        return this.collapseInterpolator;
    }

    /**
     * Is this {@link ExpandableTextView} expanded or not?
     * @return true if expanded, false if collapsed.
     */
    public boolean isExpanded()
    {
        return this.expanded;
    }

    public interface OnExpandListener
    {
        void onExpand(ExpandableTextView view);
        void onCollapse(ExpandableTextView view);
    }
}

Using the ExpandableTextView is very easy, it’s just a regular TextView with some extra functionality added to it. By defining the android:maxLines attribute, you can set the default number of lines for the TextView collapsed state.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <at.blogc.android.views.ExpandableTextView
        android:id="@+id/expandableTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/lorem_ipsum"
        android:maxLines="5"
        android:ellipsize="end"
        app:animation_duration="1000"/>
 
    <!-- Optional parameter animation_duration: sets the duration of the expand animation -->
 
    <Button
        android:id="@+id/button_toggle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/expand"/>
 
</LinearLayout>

In your Activity or Fragment:

    final ExpandableTextView expandableTextView = (ExpandableTextView) this.findViewById(R.id.expandableTextView);
    final Button buttonToggle = (Button) this.findViewById(R.id.button_toggle);
     
    // set animation duration via code, but preferable in your layout files by using the animation_duration attribute
    expandableTextView.setAnimationDuration(1000L);
     
// set interpolators for both expanding and collapsing animations
expandableTextView.setInterpolator(new OvershootInterpolator());
 
// or set them separately
expandableTextView.setExpandInterpolator(new OvershootInterpolator());
expandableTextView.setCollapseInterpolator(new OvershootInterpolator());
 

    // toggle the ExpandableTextView
    buttonToggle.setOnClickListener(new View.OnClickListener()
    {
        @Override
        public void onClick(final View v)
        {
            expandableTextView.toggle();
            buttonToggle.setText(expandableTextView.isExpanded() ? R.string.collapse : R.string.expand);
        }
    });
     
    // but, you can also do the checks yourself
    buttonToggle.setOnClickListener(new View.OnClickListener()
    {
        @Override
        public void onClick(final View v)
        {
            if (expandableTextView.isExpanded())
            {
                expandableTextView.collapse();
                buttonToggle.setText(R.string.expand);
            }
            else
            {
                expandableTextView.expand();
                buttonToggle.setText(R.string.collapse);
            }
        }
    });
     
    // listen for expand / collapse events
    expandableTextView.setOnExpandListener(new ExpandableTextView.OnExpandListener()
    {
        @Override
        public void onExpand(final ExpandableTextView view)
        {
            Log.d(TAG, "ExpandableTextView expanded");
        }
     
        @Override
        public void onCollapse(final ExpandableTextView view)
        {
            Log.d(TAG, "ExpandableTextView collapsed");
        }
    });

You can easily add this library as a gradle dependency to your Android project. Take a look at the project on Github for further instructions:

https://github.com/Blogcat/Android-ExpandableTextView

Solution 4 - Android

Smooth expanding (using heigh & ObjectAnimator)
FYI: requires API 11

public static void expandCollapsedByMaxLines(@NonNull final TextView text) {
    final int height = text.getMeasuredHeight();
    text.setHeight(height);
    text.setMaxLines(Integer.MAX_VALUE); //expand fully
    text.measure(View.MeasureSpec.makeMeasureSpec(text.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.UNSPECIFIED));
    final int newHeight = text.getMeasuredHeight();
    ObjectAnimator animation = ObjectAnimator.ofInt(text, "height", height, newHeight);
    animation.setDuration(250).start();
}

P.S. I assume TextView limited by maxLines.
P.S.S. Thanks Amagi82 for ObjectAnimator example

Solution 5 - Android

If you want to do it based on the number of lines, here's a way to do it:

(Gist of full code)

/**
 * Ellipsize the text when the lines of text exceeds the value provided by {@link #makeExpandable} methods.
 * Appends {@link #MORE} or {@link #LESS} as needed.
 * TODO: add animation
 * Created by vedant on 3/10/15.
 */
public class ExpandableTextView extends TextView {
    private static final String TAG = "ExpandableTextView";
    private static final String ELLIPSIZE = "... ";
    private static final String MORE = "more";
    private static final String LESS = "less";

    private String mFullText;
    private int mMaxLines;

    //...constructors...

    public void makeExpandable(String fullText, int maxLines) {
        mFullText =fullText;
        mMaxLines = maxLines;
        ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                ViewTreeObserver obs = getViewTreeObserver();
                obs.removeOnGlobalLayoutListener(this);
                if (getLineCount() <= maxLines) {
                    setText(mFullText);
                } else {
                    setMovementMethod(LinkMovementMethod.getInstance());
                    showLess();
                }
            }
        });
    }

    /**
     * truncate text and append a clickable {@link #MORE}
     */
    private void showLess() {
        int lineEndIndex = getLayout().getLineEnd(mMaxLines - 1);
        String newText = mFullText.substring(0, lineEndIndex - (ELLIPSIZE.length() + MORE.length() + 1))
                + ELLIPSIZE + MORE;
        SpannableStringBuilder builder = new SpannableStringBuilder(newText);
        builder.setSpan(new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                showMore();
            }
        }, newText.length() - MORE.length(), newText.length(), 0);
        setText(builder, BufferType.SPANNABLE);
    }

    /**
     * show full text and append a clickable {@link #LESS}
     */
    private void showMore() {
        // create a text like subText + ELLIPSIZE + MORE
        SpannableStringBuilder builder = new SpannableStringBuilder(mFullText + LESS);
        builder.setSpan(new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                showLess();
            }
        }, builder.length() - LESS.length(), builder.length(), 0);
        setText(builder, BufferType.SPANNABLE);
    }
}

Solution 6 - Android

Here is what worked for me using some of the above responses (I am using ButterKnife in the example):

private static final MAX_LINE_COUNT = 3;    

@Bind(R.id.description)
TextView mDescription;    

@Override
protected void onCreate(Bundle savedInstanceState) {

  if(!TextUtils.isEmpty(mDescription)) {
    mDescription.setText(mItem.description);
    mDescription.setMaxLines(MAX_LINE_COUNT);
    mDescription.setEllipsize(TextUtils.TruncateAt.END);
  } else {
    mDescription.setVisibility(View.GONE);
  }

}

@OnClick(R.id.description)
void collapseExpandTextView(TextView tv) {

    if (tv.getMaxLines() == MAX_LINE_COUNT) {
        // collapsed - expand it
        tv.setEllipsize(null);
        tv.setMaxLines(Integer.MAX_VALUE);
    } else {
        // expanded - collapse it
        tv.setEllipsize(TextUtils.TruncateAt.END);
        tv.setMaxLines(MAX_LINE_COUNT);
    }

    ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", tv.getMaxLines());
    animation.setDuration(200).start();
}   

When the user clicks on the description it will either collapse or expand based on the max lines. This will only work for API 16+.

The problem that I ran into was that line count was returning zero at points and line count and max count were the same values at certain points.

Solution 7 - Android

You can do something like this. It will work in any kind of view, whether a normal view, or a view inside ListView or RecyclerView:

In onCreate() or something similar, add:

// initialize integers
int collapsedHeight, expandedHeight;

// get collapsed height after TextView is drawn
textView.post(new Runnable() {
    @Override
    public void run() {
        collapsedHeight = textView.getMeasuredHeight();
    }
});

// view that will expand/collapse your TextView
view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // number of max lines when collapsed
        if (textView.getMaxLines() == 2) {
            // expand
            textView.setMaxLines(Integer.MAX_VALUE);
            textView.measure(View.MeasureSpec.makeMeasureSpec(notifMessage.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED));
            expandedHeight = textView.getMeasuredHeight();
            ObjectAnimator animation = ObjectAnimator.ofInt(textView, "height", collapsedHeight, expandedHeight);
            animation.setDuration(250).start();
        } else {
            // collapse
            ObjectAnimator animation = ObjectAnimator.ofInt(textView, "height", expandedHeight, collapsedHeight);
            animation.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animator) {

                }

                @Override
                public void onAnimationEnd(Animator animator) {
                    // number of max lines when collapsed
                    textView.setMaxLines(2);
                }

                @Override
                public void onAnimationCancel(Animator animator) {

                }

                @Override
                public void onAnimationRepeat(Animator animator) {

                }
            });
            animation.setDuration(250).start();
        }
    }
});

This will let you expand/collapse a TextView by clicking any view you want. (you can surely choose the TextView itself too)

Solution 8 - Android

You can use the new TransitionManager for the animation and calling the maxLines attribute to set the amount

fun toggleReadMoreTextView(linesWhenCollapsed: Float) {
    if (viewDataBinding.textView.maxLines != Integer.MAX_VALUE) {
        // exapand
        viewDataBinding.textView.maxLines = Integer.MAX_VALUE
    } else {
        // collapse
        viewDataBinding.textView.maxLines = linesWhenCollapsed
    }
    // start animation
    TransitionManager.beginDelayedTransition(viewDataBinding.constraintLayout)
}

Solution 9 - Android

Refer below link for expandable TextView with options for a number of lines and more less text.

Resizeable Text View(View More and View Less)

Add below line in Java class after setting text in your TextView.

// YourCustomeClass.class [your customized class]
// yourTextView [ TextView yourTextView = findViewById(R.id.yourTextView) ];

YourCustomeClass.doResizeTextView(yourTextView, 3, "More", true);

// 3 - No of lines after user wants to expand it. 
// "More" : text want to see end of your TextView after shrink
// True : flag for viewMore

Solution 10 - Android

Cliffus' answer came close to what I was looking for, but it doesn't support using the setMaxLines() method, which causes issues when you can't set the max lines through XML.

I've forked their library and made it so that using setMaxLines() won't break the expand/collapse action. I also updated the Gradle configuration and migrated it to AndroidX. Otherwise, the usage is the same as before.

You can include it in your project using Jitpack:

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}
dependencies {
	implementation 'com.github.zacharee:Android-ExpandableTextView:Tag'
}

Where Tag is the latest commit tag (https://jitpack.io/#zacharee/Android-ExpandableTextView/).

The usage is exactly the same as the original library's. Include the ExpandableTextView in your XML:

<at.blogc.android.views.ExpandableTextView
	...
	android:maxLines="10"
	/>

And expand/collapse in code:

if (expandable.isExpanded) {
	expandable.collapse()
else {
	expandable.expand()
}

Solution 11 - Android

In ListView or RecyclerView instead of using OnGlobalLayoutListener we always use OnPreDrawListener. This callback is fired also for non visible rows at start. From the official documentation:

private void makeTextViewResizable(final TextView tv, final int maxLine, final String expandText, final boolean viewMore){
        try {
            if (tv.getTag() == null) {
                tv.setTag(tv.getText());
            }
            //OnGlobalLayoutListener
            ViewTreeObserver vto = tv.getViewTreeObserver();
            vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {

                @Override
                public boolean onPreDraw() {

                        ViewTreeObserver obs = tv.getViewTreeObserver();
                       // obs.removeGlobalOnLayoutListener((ViewTreeObserver.OnGlobalLayoutListener) mActivity);
                        obs.removeOnPreDrawListener(this);
                        if (maxLine == 0) {
                            int lineEndIndex = tv.getLayout().getLineEnd(0);
                            String text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
                            tv.setText(text);
                            tv.setMovementMethod(LinkMovementMethod.getInstance());
                            tv.setText(
                                    addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, expandText,
                                            viewMore), TextView.BufferType.SPANNABLE);
                        } else if (maxLine > 0 && tv.getLineCount() >= maxLine) {
                            int lineEndIndex = tv.getLayout().getLineEnd(maxLine - 1);
                            String text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
                            tv.setText(text);
                            tv.setMovementMethod(LinkMovementMethod.getInstance());
                            tv.setText(
                                    addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, expandText,
                                            viewMore), TextView.BufferType.SPANNABLE);
                        } else {
                            int lineEndIndex = tv.getLayout().getLineEnd(tv.getLayout().getLineCount() - 1);
                            String text = tv.getText().subSequence(0, lineEndIndex) + " " + expandText;
                            tv.setText(text);
                            tv.setMovementMethod(LinkMovementMethod.getInstance());
                            tv.setText(
                                    addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, expandText,
                                            viewMore), TextView.BufferType.SPANNABLE);
                        }


                    return true;
                }


            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

Solution 12 - Android

Primarily for the case of adding the "See More" to the end of the text, I present to you my TruncatingTextView. After much experimentation it seems to work seamlessly when loading these text views in a RecyclerView item view.

package com.example.android.widgets;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatTextView;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;

import com.example.android.R;

public class TruncatingTextView extends AppCompatTextView {
    public static final String TWO_SPACES = "  ";

    private int truncateAfter = Integer.MAX_VALUE;

    private String suffix;
    private RelativeSizeSpan truncateTextSpan = new RelativeSizeSpan(0.75f);
    private ForegroundColorSpan viewMoreTextSpan = new ForegroundColorSpan(Color.BLUE);
    private static final String MORE_STRING = getContext().getString(R.string.more);

    private static final String ELLIPSIS = getContext().getString(R.string.ellipsis);

    public TruncatingTextView(Context context) {
        super(context);
    }

    public TruncatingTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TruncatingTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setText(CharSequence fullText, @Nullable CharSequence afterTruncation, int truncateAfterLineCount) {
        this.suffix = TWO_SPACES + MORE_STRING;

        if (!TextUtils.isEmpty(afterTruncation)) {
            suffix += TWO_SPACES + afterTruncation;
        }

        // Don't call setMaxLines() unless we have to, since it does a redraw.
        if (this.truncateAfter != truncateAfterLineCount) {
            this.truncateAfter = truncateAfterLineCount;
            setMaxLines(truncateAfter);
        }

        setText(fullText);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        if (getLayout() != null && getLayout().getLineCount() > truncateAfter) {
            int lastCharToShowOfFullTextAfterTruncation = getLayout().getLineVisibleEnd(truncateAfter - 1) - suffix.length() - ELLIPSIS.length();

            if (getText().length() <= lastCharToShowOfFullTextAfterTruncation) {
                // No idea why this would be the case, but to prevent a crash, here it is. Besides, if this is true, we should be less than our maximum lines and thus good to go.
                return;
            }

            int startIndexOfMoreString = lastCharToShowOfFullTextAfterTruncation + TWO_SPACES.length() + 1;

            SpannableString truncatedSpannableString = new SpannableString(getText().subSequence(0, lastCharToShowOfFullTextAfterTruncation) + ELLIPSIS + suffix);
            truncatedSpannableString.setSpan(truncateTextSpan, startIndexOfMoreString, truncatedSpannableString.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
            truncatedSpannableString.setSpan(viewMoreTextSpan, startIndexOfMoreString, startIndexOfMoreString + MORE_STRING.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            setText(truncatedSpannableString);
        }
    }
}

You can always choose to add your own attribute for truncateAfter, and use any of the above answers to add the animation for expand/collapse (I did not code to handle expand/collapse but easily done by using one of the above animation answers).

I'm placing this here more for others who are trying to find "View More" functionality for their text views.

Solution 13 - Android

Now, it's even more easy to provide the requested TextView with animation and all the required controls using this awesome library ExpandableTextView, in this library you have only to add it into your gradle and then define it like the following in your xml:

  <com.ms.square.android.expandabletextview.ExpandableTextView
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:expandableTextView="http://schemas.android.com/apk/res-auto"
      android:id="@+id/expand_text_view"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      expandableTextView:maxCollapsedLines="4"
      expandableTextView:animDuration="200">
      <TextView
          android:id="@id/expandable_text"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_marginLeft="10dp"
          android:layout_marginRight="10dp"
          android:textSize="16sp"
          android:textColor="#666666" />
      <ImageButton
          android:id="@id/expand_collapse"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:padding="16dp"
          android:layout_gravity="right|bottom"
          android:background="@android:color/transparent"/>
  </com.ms.square.android.expandabletextview.ExpandableTextView>

and after that use it in your code like:

TextView expandableTextView = (ExpandableTextView) findViewById(R.id.expand_text_view);

And as you see you can control the max lines you want and the animation duration and all the required settings for your TextView expand technique.

Solution 14 - Android

Solution 15 - Android

enter image description here

enter image description here

Step 1

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:startColor="#11FFFFFF"
        android:centerColor="#33FFFFFF"
        android:endColor="#99FFFFFF"
        android:angle="270" />
</shape>

Step 2

<TextView
    android:id="@+id/overviewText"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:maxLines="3"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.0"
    app:layout_constraintStart_toStartOf="@+id/textView8"
    app:layout_constraintTop_toBottomOf="@+id/textView8" />

    <ImageView
        android:id="@+id/seeMoreImage"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@drawable/background_white"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="@+id/overviewText"
        app:layout_constraintEnd_toEndOf="@+id/overviewText"
        app:layout_constraintStart_toStartOf="@+id/overviewText"
        app:srcCompat="@drawable/ic_arrow_down"
        tools:ignore="VectorDrawableCompat" />

Step 3

    var isTextViewClicked = true
    if (binding.overviewText.lineCount > 3)
        binding.seeMoreImage.visibility = View.VISIBLE
    binding.seeMoreImage.setOnClickListener {
        isTextViewClicked = if(isTextViewClicked){
            binding.overviewText.maxLines = Integer.MAX_VALUE
            binding.seeMoreImage.setImageResource(R.drawable.ic_arrow_up)
            false
        } else {
            binding.overviewText.maxLines = 3
            binding.seeMoreImage.setImageResource(R.drawable.ic_arrow_down)
            true
        }
    }

Solution 16 - Android

Create simple solution without libraries and without custom classes.

First of all create item.xml with (for example) two TextView. One for display text, which will be expanded, and another for button - "show more".

...

<TextView
    android:id="@+id/item_info_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="16sp"
    tools:text="Test long text info\nTest long text info\nTest long text info\nTest long text info | Test long text info | Test long text info"
    android:maxLines="@integer/info_collected_lines"
    android:fontFamily="@string/font_roboto_regular"
    android:textColor="@color/text_second"
    android:layout_marginTop="8dp"
    android:ellipsize="end"/>

<TextView
    android:id="@+id/item_more_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="12sp"
    android:text="@string/see_more"
    android:singleLine="true"
    android:fontFamily="@string/font_roboto_regular"
    android:textColor="@color/text_accent"
    android:ellipsize="marquee"/>

...

Other resources:

<color name="text_accent">#0070AA</color>
<color name="text_second">#616161</color>

<string name="font_roboto_regular" translatable="false">sans-serif</string>
<string name="font_roboto_medium" translatable="false">sans-serif-medium</string>

<string name="see_more">Show more</string>

<integer name="club_info_collected_lines">4</integer>
<integer name="club_info_expanded_lines">10</integer>

And it looks like this:

XML layout preview

Next step is add logic for expand out text. We do it inside RecyclerView.ViewHolder:

class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {

    ...

    private val infoText = view.findViewById<TextView>(R.id.item_info_text)
    private val moreText = view.findViewById<TextView>(R.id.item_more_text)

    fun bind(item: Item, callback: Callback) {
        infoText.text = item.info
        
        // This is extension (show code later) need for getting correct [TextView.getLineCount]. Because before draw view it always == 0.
        infoText.afterLayoutConfiguration {
            val hasEllipsize = infoText.layout.getEllipsisCount(infoText.lineCount - 1) > 0

            moreText.visibility = if (hasEllipsize) View.VISIBLE else View.GONE

            if (hasEllipsize) {
                val maxLines = itemView.context.resources.getInteger(R.integer.club_info_expanded_lines)
                moreText.setOnClickListener {
                    infoText.maxLines = maxLines
                    it.visibility = View.GONE
                }
            }
        }

        ...
    }

    // Call this inside [RecyclerView.Adapter.onViewRecycled] for prevent memory leaks.
    fun unbind() {
        moreText.setOnClickListener(null)
    }
}

Extension:

/**
 * Function for detect when layout completely configure.
 */
fun View.afterLayoutConfiguration(func: () -> Unit) {
    viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            viewTreeObserver?.removeOnGlobalLayoutListener(this)
            func()
        }
    })
}

I try use animation with TransitionManager.beginDelayedTransition but it looks ugly inside RecyclerView. And like how it looks without any animation.

Solution 17 - Android

Add the dependency in your app module gradle

dependencies {
          implementation 'com.github.arshadbinhamza:ViewMore:1.0.9'
}

 //        ViewMoreHolder.load(textView_description,text, Typeface of end Text,UnderLine,number_of_lines,click_for_end_text_only);
  //  ViewMoreHolder.load(tv_description,description, Typeface.DEFAULT,true,3,false);

Please see a sample i have added(It is the extracted solution for my app requirement from previous answers).We can update/enhance the library as per request

https://github.com/arshadbinhamza/ViewMore

Solution 18 - Android

I know it may be too late. For the part to achieve the "see more" action at the end, I had written a blog post here. Basically, I used a static layout to do all the text measurements. However, the blog post doesn't cover the animation part. As many people pointed out, we can use ValueAnimator or ObjectAnimator to achieve that. You can find the full code for animated expandable text view in this repo, and even use the library as-is.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJorge GilView Question on Stackoverflow
Solution 1 - AndroidA N M Bazlur RahmanView Answer on Stackoverflow
Solution 2 - AndroidJim PekarekView Answer on Stackoverflow
Solution 3 - AndroidCliffusView Answer on Stackoverflow
Solution 4 - AndroidigorkView Answer on Stackoverflow
Solution 5 - AndroidVedant AgarwalaView Answer on Stackoverflow
Solution 6 - AndroidRay HunterView Answer on Stackoverflow
Solution 7 - AndroidHussein El FekyView Answer on Stackoverflow
Solution 8 - AndroidMartijnView Answer on Stackoverflow
Solution 9 - AndroidMihir TrivediView Answer on Stackoverflow
Solution 10 - AndroidTheWandererView Answer on Stackoverflow
Solution 11 - AndroidAndroid TutorialView Answer on Stackoverflow
Solution 12 - AndroidChantell OsejoView Answer on Stackoverflow
Solution 13 - AndroidMuhammed RefaatView Answer on Stackoverflow
Solution 14 - AndroidSebastian CorradiView Answer on Stackoverflow
Solution 15 - AndroidHamdy Abd El FattahView Answer on Stackoverflow
Solution 16 - AndroidSerjantArbuzView Answer on Stackoverflow
Solution 17 - AndroidArshadView Answer on Stackoverflow
Solution 18 - AndroidPhạm GiangView Answer on Stackoverflow