android – Animate MaxLines and Ellipsize-ThrowExceptions

Exception or error:

I have textview which contains a part of a text. When the user clicks the arrow, the textview resizes so the full text is shown. See the images below for an example:
collapsed

expanded

The TextView has a wrap_content height, and when collapsed a maxLines=”4″.

The onClick of the arrow contains this code:

        if (isExpanded) {
            btnToggle.setImageDrawable(getResources().getDrawable(
                    R.drawable.arrow_down));
            tvText.setMaxLines(4);
            tvText.setEllipsize(TruncateAt.END);
        } else {
            btnToggle.setImageDrawable(getResources().getDrawable(
                    R.drawable.arrow_up));
            tvText.setMaxLines(Integer.MAX_VALUE);
            tvText.setEllipsize(null);
        }
        isExpanded = !isExpanded;

This code works, but it is not animated. I need to animate the expansion, so the TextView animates to it’s full height.
I can’t find anything about animating properties like MaxLines. Who can help me out?

How to solve:

You can achieve this using an ObjectAnimator

ObjectAnimator animation = ObjectAnimator.ofInt(
        tvText,
        "maxLines", 
        25);
animation.setDuration(4000);
animation.start();

This will increase the “maxLines” property of the “tvText” TextView from whatever it initially is set to, to 25, over the period of 4000 milliseconds.

See more here and here.

###

While animating maxLines works, the result is a bit choppy since your view height jumps a lot.

int startHeight = content.getMeasuredHeight();
content.setMaxLines(Integer.MAX_VALUE);
content.measure(
            View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.AT_MOST),
            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
int endHeight = content.getMeasuredHeight();

content is a TextView with maxLines set to 2. Now you can animate the TextView height instead.
Edit: TextView scrolls when it can’t fit it’s content vertically, you’ll need workarounds. setMovementMethod(null) disables scrolling, but it also disables link clicking. Because Android.

###

The accepted answer is fundamentally wrong, because it forces the call of TextView.setMaxLines() lots of times with the same value without reason, making the resulting animation jerky.

You could use a simple ValueAnimator with a lastValue flag instead:

    ValueAnimator animator = ValueAnimator.ofInt(fromThisLineCount, toThisLineCount).setDuration(250);

    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        int lastValue = -1;

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int value = (int) animation.getAnimatedValue();

            if (value == lastValue) {
                return;
            }

            lastValue = value;

            yourTextView.setMaxLines(value);
        }
    });

    animator.start();

Remember that the animation adds/removes 1 whole line every time, so it may still look jerky if the animation duration is too high or if it affects just a few lines. The best approach is to create a custom View and make the appropriate measurements onMeasure (see e.g. https://github.com/Manabu-GT/ExpandableTextView).

###

All of the other solutions didn’t work nicely for me. Animation was choppy or even blinking.

What I chose to do is animating of the layoutParams height instead. This solution might not fit for every case, but for me it seems to work nicely. Here’s a demonstration:

enter image description here

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val expectedWidthOfTextView = resources.displayMetrics.widthPixels
        val originalMaxLines = textView.maxLines
        if (originalMaxLines < 0 || originalMaxLines == Integer.MAX_VALUE)
            Log.d("AppLog", "already unbounded textView maxLines")
        else {
            textView.maxLines = Integer.MAX_VALUE
            textView.measure(
                View.MeasureSpec.makeMeasureSpec(expectedWidthOfTextView, View.MeasureSpec.AT_MOST),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
            )
            val measuredLineCount = textView.lineCount
            val measuredTargetHeight = textView.measuredHeight
            Log.d("AppLog", "lines:$measuredLineCount/$originalMaxLines")
            textView.maxLines = originalMaxLines
            if (measuredLineCount <= originalMaxLines)
                Log.d("AppLog", "fit in original maxLines")
            else {
                Log.d("AppLog", "exceeded original maxLines")
                textView.setOnClickListener {
                    textView.setOnClickListener(null)
                    textView.maxLines = Integer.MAX_VALUE
                    val layoutParams = textView.layoutParams
                    val animation = ValueAnimator.ofInt(textView.height, measuredTargetHeight)
                    animation.addUpdateListener { valueAnimator ->
                        val value: Int = valueAnimator.animatedValue as Int
                        layoutParams.height = value
                        textView.requestLayout()
                    }
                    animation.start()
                    layoutParams.height = textView.height
                }
            }
        }

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent" android:orientation="vertical" android:gravity="center_horizontal"
        android:layout_height="match_parent" android:animateLayoutChanges="true"
        tools:context=".MainActivity">

    <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content"
               android:src="@android:drawable/sym_def_app_icon"/>

    <TextView
            android:id="@+id/textView" android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:ellipsize="end" android:maxLines="4" android:clickable="true" android:focusable="true"
            android:paddingEnd="16dp" android:paddingStart="16dp"
            android:textColor="#c1000000" android:textSize="14sp"
            android:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."/>

    <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content"
               android:src="@android:drawable/sym_def_app_icon"/>
</LinearLayout>

Leave a Reply

Your email address will not be published. Required fields are marked *