Expandable TextView using StaticLayout’s data. Part 2.

Animate expanding and collapsing using ValueAnimator and StaticLayout for computation destination height of animation

Yuriy Skul
5 min readJul 15, 2020

Part 1: Expandable TextView with LayoutTransition.

In pervious article I have tried to solve this task with the help of LayoutTransition.class, but because of some unacceptable workarounds I had to look up for better solution.
Current way Is just simply usage of
layout(StaticLayout) instance of this TextView to get info of its height for collapsed or expanded state and exact place to replace text with ellipsized custom postfix.

Starter Code

Just any regular parent layout with current custom TextView as a child with mach_parent width:

and starter of custom TextView :

Simple Animation

Now instead of creating constants of collapsed/expanded state and current flag to keep current state let’s do it with the help of textView.getMaxLines().
Add next method to detect current state:

For animation define new field ValueAnimator.
Inside init() add initAnimator() method.
And implement this method.

Real Start and End value of animator is going to be calculated later in onClick() method.

And start animation.

onAnimationUpdate(…) calls updateHeight(newHeight) method.
Let’s define it:

When animation ends we need to set back to the textView wrap_content layout param for its height :

Now inside onClickListener():

check for current animation and if it is running — just return from method so if user keeps spamming click during animation nothing will be changed until this animation will be finished;

For animator we have to set up start value and end value.
startPosition value is current height.

Add animateTo() method.
endPosition value is needed to be calculated with the help of Layout (StaticLayout) which has all info for lines count, height of every line…

And start animation at the end of method.

Current result is:

duration = 1000 ms for better visualization

So, we have same issues like in previous article with LayoutTransition:

  • It has immediate text trimming when collapsing starts.
  • Clicking during animation has to start reverse animation from current state.

Fix Text Issue

To fix this issue we have to call setMaxLines(COLLAPSED_MAX_LINES)from onAnimationStart, but during collapsing and during expanding call setMaxLines(Integer.MAX_VALUE) from onAnimationStart():

So add new flag as a field variable: boolean isCollapsing and change anonymous AnimatorListener ( AnimatorListenerAdapter ):

Result:

Fixed text issue result

Implement reverse feature

Call in onClick() method in mAnimator.isRunning() block animatorReverse() method and implement it:

Now result is:

Fixed text issue and implemented reverse feature

Handle short text case and runtime text update

What if text is short enough so it fits maxLines limit… Let’s handle this case and make textView not clickable. And by the way new text can be set from xml or programmatically so this needs to be handled too.

When we click — textView has been already “layouted”, so we can confidently use its layout values or call textView.getHeight() but when new text just has been set the textView needs time for “relayouting”. I don’t see the need to use ViewTreeObserver.OnGlobalLayoutListener, so we can use OnLayoutChangeListener but textView.layout(...) calls textView.onLayout() and gets list of listeners from ListenerInfo mListenerInfo and if it is not null and listeners are not null ,it clones this list and looping through it, calling onLayoutChange() for each of it— so I will do it directly inside onLayout() method without listener’s help:

Let’s test it:

Disable click if text is short

Implement custom colored ellipsizing

And finally let’s add postfix like “…see more “ with another color to the end of last visible text line if text is trimmed and collapsed.

We can not substring first lines and then replace last characters with postfix because in such case inside animateTo() method, when is it is collapsed, layout.getHeight() will return instead of full original text height — currently trimmed height associated with only collapsed lines count and startPosition will be equals to endPosition.

So endPosition still can be computed as example measured with measure proper measure spec but I will implement another solution.

Instead of creating substring from original long text — I will just replace in proper place its characters with postfix to keep same text size.

Let’s override setText() method so it will store origin text every time it is called with new argument:

Add postfix constant. In article I use a constant, but for proper implementation this one definitely should be a string resource.

For ellipsizing with colored text I use SpannableStringBuilder:

And deEllipsize(): for changing back to original not ellipsized text here could be used same replace technics but I have added previously CharSequence mOriginalText field so now I can use it:

Inside onLayout() call ellipsizeColored() but only if it is not running animation case:

In initAnimator() cal deElipsize() from onAnimationStart() if it is expanding animation and ellipsizeColored() from onAnimationEnd() :

Run the code and … It has a bug! If user updates textview with short text during collapsing — ellipsized postfix appears:

Bug with short text and ellipsize postfix

onLayout() gets called many times during collapsing and expanding, and I don’t want to call many times “ellipsizing” logic. That is why I check in it during every call — so during animation I don’t handle it. But onLayout() has to be called after animation ends even once, so for fast fix is just enough to add one line in onLayut():

It is fine to call deEllipsize() in onLayout() even during animation because in comparing with heavy ellipsizeColored() method with SpannableStringBuilder usage this one is much lighter and actually regular animation will never be running if text is short, only in such rare cases when during collapsing animation textView updates with short text.

Thanks for reading:)

Solution code:

--

--