Correctly animate removing row in ListView?

2019-02-11 06:34发布

问题:

The problem:

(1) add a touch listener to rows in a listview so that on swipe.

(2) swipe animation plays

(3) rows get deleted in the backend, and

(4) the animation plays without any flicker or jerkiness. By "flicker" I mean that the deleted row briefly shows after the animation finished.

I suspect that something funky was happening with the animation listener, so I ended up doing the following (done in the order given):

  1. Do the animation and make it persist by setting setFillAfter and setFillenabled to true
  2. Make the view invisible when the animation ends
  3. Delete the row in the database
  4. Reset the animation
  5. Reload the listview
  6. Make the view visible (but wait an additional 300 ms)

The result deletes the row without jerkiness or flicker BUT it now feels sluggish because of extra 300 ms wait. (I'm also not sure if this delay works across all devices.)

Update: I should point out that the 300 ms delay is what makes it work. That's weird because by that point the animation was reset and the listview has the latest data. There should be no reason why making the view visible makes the old row briefly show, right?

I also tried using a ViewPropertyAnimator (as per Using animation on a ViewPager and setFillAfter) but for some reason the onAnimationEnd listener was called at every step of the animation.

I also read that we should implement a custom view and override its onAnimationEnd listener. (However, I haven't tried that approach yet.)

Update: just tried to add an extra dummy animation at the end (as per Android Animation Flicker). However, that doesn't work

My test phone runs Ice Cream Sandwich. My app is targeting Gingerbread and after.

So what's the proper solution? Am I doing this the wrong way?

Here's the code:

 @Override
public boolean onTouch(final View view, MotionEvent event)
{
    //...

    switch(action) {
        //...
        case MotionEvent.ACTION_MOVE:
            // ...
            if (//check for fling
            {
                view.clearAnimation();
                //animation = standard translate animation
                animation.setAnimationListener(new AnimationListener() {
                    //

                    @Override
                    public void onAnimationEnd(Animation animation)
                    {
                        view.setVisibility(View.INVISIBLE);
                        flingListener.onFling(cursorPosition, view, velocity);
                    }

                    //...
                });
                view.startAnimation(animation);
            }
            break;
      //
 }

The "fling listener":

    @Override
    public void onFling(int position, final View view, float velocity)
    {
        //delete row -- its actually a Loader
        //the following code runs in the Loader's onLoadFinished
        view.clearAnimation();
        adapter.notifyDataSetChanged();
        adapter.swapCursor(null);

        //reload listview -- it's actually a Loader
        //the following code runs in the Loader's onLoadFinished
        adapter.swapCursor(cursor);
        view.postDelayed(new Runnable() {
            @Override
            public void run()
            {
                view.setVisibility(View.VISIBLE);
            }
        }, 300);
    }

Update: After comparing Chet Haase's code, we are doing similar things with some important differences: (1) he uses a onPreDraw listener with the ListView tree observer to do the actual deletion, (2) he removes the row not only from the array but also from the listview. After mimicking his code, it still didn't work. The problem is now the Loader---I use a Loader to delete rows asynchronously. The Loader seems to force an additional draw call to the ListView...before the row has been deleted in the backend. And that's (another) cause of the flicker. Still haven't figured out a workaround though.

回答1:

Chet Haase (Google Engineer) put together a really good DevBytes on this topic I'd suggest watching/taking ideas from his source. If you use NineOldAndroids, I think this'll be backwards compatible to GB.

Check it out here:

http://graphics-geek.blogspot.com/2013/06/devbytes-animating-listview-deletion.html



回答2:

As I pointed out in the comments, the problem with Chet's code is that its designed for synchronous data access. Once you start asynchronously deleting rows, his code fails.

I found the solution to the flicker problem by combining Chet's code with this answer: CursorAdapter backed ListView delete animation "flickers" on delete

The solution to correctly do a row deletion aynchronously is:

  1. Create a onPreDraw listener for the ListView tree observer to prevent flicker. All code in this listener runs before the list view re-draws itself, preventing flicker.
  2. Find a way to delete the row in the listview (but not yet in the database). There are two approaches (see CursorAdapter backed ListView delete animation "flickers" on delete):
    1. Create a AbstractCursor wrapper that ignores the row to be deleted and swap it in for the real cursor. OR
    2. Mark the row to be deleted as "stained" and act on it appropriately when redrawing the row.
  3. Remove the row for real in the database (asynchronously).

Some pseudo-code using the AbstractCursor wrapper (it's technically called a "proxy"):

    //Called when you swipe a row to delete
    @Override
    public void onFling(final int positionToRemove, final View view)
    {
        final ViewTreeObserver observer = listView.getViewTreeObserver();
        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
        {
            public boolean onPreDraw()
            {
                observer.removeOnPreDrawListener(this);

                //remove the row from the matrix cursor
                CursorProxy newCursor = new CursorProxy(cursor,
                                                        positionToRemove);
                swapCursor(newCursor);
                //delete row in database
             }
        }
  }