25

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?

harmjanr
  • 930
  • 2
  • 11
  • 27

5 Answers5

69

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.

Ken Wolf
  • 23,133
  • 6
  • 63
  • 84
  • 1
    Seems to work! Only 25 works though, Integer.MAX_VALUE isn't working. But I'll just put that value on 1000, not gonna exceed that anyway :) Worth the bounty! – harmjanr Jun 17 '13 at 06:46
  • 5
    @harmjanr Integer.MAX_VALUE works, but you don't notice the animation because it's going from maxLines=4 (or whatever your minimum desired line number is) to maxLines=2147483647 in the span of 4000ms. An ideal solution is to know the lines your full text will use before rendering (http://stackoverflow.com/questions/15679147/how-to-get-line-count-of-textview-before-rendering) – Sanders Mar 24 '14 at 18:34
  • This was definitely helpful so, thank you! However, any suggestions if our resulting animation is choppy? My layout very well may be a bit heavy so I'll try trimming it down but other than that, any thoughts? – Forrest Bice Jul 17 '14 at 21:21
  • Make sure you don't have too many overlapping layers. Alpha transparency in particular costs a lot. And yes, simplify your layouts. If your phone supports it you can also make sure hardware acceleration is enabled for your app. – Ken Wolf Jul 18 '14 at 05:06
  • My target API target is > 14 thus it appears hardware acceleration should be enabled by default . Additionally, I tried, without luck, explicitly accelerating the view animation as explained here . Also pretty sure the activity enclosing my custom view doesn't have much overlap or use of alpha but I'll keep working on it; thanks for your answer and help @KenWolf! – Forrest Bice Jul 18 '14 at 17:58
  • Turns out the expansion was only appeared choppy because my duration was set to 300ms. Bumping it to 200 or even 100 causes the animation to become both smooth and speedy. However, I guess this raises the question of how you can create slow and smooth animations? – Forrest Bice Jul 18 '14 at 21:55
  • @sudocoder the code above is only to animate `maxLines`. Make sure the `ellipsize` property is on for ellipses. `android:ellipsize="end"` – Ken Wolf Dec 22 '15 at 06:47
  • thanks @Ken Wolf, I have made sure the ellipsize property is on for ellipses and still do not see ellipses. – sudocoder Dec 22 '15 at 23:47
  • @sudocoder Make sure you have words that are longer than the view is wide. Ellipsis only appear if words need to be truncated. – Ken Wolf Dec 23 '15 at 05:38
  • 7 years later after this answer, any animation attempts on Android hasn't got any better and is never nearly as good as on iPhone ((( – ekashking Mar 26 '20 at 20:42
5

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.

DariusL
  • 4,007
  • 5
  • 32
  • 44
3

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).

1911z
  • 1,073
  • 10
  • 15
2

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>
android developer
  • 114,585
  • 152
  • 739
  • 1,270
0

Easiest solution

val transitionSet = TransitionSet().addTransition(ChangeBounds())
TransitionManager.beginDelayedTransition(
    textView.parent as ViewGroup,
    transitionSet
)
kengura
  • 121
  • 1
  • 5