Tip:
Highlight text to annotate it
X
CHET HAASE: Hi.
I'm Chet Haase, an engineer on the Android Team at Google.
I do a bunch of work on animations, and I've done some
DevBytes in the past on ListView animations.
And I got a request to show how you might animate adding
and removing items from the ListView.
So recently, I did a talk with Romain Guy on animations at
Google I/O. You might want to check that out since it has
some of the content and related content as well.
And one of the demos that I showed was removing items from
the ListView, exactly what people wanted.
I've tuned the code a little bit to make it a little more
general purpose, and we're going to show a bit more about
how it actually works today.
So the idea with ListView removals is that you're
deleting an item there.
Maybe you want to swipe it out.
In the demo that I have, you swipe these items out, and
then the swipe itself is animated and after the item is
removed from the ListView, the ListView collapses the gap
around that item and views move in from the top or the
bottom, wherever they need to.
And all of this is seamless to the user.
It's kind of the effect and the feel that you want with
the ListView, but it may not be particularly
obvious how to do so.
So let's see how this actually works today.
Now, one important point that I should point out, if you
look closely at the code in the Google I/O version of this
demo, you'll see that I was using a property called
Transient State, which was added in Jelly Bean.
I've actually removed that.
It's used implicitly under the hood when you use the View
Property Animator, but I'm not using it explicitly in the
demo code anymore, because I actually found a more general
purpose way of doing this animation so it's a little
more applicable to different releases as well.
So let's see how it works.
So if we take a look at the code, one important point--
and this is right at the heart of why I don't need Transient
State anymore-- is that I'm using a Stable Array Adapter.
This is a subclass of Array Adapter that I created to
return True for Has Stable IDs.
This is a key element because I'm no longer animating the
views directly, getting information directly from the
views and then making sure that those views all stay on
the screen at the same time.
That's dependent upon the Jelly Bean capability of
Transient State.
Instead, I am dependent upon the underlying Array Adapter
to have Stable IDs so that I can count on those items
actually being the same as changes in the
Array Adapter happen.
So I do that by returning True there, and when I get Item ID,
I actually get unique IDs as I process the array.
And then the only other custom thing that happens in this
Array Adapter is I set an On Touch Listener because every
item in the view, as it's created, I want it to know
where to go for its touch events because I'm going to
customize that with some swiping behavior, which we'll
see very soon.
So if we go back to our Activity Class, here's that
Touch Listener that we were just talking about.
So the idea here is this.
I want to note when the user clicks down, and I want to
note when they swipe left to right.
Because I want to, first, detect when it's gone far
enough that we're actually in a swiping motion.
So I'm going to start animating that as they move
their finger.
So then I want to position the view, and I want to also set
the alpha of the view to make it disappear more, the closer
to the edge of the screen that it gets.
And then when they actually let go of that item, I want to
determine whether to snap the item back or to animate it all
the way off.
I should point out that in the widgets that we actually ship,
we use Velocity Tracking, which is a better way to do
this in general.
I'm using a simple heuristic of how far have you moved
instead, which works for the purposes of the demo.
I think a more full version of this effect would actually use
Velocity Tracking instead so that as you lifted off your
finger as you're swiping to the right, then the item would
actually animate at the same speed that it was just moving,
which is a nicer effect.
Nevertheless, a simple approach here, and also simple
to change using Velocity instead.
So when the user presses down, if there's another item
pressing, I'm not handling multi-touch clicks here.
We're just making a simple demo here so we return False.
We're not going to handle this event.
Otherwise, we set the value to say, OK, we're now tracking
things, and we're going to note the initial location that
they pressed down in so that we know how far to move the
item in initial move events.
When there is a subsequent move event here, we get the
current location of that, and then we know how far the user
has moved the item in general, and then we also know whether
to detect for swipe slop, which is basically how far
have they moved?
Have they moved past the point where yes, we think it's not
just an error, but we should actually start
swiping this thing out.
So you see a little jump at the beginning as we detect
that it's not just noise but they're actually moving it far
enough for us to start swiping it away.
We tell the ListView to disallow intercept touch
events, because I don't want the ListView doing its own
thing with scrolling up and down at the same time as I'm
trying to swipe items back and forth.
So when you call this method and set it to True, then until
the final Up Event from the Touch Event Processor, then
the items have full control over here.
The ListView will not get in your way.
And then we tell the Background Container to show
the background.
We'll get into this a little bit later.
This is the detail about what exactly are we going to show
behind the item as it's swiped away?
So if we're actually swiping this thing, then we're going
to set the translation x.
So we're going to move this item back and forth according
to how far the user's finger has moved, and we're going to
set the alpha.
We basically want it to be the inverse of how far away it is
from wherever they started.
So the closer they get to the edge of the screen one way or
the other, the more translucent it's going to be,
the idea being that it's going to fade out all the way by the
time it gets off the screen.
And then finally, when the user lifts up, if they were
swiping this thing, then we can find out how far they
swiped it, whether they're going off the screen to the
left, off the screen to the right, and then we can set the
duration based on that.
We're going to set the duration based on how far they
have left to go to the edge of the screen, whether it's
popping back into place or actually popping all the way
off the screen.
We're going to set enabled False on the ListView.
This prevents the user from doing something silly like
actually scrolling and flinging the ListView at the
same time as we're animating all the views into place.
Makes things look a little bit more consistent and reasonable
on the screen.
And then we're going to set that enabled to True when
we're actually all done with the animation.
There's two animations that are going to kick in here.
The first is that we're going to animate the Swipe Out or
the Swipe Back.
And if it was a Swipe Out, then we're going to run a
following animation, which is going to animate the closure
of the gap that that item created when it was removed
from the list.
So we're going to set the duration according to that
duration that we calculated before.
We're going to animate alpha.
This is using a View Property Animator, which was an API
that came in in 3.1, Honeycomb 3.1.
And we're also going to animate translation x to its
end value, either back all the way on the screen or all the
way off the screen.
And then we're going to run End Action.
Now this is an API that came in in Jelly Bean, so if you're
writing for Jelly Bean and later, that's fine.
If you're writing for run times that are earlier than
Jelly Bean, you don't have to use an End Action here.
Instead, you can set up a listener on the View Property
Animator, and it's just a little bit more code, but it's
effectively the same thing.
When the end of the animation happens, we're
going to run this code.
So the swipe has happened.
Now we actually want to animate the removal, animate
removing the gap in the ListView.
So if we're actually removing the item if it was swiped off,
then we're going to animate removal here.
Otherwise, we're all done, and we're just going to restore
the values that we need.
So animate removal is down here.
Now, this is the different approach from the one that we
took at Google I/O. The talk in Google I/O talked about
let's track where the views are here and let's set
Transient State on all those views to make sure that they
don't get recycled when the layout occurs.
We don't care about that anymore because we're using
Item IDs instead.
So what's happening is the following.
For all children that are currently in the ListView--
and this is before layout runs.
So there's no data set change yet.
The list is just the way that it was at the time that the
user was swiping it-- we're going to track where these
things are.
So we want to know where the views are, but we're going to
store that information about position in a hash map that's
keyed off of the Item ID instead of the view itself.
So for every view, we're going to say, where are you now?
We're going to get the top value, child.getTop.
We're going to find out where it is in its container, and
we're going to store that information with the key of
the Item ID that's associated with that view before Layout
runs and Recycle happens and everything gets all confused
in the ListView.
But we don't care, because we've got the information we
need, Item ID associated with position of
the view at the time.
Then we're actually going to remove the item, and we're
going to remove it from the adapter.
That automatically does a Notify Data Set change.
And then we use a trick that's common to a lot of my
animation demos, which is we set an On Pre-Draw Listener.
So what's happened is we've removed the item.
This is going to force a Layout to
happen on the ListView.
Views are probably going to be recycled.
We're going to chuck them out of the ListView.
We're going to figure out where items need to go.
Then we're going to repopulate views.
And who knows what view is where?
But we don't care, because we have stable IDs.
We're good to go.
Layout is going to happen, and then our Pre-Draw Listener is
going to be called.
And this is the important point.
It's going to be called before draws happen.
So Layout runs, then it calls our On Pre-Draw Listener, and
then we can do whatever we want.
And what we want to do is set up an animation to animate
views from where they were in the previous frame into where
we want them to be now that Layout has run and the
ListView has finalized all the content for
these views that changed.
So the On Pre-Draw Callback gets called here.
We remove the Listener, because we don't care about it
after this one frame, then we come in here and walk through
all current children of the ListView.
So this is everything after Layout ran, everything got
recycled, shuffled around.
For every one of those, we're going to say, OK,
where are you now?
Give me your current top position.
And if the position of that same ID--
so I'm also tracking the current ID of this view, and I
know what the ID used to be for that content
before Layout ran.
And if we have a non-null value for Start Top, that
means that this view used to be somewhere else in the
container, and we're going to run an animation to animate
that change in values.
So if the top position before Layout for this content is
different than the top position for that item after
Layout, then we want to run the animation.
So we're going to set an initial translation y value to
basically run it back to wherever it started from, and
then we're going to animate to translation y of 0.
So we're basically just going to animate it into place.
If this is the first time it's running, then we want to, with
the first animation, go ahead and restore some values when
we're done, kind of a tedious detail there.
If there is no starting position for that particular
content item, then that means that this view is just moving
onto the screen, either from the bottom or the top.
So we calculate an initial position to start from, and
there's a little bit of repeated code there.
Don't worry about that.
And then we come in here and we, again, set the initial
translation y value to that delta, and we animate it.
The animation code is exactly what we saw before.
The only difference is calculating the initial
position for an item that was previously off the screen,
didn't exist in the ListView container before.
And then when we're all done, we clear the top map, which is
where we were storing these values of Item IDs associated
with the top positions.
That's all the animation code here.
The only other thing that's interesting here is the
Background Container.
So there's kind of a graphics optimization here where, if we
are constantly drawing a background behind a ListView
where all of the items are opaque, then that means we're
basically getting overdraw.
We're drawing an entire background, and then we're
drawing all the items on top of that background, which is
kind of a wasted effort.
If you really care about animation, if you really care
about performance and you notice that you're getting
bottlenecked in some situations, then one way you
could work around this is to set a null background on that
container so that we're not going to draw
anything behind it.
Well, that's a problem if you're swiping out and you see
into the nothingness that's behind the ListView.
So what we do is when we start doing the swipe, we tell the
Background Container to show the background.
That sets up some initial values here so that we know
where to draw this item background.
And then we override on Draw and say, OK, if we're drawing
this thing, then we'll go ahead and translate to the
open area, and we'll draw our shadowed background.
And our shadowed background is seen here.
This is called from the constructors, and we simply
load in this resource.
This is a nine patch that I defined.
It's very simple.
It's basically shadow on top, shadow on bottom, and then an
area in the middle that gets stretched.
And the net effect, which you can see in the demo, is that
we have a little bit of shadow on top and a little bit of
shadow on bottom, and then this grey area in the middle,
so it makes it look like all of the items in the ListView
are popped out from this virtual background that we
have there.
Another item to mention is sometimes when you're
animating ListViews, if you have dividers in the ListView
and you're playing with things like we are here, like
animating the translation properties, you may notice the
dividers aren't necessarily getting drawn correctly.
So I disabled dividers.
So if you take a look at the layout for application, we
have ListView divider of null and a divider height of zero.
So the ListView itself is not drawing dividers, and instead,
I've defined the TextView to actually use a divider.
So it's using a background, which I defined, which is
basically all opaque white with an opaque line at the
bottom of it, and that gives me my own divider that gets
animated along with these items as they're moving around
the ListView.
So again, this is ListView removal animations.
It's the ability to animate all the changes.
You could do a very similar thing with adding.
I just only took care of the one direction today.
But it's doing it in a way that is usable on all property
animation APIs all the way back to when View Property
Animator came in, which was 3.1.
In fact, you could use Object Animator for everything and
take it all the way back to 3.0.
And if you want a little teaser, in a future demo,
we'll see how we can use a very similar technique and
actually run this thing on Gingerbread as well.
So check out the talk at Google I/O that we did on
animation called "A Moving Experience," and download the
code and play with it, and make your
ListViews more dynamic.
Thanks.