Expandable TextView with LayoutTransition. Part 1

Animate layout updates with LayoutTransition.

Yuriy Skul
5 min readJul 15, 2020

Part 2: Expandable TextView with StaticLayout

This story is just an investigation. My goal was to get required expanding functionality for regular TextView class with the help of LayoutTransition.class.

Starter code

Current solution has no view customization. Whole code will be outside regular TextView class without extending it.

Layout is just a parent container ConstraintLayout with child TextView in the center.
I have added it for it to be clickable with selectable item background which provides ripple animation on click for better visualization.

And additionally -MainActivity.class which already has some initial fields, and constants with hardcoded long and short text because it is demo.

I want my textView always be collapsed at first so initial state is collapsed to the MAX_LINES_COLLAPSED =3 lines.

TextView changes its state on every click.

Result is like expected to be:

Starter code

Add LayoutTransition
We all know the android:animateLayoutChanges=”true” in xml so let’s do it programmatically with 300 ms duration

And result is:

apply LayoutTransition

Great! it works. Everything is fine!

But wait a minute… Let’s change duration from 300 ms to a longer one and look at the obtained result: transition.setDuration(6000);

apply LayoutTransition with duration = 6 sec

Well for fast animation this is actually imperceptible and insignificant…
But not for me! I like these little things and always watch them.

The first thing I do not like is: when I click during expanding or collapsing each of these animation still keeps going and reverse starts only after current one has finished.

And the second thing that bothers me — if I click when textView is idle in expanded state the animation goes smoothly to collapsed but text gets cut immediately right after I have clicked and terrible empty space appears bellow collapsed three lines text. So “unmaterial”!

Apply revers on multi clicks
If I click during layout transition I want previous transition to be stopped at this moment and revers should be started form this state.
But how it can be canceled…
Canceling animator has no effect: mTransition.getAnimator(LayoutTransition.CHANGING).cancel();
and it is cloned inside LayoutTransition.setupChangeAnimation() method

private void setupChangeAnimation(final ViewGroup parent, 
final int changeReason,
Animator baseAnimator,
final long duration,
final View child) {

...

// Make a copy of the appropriate animation
final Animator anim = baseAnimator.clone();
...

};

and public void endChangingAnimations() has @hide .

Disabling transition affects only after animation has finished but not during
mTransition.disableTransitionType(LayoutTransition.CHANGING);

cancel() is @hide:

@UnsupportedAppUsage
(maxTargetSdk = Build.VERSION_CODES.P)
public void cancel() { }

WorkAround

Let’s look inside ViewGroup.setLayoutTransition(...) method:

Replacing a non-null transition will cause that previous transition to be
canceled”

This might help, just get it from layout and set it back.
Update:

So now it has reverse on click feature:

revers click layout transition

Before getting to the next “Text issue” I want to handle case when its size is fits entirely MAX_LINES_COLLAPSED=3 limit and implement ellipsizing.

Disable click animation if text is short enough

TextView can be updated with new text data many times during runtime and text can be short and fit in 3 lines limit.

Above mExpandableTV.setText(LONG_TEXT) method add next lines to handle correct initialization of textView state:

if (isCollapsed) {
mExpandableTV.setMaxLines(MAX_LINES_COLLAPSED);
} else {
mExpandableTV.setMaxLines(Integer.MAX_VALUE);
}
mExpandableTV.setText(LONG_TEXT);

Remove from layout xml android:clickable=”true” — it will be handled dynamically at runtime.

Inside onCreate() remove mExpandableTV.setText(LONG_TEXT),
and replace it with new method: updateWithNewText(LONG_TEXT).

Let’s implement new method updateWithNewText(LONG_TEXT).

First set new text to the mExpandableTV;

mExpandableTV still has previous instance of Layout(StaticLayout.java) from old text so to use new Layout values for computation we have to wait until text is going to be “relayouted”. It can be done with the help of OnGlobalLayoutListener. Ad listener and remove it form ViewTreeObserver after computation is finished.

Computation logic is:
detect whether new textView with new text is limited with maxLines and if it is not — compare its lines count with limit value, and if text is short enough to fit the limit lines size — disable clicking, otherwise — enable;
if text is limited (getMaxLines() != Integer.MAX_VALUE) — check whether it is trimmed up to limit lines count or it is just short enough to fit the limited textview size:

I added radioGroup which sets long and short text to depending on selection using this updateWithNewText(…) method:

Disable click if text is short

Ellipsize text at the end

Let’s make text ellipsized if it can be collapsed/expanded and clickable.
Simply add to the updateWithNewText(…) setEllipsize(…) in every case:

Warning. If I remove ellipsizing when text is short in previous methodmExpandableTV.setEllipsize(null): by switching to the short text everything will be fine — there wont be three dots at the end, but after switching back to the long text it is still not clickable. The reason of this bug is in mExpandableTV.getMaxLines(): when very long text with 20 lines has maxLines value 3 and is not ellipsized, in its collapsed state method getMaxLines() still returns 20, but if it is ellipsized there is something magic done in static layout with getting substring and replacing last characters with three dots so this method will return just 3, not 20:

textViewWith_20Lines.setMaxLines(3);
then
textViewWith_20Lines.getLineCount() - returns 20
but if
textViewWith_20Lines.setEllipsize(TextUtils.TruncateAt.END);
then
textViewWith_20Lines.getLineCount() - returns just 3
Now it has three dots in collapsed state

Text Issue
As I mentioned before about text behavior during collapsing: it changes to the short version immediately during beginning of collapsing.

Workaround
I have not found proper correct solution but the mechanic of transition is that new layout transition keeps waiting for current one to be finished while text changes immediately. Let’s use it:

a) when collapsing has started — set back setMaxLines(Integer.MAX_VALUE to force textView become long and prevent from trimming:

b) but when transition has ended in transition listener in case of collapsing animation - call setMaxLines(MAX_LINES_COLLAPSED) otherwise textView will start expanding because of (a):

Now it has no text issue:

Fixed text behavior during collapsing

Thanks for reading)

Solution code:

--

--