Expandable TextView using StaticLayout’s data. Part 2.
Animate expanding and collapsing using ValueAnimator
and StaticLayout
for computation destination height of animation
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:
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:
Implement reverse feature
Call in onClick()
method in mAnimator.isRunning()
block animatorReverse()
method and implement it:
Now result is:
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:
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:
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: