Hi, I'm Chet Haase, an engineer on the
Android team at Google.
I work on graphics and animation, but today, I'm
going to talk about something a little bit different.
Little bit of animation, but it's more about
doing things wrong.
So this is the first in possibly an ongoing series
which I like to call You're Doing It Wrong.
So the demo I'm going to show you today, this is really
hacked up, as many of my demos are, and very obvious, and you
may not do things exactly this way.
But these are examples taken from real apps shipping on the
device, where we've seen approaches like this or like
this in a maybe much more complex and not as
obviously bad way.
So it's important to sort of take the main idea away from
it, if not the exact implementation.
So in particular, I want to look at ListView and the idea
that there's a distinction between the position of items
on the screen and the views that represent the content in
those items.
And you should understand those differences very
carefully for the following reason.
So let's take a look at the demo, which I call
ListViewDeletion.
And we've got a list of cheeses on the screen.
We can select several of these, and we want to select
the ones that we're getting rid of, so we
click the Delete Selected.
They fade out, so that's the animation part of this thing.
They fade out, and then we remove them from the adapter.
And the ListView updates, and all things are wonderful.
But what happens if we select a few of these, and then we
scroll down and, OK, we'll go ahead and run the deletion.
OK, let's run that one again.
If I could slow-mo this, I would.
Notice that there are things fading out on the screen that
had nothing to do with the ones that are checked.
In fact, if I scroll all the checked ones off the screen,
nothing should fade out because none of these items
are actually selected.
So they shouldn't be removed, but they are.
But actually, the content of the items themselves is not
being deleted.
What's being deleted are the views.
So we fade out the view, and then we delete the items.
Well, the items that are being deleted are not the views that
are being faded out.
So what's going on?
So there's a concept in ListView of view recycling.
This is done because you can have ListViews of enormous
size with tons of views in them, and we don't want to
instantiate all of those things and have them
persistent in the state of the activity at all times, and we
don't want to have to update all this stuff every time we
redraw the ListView.
So instead, we have active views for the items that are
on the screen at the time.
And as views scroll off, those views get recycled and used
for other content on the screen.
It's very nice.
It makes ListView very smooth to fling and to scroll and to
interact with.
However, it means that if you are assuming things about the
specific views that are being used in a ListView, you're
doing it wrong.
It's important to understand that ListView--
you should really think of it as a holder of content, and
that you need to really interact with that content in
terms of the adapter directly and not necessarily the views
on the screen, or else you at least need to
know what you're doing.
So let's take a look at the code and see what the
application is doing wrong.
So we have this ListViewDeletion class.
We create this array list holding this large list of
cheeses, terribly interesting content.
And then we have this set on Click Listener
for the Delete Button.
So when you click the button, it figures out what to delete
and runs animations and removes things appropriately.
And then there's some code down here where, when you
actually checked on these things so we can
take a look at that.
On Item Click Listener for the check box, it basically adds
these views to this list of checked views there.
So we track that.
Now, this is kind of the hack part of the demo, where we're
actually tracking the views that are actually being
deleted for the purposes of the demo.
So we go through here, and we go through the checked items
of the ListView.
And this is the default code, which is bad, which is--
OK, this check box to control the behavior is not currently
checked, so we're going to run the code that we thought was
obvious, and we're going to fade out
views and remove them.
So first of all, we're going to post this delayed thing to
actually remove it from the adapter after the fade
animation is done.
We know that the animation is going to take 300
milliseconds.
The actual animation is run down here, where we iterate
through the checked views list.
And for each one of those, we grab the view that was
checked, and we run an animation on it.
This is using the view property animator API, which
was introduced in the 3.2 release.
So we say animate the alpha property to 0, and when you
finish, let's restore the alpha property because that's
good practice.
Otherwise, the thing stays invisible.
So all of that looks good except for the part where, if
you scrolled off the screen, that view got recycled and
reused by completely different content.
So now we're going to fade out the view, which has nothing to
do with the content that was checked, which is now sitting
off the screen.
Bad way to go.
If we go back and look at the demo again, and we say Use
Positions instead of Use Views, and we check a bunch of
things on here, and then we run this thing, well, of
course, it does the right thing when they're on the
screen anyway.
But if we scroll these things off the screen, then it fades
out only the ones that are actually selected that are on
the screen.
And that logic works as follows.
So in here, we go into the else clause.
First of all, we clear this thing so that we're not going
to run animations on these because we have a different
way of managing our animations in this approach.
We get the position on the screen, so we can get the
first visible position, and then everything after that is
numbered after that position.
So we can say, OK, this is the first visible position, and
then we iterate through it, so this is the
number of checked items.
So we're actually iterating through the
ListView items here.
So we can get the position of the ListView item itself, the
position of the first visible position, and then figure out
whether that thing is actually on screen.
And if it's not on screen, don't run an animation at all.
Just remove the thing.
In fact, we should remove it after we're done so that it
doesn't blink the list and move stuff around at the time.
But we basically use that as a trigger to determine whether
it's on screen, therefore run an animation on the thing, or
it's off screen, don't bother with the animation, just
remove it when all the animations are done.
So if it is on screen, then we do a similar animation as we
did before.
We animate the alpha property on that view with an End
action to restore alpha, and after we've restored alpha,
then we remove it from the adapter.
So all the animation code, before it was good.
What wasn't good was that we were using a view that was
determined at check time, not at the time when we were
actually telling the system to delete, and we knew what was
actually on the screen at the time.
So the takeaway here is be aware of the position of your
items on the screen, be aware that your views can be
recycled, and be aware that there is a really large
disconnect, potentially, between those positions and
the views and the data underneath that the views
happen to be displaying at any point in time.
So thanks.