Hi.
My name is Daniel Olshansky.
And today I'll be showing you how to do an animation to
extend cells in the ListView.
So if you have a ListView with certain data and you want to
show extra content associated with every single cell,
instead of taking the user to a new page, you can simply
click on a cell, expand it, show the extra content and
collapse it.
A few of the things about this animation is that as a cell
expands, it moves around all the items around it to make
room for the new extra content.
And furthermore, this new size persists.
So if you were to expand a cell, then scroll away from
it, and then come back to it, the new size will persist on
the expanded cell until it is collapsed manually by the user
by clicking on the cell again.
A couple of the tricks and the problems they came across
while implementing this animation is the fact that
when you animate a cell-- when you change the layout of a
cell, what's going to happen is that some of the other
cells around it are going to disappear.
They're going to be moved outside the
bounds of the ListView.
And when this happens, you no longer have a reference to
those views.
So even though you still need to animate them to achieve
that animation effect.
That's problem number one.
The second problem when working with this is that when
you change the layout of a certain view in the ListView,
the Container, what's going to happen is that the ListView
will try to rearrange the cells as it wants to
make room for it.
But if you want to choose some sort of custom expansion, such
that when you expand the cell all the content should be
visible, or as much of it as can be visible to the user,
should be visible once.
You're going to want to rearrange the cells in a way
that's most optimal for your application.
And in this case, you need to tell the ListView exactly how
to place them on the screen.
So going straight into the demo.
Here we have a ListView of
chameleons, rocks, and flowers.
And say I click on a cell, see how it expands,
displays a new text.
And then you can click again to close it.
This works for every single cell.
It works for extra content of different sizes.
You can also see that as a cell expands and collapses,
the actual content, the text, actually fades
in and fades out.
And this is not only restricted to text.
This will also work with other types of layouts.
Another interesting thing about this is that, say the
cell is at the edge and you want to expand it.
You can see that it'll animate it to make room for it.
So if we were to animate, to bring this flower down, and we
click here, it'll actually animate upwards instead of
animating downwards.
And then when I click on it, it makes room for all the
other cells and then animates them back in.
So jumping straight to the code, here in Custom ListView,
which extends the ListView which can expand
and collapse cells.
First we initiate an item like click listener, so that when
you click and click again, you can expand or collapse it,
respectively.
And then scrolling down furthermore, this is the Get
Top and Bottom Translations method.
So what this does, this can be customized by you depending
what kind of user experience you'd like.
But in this specific case, what I did is always try to
maximize the amount of content that displayed to the user.
And all it is is really just a little bit of math, and a
little bit of arithmetic, using the bounds of the
ListView, using how much, what the current offset is, and
using what the size of the current visible part of the
ListView as well.
So this will return a bottom and a top.
And all this will say is how much the bottom and the top
bounds of the expanding or the collapsing cell should be
modified by.
And you can use these as a reference to animate all the
cells around the cells being expanded or collapsed.
Scrolling down furthermore.
We reach the expanding method, which takes in the view that's
being expanded.
And before we actually expand anything, there's a few things
we need to do.
We need to store what the old bounds of the cell that's
about to be expanded is.
And we also need to store the top and bottom bounds of all
the cells that are around the cell
that's going to be expanded.
After that, we get a reference of the expanding layout.
So even though it's not visible at first, the
expanding layout is present in every single cell.
Except its visibility is set to gone.
So it's not being rendered.
It's not affecting the layout at all.
But by sending it to visible, we now tell it that we want--
by sending it to visible and have its layout set to wrap
content, what we're actually going to be doing is invoking
a request layout call without having that be here
explicitly.
Because we know that now something that was gone has
become visible.
And we need to render it.
And like I've mentioned in a few of my other animations,
what we're going to want to do is add an OnPreDraw Listener.
So by invoking said visible, we're going to change the
layout of this cell along with the bounds and the layout of
the cells around it.
And On Layout and On Measurable get called.
The final values for all the cells will be computed, but
they will not yet be drawn on the screen.
So this is the best place to run animation.
Except the thing about this animation right here that's
going to be a little different is that we're going to have
two passes in OnPreDraw instead of just one.
During the first PreDraw pass, what we want to do is we want
to offset the content in the ListView in such a way such
that after we invoke our layout once again, all the
cells that we want rendered on the screen will be visible and
will be accessible to the user.
So if we were not to have this first pass, the ListView would
arrange all the cells in a way most convenient for this
ListView to display the content.
But seeing how we're animating the top and bottom bounds in a
very custom manner, we need to specify exactly which cells we
want to have visible.
The way this is done is through a little bit of math,
determining how much we need to offset it by.
But the key method I'm using here is set
selection from top.
So after determining which cell is going to be at the
very top after my custom changes for the top and bottom
bounds are run, I know what the first visible position is
going to be afterwards.
And I also know what its offset is going
to be from the top.
So doing some math, and then calling said selection from
top, I set the offset that I want for the ListView, and
then I call request layout.
So what this is going to do is render all the cells that I
need to be rendered on the screen, after having the
ListView be offset by how much I need.
I'm also going to be returning false from OnPreDraw.
Because all I'm doing here is offsetting the content.
But I don't want the current state of the
M View to be redrawn.
Because if I were to redraw in two different states, it would
be a very janky and not a very pleasant
animation for the user.
So returning false, but still not having removed the PreDraw
Listener, what's going to happen is it's going to come
back to OnPreDraw after the new values have been
recomputed.
At which point we will no longer
need the PreDraw Listener.
Then we go down.
We can retrieve the values from before for the top and
bottom bound changes.
And then we can get straight into calculating all the
animations that we need.
So before the animation took place, we stored the top and
bottom bounds for all the cells that
were previously visible.
And so if it was visible before the animation and the
cell is still visible after the animation, we're going to
go into this else case.
And all we need to do is animate the bounds from the
before state to the after state, which is very
straightforward.
This is what happens when the Parent of
the view is not known.
So this means it was visible before the layout change and
it's also present after the layout change.
But say the cell expanded so much that the cell will no
longer be visible after the layout change.
We're going to see that our View's parent is null.
So it's no longer contained in the ListView.
But up here, when I created my hash map, I said
setHasTransientState to true.
So what that did is even though it's no longer part of
the ListView, this view is not going to be recycled.
And as long as I store the reference to this ListView
myself, I can still make use of it after the layout has
taken place as long as I have a reference to it.
So scrolling down here, all I'm going to do is add the
exact same animation as I do for cells that are still
visible after the layout.
Except I'm also going to add it to this array of mine
called mViewsToDraw, which is going to tell me that this is
a view that needs to be animated off the screen, just
for animation purposes.
But it doesn't actually need to be rendered for the
purposes of the ListView use.
So scrolling down furthermore, I add animation to actually
expand the bounds of the cell being expanded.
And then I also fade in the expanding layout text.
So this does not necessarily have to be text, but the
layout itself is faded in by animating its alpha value from
zero to one.
Then during the animation itself, I wanted to see all my
ListView to prevent some weird behavior if the user were to
scroll during that time.
And once my animation completes, one thing that I
really have to do is make sure to set the transient state to
false for any views that previously had it to true, so
that these views don't get stored but can actually be
recycled and make lists you're scrolling more efficient
afterwards.
So that was expand view.
Now in dispatchDraw, what's going to happen is the only
things that I would want to draw on to the screen that are
not on the screen right now are views that were on the
ListView, but are no longer on the ListView afterwards.
I only need a reference to these during the animation.
Because once the animation completes, the user should not
be able to track them anymore.
And so since I have a reference to the view itself,
all I need to do is transfer the canvas appropriately, just
its top value.
Because the cell takes up the whole width, I just need to
modify where it's located vertically on the screen.
I draw the view onto the canvas.
And then I translate it back so that the next cell to be
drawn will be drawn in the correct location as well.
Scrolling down furthermore into Collapse View.
It's going to be very similar in most ways.
So before the cells collapse, I want to store the top and
bottom bounds of all the cells prior to the collapse.
I want to set Translate to True so nothing gets recycled
during the collapsing animation.
And then the only really important thing to note here
is that I'm not changing the expanding layout visibility
from visible to gone.
What I'm actually doing is setting the height of the
layout to a custom height that I pre-computed before, which
is what its default height is when it's
in a collapsed state.
And the reason I can't set the visibility to gone and just
work with the layout change from there is because I want
to get that fading effect as the cell is collapsing on the
expanding layout.
And that fading effect cannot be achieved if the expanding
layout visibility is gone or invisible.
By setting the height to a custom value, I can then
invoke a layout and add an OnPreDraw
Listener like we did before.
Very similar to the expand view method, I'm going to have
two passes.
The first pass is going to offset the ListView in such a
way to account for the custom collapse that
I'm going to achieve.
And once again I'm going to call set selection from top,
request a layout, and make sure to return false so the
TransientState of the ListView does not get
drawn onto the screen.
And then since the PreDraw Listener has not yet been
removed, I'm going to request that it has been called again.
It'll come back here for its second pass, where I will
firstly loop through all the visible
children in the ListView.
If I find that I do have access to the old bounds of
that view in the ListView, that means that this view was
present in the ListView before and after the collapse.
At which point, all I need to do is update its bounds and
then later animate them.
But if I find that I don't have access to the old bounds
of the cell, that means that this cell became visible only
after the collapse of the view.
So the collapse of the view actually made room for new
cells to appear.
But since I don't know what their old bounds were, I don't
have a reference point from where I want to animate them.
And so I just used the top and bottom translation values to
account for that.
Scrolling down here, I loop through all these same views
and add the animations to actually animate them from
these old bounds to their new bounds.
I also want to animate the collapse of
the collapsing cell.
And I want to animate the expanding layout from a value
of one to a value of zero, so it disappears.
Then I play all of these together in an animator set.
And then once it's actually done, what I do is set the
expanding layout visibility to gone.
So it doesn't get rendered on the screen at all anymore, nor
is it visible.
And I also update my layout parameters to wrap content.
Because when it's gone, I want to just wrap the content,
which means that none of the expanding
layout should be wrapped.
And then here, get animation.
This is actually how I animate the top and bottom bounds.
And since top and bottom have associated setters in Views,
called set top and set bottom, these values can be animated
as I would like for them to be.
And then I just return Object Animator, and
it does what I need.
The one other thing that I wanted to mention in this
animation is how I actually managed to achieve the
collapse of the expanding layout while fading it out.
So what's going to happen is when I set the height of the
layout to be a collapsed state, the ListView will not
want to render the expanding layout because it's not
visible on the screen.
So what I do here is overwrite OnMeasure, OnSize, and
onSizeChanged.
And what I'm telling it to do is that when it doesn't want
to display the expanding layout content, but if it
previously was in expanding layout state, by overriding
OnMeasure and returning the bigger height as opposed to
the smaller height, will keep it visible.
And that lets me animate its alpha value.
And once it's completely collapsed, that's when I set
it to gone, because I don't have anything
else to do with it.
So that's how you can expand and
collapse cells in ListView.
Thanks for watching.