简体   繁体   中英

How to manage undo after swipe to delete in android ListView?

I need to implement swipe to delete in a ListView with Undo function as in Gmail App

I know that there are questions already asking about swipe to delete like

Remove item listview with Slide - Like Gmail

and

android swipe to delete list row

But none of them explains how to manage Undo after deletion and Animate back the deleted view on undo!

I found other one with no Answer here

How to implement gmail like Achieve or Undo action in a list item

So here is my question.

How to create a listview in android with swipe to delete then show UNDO in the deleted space and Animate back the same view if the undo is pressed, also remove the undo option on scroll or other item click or swipe?

You smart people have any idea?

| ----------- ITEM 1-----------|

| ----------- ITEM 2-----------|

| --Deleted-------<[UNDO]>|

| ----------- ITEM 4-----------|

| ----------- ITEM 5-----------|

note: sorry that I cant add an image because of my low reputation!

您可以在https://github.com/nhaarman/ListViewAnimations找到您要求的确切内容

Your question sounds like you're confused about how to manage the actual undo part. If that is the case then I would suggest you look into the design pattern called 'Command Design Pattern'. You can queue up 'Commands' like delete, then only commit them once the undo timer or however you implement it is finished.

Library to make the items in a ListView or RecyclerView dismissable with the possibility to undo it, using your own view to provide this functionality like the Gmail app for Android does.

https://github.com/hudomju/android-swipe-to-dismiss-undo

Here's a gist, just in case the page gets changed or link goes invalid:

ListViewActivity:

public class ListViewActivity extends Activity {

private static final int TIME_TO_AUTOMATICALLY_DISMISS_ITEM = 3000;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.list_view_activity);
    init((ListView) findViewById(R.id.list_view));
}

private void init(ListView listView) {
    final MyBaseAdapter adapter = new MyBaseAdapter();
    listView.setAdapter(adapter);
    final SwipeToDismissTouchListener<ListViewAdapter> touchListener =
            new SwipeToDismissTouchListener<>(
                    new ListViewAdapter(listView),
                    new SwipeToDismissTouchListener.DismissCallbacks<ListViewAdapter>() {
                        @Override
                        public boolean canDismiss(int position) {
                            return true;
                        }

                        @Override
                        public void onPendingDismiss(ListViewAdapter recyclerView, int position) {

                        }

                        @Override
                        public void onDismiss(ListViewAdapter view, int position) {
                            adapter.remove(position);
                        }
                    });

    touchListener.setDismissDelay(TIME_TO_AUTOMATICALLY_DISMISS_ITEM);
    listView.setOnTouchListener(touchListener);
    // Setting this scroll listener is required to ensure that during ListView scrolling,
    // we don't look for swipes.
    listView.setOnScrollListener((AbsListView.OnScrollListener) touchListener.makeScrollListener());
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            if (touchListener.existPendingDismisses()) {
                touchListener.undoPendingDismiss();
            } else {
                Toast.makeText(ListViewActivity.this, "Position " + position, LENGTH_SHORT).show();
            }
        }
    });
}

static class MyBaseAdapter extends BaseAdapter {

    private static final int SIZE = 100;

    private final List<String> mDataSet = new ArrayList<>();

    MyBaseAdapter() {
        for (int i = 0; i < SIZE; i++)
            mDataSet.add(i, "This is row number " + i);
    }

    @Override
    public int getCount() {
        return mDataSet.size();
    }

    @Override
    public String getItem(int position) {
        return mDataSet.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    public void remove(int position) {
        mDataSet.remove(position);
        notifyDataSetChanged();
    }

    static class ViewHolder {
        TextView dataTextView;
        ViewHolder(View view) {
            dataTextView = (TextView) view.findViewById(R.id.txt_data);
            view.setTag(this);
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder viewHolder = convertView == null
                ? new ViewHolder(convertView = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.list_item, parent, false))
                : (ViewHolder) convertView.getTag();

        viewHolder.dataTextView.setText(mDataSet.get(position));
        return convertView;
    }
}

}

SwipeToDismissTouchListener:

public class SwipeToDismissTouchListener<SomeCollectionView extends ViewAdapter> implements
    View.OnTouchListener {

// Cached ViewConfiguration and system-wide constant values
private final int mSlop;
private final int mMinFlingVelocity;
private final int mMaxFlingVelocity;
private final long mAnimationTime;

// Fixed properties
private final SomeCollectionView mRecyclerView;
private final DismissCallbacks<SomeCollectionView> mCallbacks;
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero

// Transient properties
private PendingDismissData mPendingDismiss;
private float mDownX;
private float mDownY;
private boolean mSwiping;
private int mSwipingSlop;
private VelocityTracker mVelocityTracker;
private int mDownPosition;
private RowContainer mRowContainer;
private boolean mPaused;

// Handler to dismiss pending items after a delay
private final Handler mHandler;
private final Runnable mDismissRunnable = new Runnable() {
    @Override
    public void run() {
        processPendingDismisses();
    }
};
private long mDismissDelayMillis = -1; // negative to disable automatic dismissing

public class RowContainer {

    final View container;
    final View dataContainer;
    final View undoContainer;
    boolean dataContainerHasBeenDismissed;

    public RowContainer(ViewGroup container) {
        this.container = container;
        dataContainer = container.getChildAt(0);
        undoContainer = container.getChildAt(1);
        dataContainerHasBeenDismissed = false;
    }

    View getCurrentSwipingView() {
        return dataContainerHasBeenDismissed ? undoContainer: dataContainer;
    }

}

/**
 * The callback interface used by {@link SwipeToDismissTouchListener} to inform its client
 * about a successful dismissal of one or more list item positions.
 */
public interface DismissCallbacks<SomeCollectionView extends ViewAdapter> {
    /**
     * Called to determine whether the given position can be dismissed.
     */
    boolean canDismiss(int position);

    /**
     * Called when an item is swiped away by the user and the undo layout is completely visible.
     * Do NOT remove the list item yet, that should be done in {@link #onDismiss(com.hudomju.swipe.adapter.ViewAdapter, int)}
     * This may also be called immediately before and item is completely dismissed.
     *
     * @param recyclerView The originating {@link android.support.v7.widget.RecyclerView}.
     * @param position The position of the dismissed item.
     */
    void onPendingDismiss(SomeCollectionView recyclerView, int position);

    /**
     * Called when the item is completely dismissed and removed from the list, after the undo layout is hidden.
     *
     * @param recyclerView The originating {@link android.support.v7.widget.RecyclerView}.
     * @param position The position of the dismissed item.
     */
    void onDismiss(SomeCollectionView recyclerView, int position);
}

/**
 * Constructs a new swipe-to-dismiss touch listener for the given list view.
 *
 * @param recyclerView  The list view whose items should be dismissable.
 * @param callbacks The callback to trigger when the user has indicated that she would like to
 *                  dismiss one or more list items.
 */
public SwipeToDismissTouchListener(SomeCollectionView recyclerView,
                                   DismissCallbacks<SomeCollectionView> callbacks) {
    ViewConfiguration vc = ViewConfiguration.get(recyclerView.getContext());
    mSlop = vc.getScaledTouchSlop();
    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    mAnimationTime = recyclerView.getContext().getResources().getInteger(
            android.R.integer.config_shortAnimTime);
    mRecyclerView = recyclerView;
    mCallbacks = callbacks;
    mHandler = new Handler();
}

/**
 * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
 *
 * @param enabled Whether or not to watch for gestures.
 */
public void setEnabled(boolean enabled) {
    mPaused = !enabled;
}

/**
 * Set the delay after which the pending items will be dismissed when there was no user action.
 * Set to a negative value to disable automatic dismissing items.
 * @param dismissDelayMillis The delay between onPendingDismiss and onDismiss calls, in milliseconds.
 */
public void setDismissDelay(long dismissDelayMillis) {
    this.mDismissDelayMillis = dismissDelayMillis;
}

/**
 * Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the {@link
 * android.widget.ListView} using {@link android.widget.ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
 * If a scroll listener is already assigned, the caller should still pass scroll changes through
 * to this listener. This will ensure that this {@link SwipeToDismissTouchListener} is
 * paused during list view scrolling.</p>
 *
 * @see SwipeToDismissTouchListener
 */
public Object makeScrollListener() {
    return mRecyclerView.makeScrollListener(new AbsListView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView absListView, int scrollState) {
            processPendingDismisses();
            setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
        }

        @Override
        public void onScroll(AbsListView absListView, int i, int i1, int i2) {
        }
    });
}

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
    if (mViewWidth < 2) {
        mViewWidth = mRecyclerView.getWidth();
    }

    switch (motionEvent.getActionMasked()) {
        case MotionEvent.ACTION_DOWN: {
            if (mPaused) {
                return false;
            }

            // TODO: ensure this is a finger, and set a flag

            // Find the child view that was touched (perform a hit test)
            Rect rect = new Rect();
            int childCount = mRecyclerView.getChildCount();
            int[] listViewCoords = new int[2];
            mRecyclerView.getLocationOnScreen(listViewCoords);
            int x = (int) motionEvent.getRawX() - listViewCoords[0];
            int y = (int) motionEvent.getRawY() - listViewCoords[1];
            View child;
            for (int i = 0; i < childCount; i++) {
                child = mRecyclerView.getChildAt(i);
                child.getHitRect(rect);
                if (rect.contains(x, y)) {
                    assert child instanceof ViewGroup &&
                            ((ViewGroup) child).getChildCount() == 2 :
                            "Each child needs to extend from ViewGroup and have two children";

                    boolean dataContainerHasBeenDismissed = mPendingDismiss != null &&
                            mPendingDismiss.position == mRecyclerView.getChildPosition(child) &&
                            mPendingDismiss.rowContainer.dataContainerHasBeenDismissed;
                    mRowContainer = new RowContainer((ViewGroup) child);
                    mRowContainer.dataContainerHasBeenDismissed = dataContainerHasBeenDismissed;
                    break;
                }
            }

            if (mRowContainer != null) {
                mDownX = motionEvent.getRawX();
                mDownY = motionEvent.getRawY();
                mDownPosition = mRecyclerView.getChildPosition(mRowContainer.container);
                if (mCallbacks.canDismiss(mDownPosition)) {
                    mVelocityTracker = VelocityTracker.obtain();
                    mVelocityTracker.addMovement(motionEvent);
                } else {
                    mRowContainer = null;
                }
            }
            return false;
        }

        case MotionEvent.ACTION_CANCEL: {
            if (mVelocityTracker == null) {
                break;
            }

            if (mRowContainer != null && mSwiping) {
                // cancel
                mRowContainer.getCurrentSwipingView()
                        .animate()
                        .translationX(0)
                        .alpha(1)
                        .setDuration(mAnimationTime)
                        .setListener(null);
            }
            mVelocityTracker.recycle();
            mVelocityTracker = null;
            mDownX = 0;
            mDownY = 0;
            mRowContainer = null;
            mDownPosition = ListView.INVALID_POSITION;
            mSwiping = false;
            break;
        }

        case MotionEvent.ACTION_UP: {
            if (mVelocityTracker == null) {
                break;
            }

            float deltaX = motionEvent.getRawX() - mDownX;
            mVelocityTracker.addMovement(motionEvent);
            mVelocityTracker.computeCurrentVelocity(1000);
            float velocityX = mVelocityTracker.getXVelocity();
            float absVelocityX = Math.abs(velocityX);
            float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
            boolean dismiss = false;
            boolean dismissRight = false;
            if (Math.abs(deltaX) > mViewWidth / 2 && mSwiping) {
                dismiss = true;
                dismissRight = deltaX > 0;
            } else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity
                    && absVelocityY < absVelocityX && mSwiping) {
                // dismiss only if flinging in the same direction as dragging
                dismiss = (velocityX < 0) == (deltaX < 0);
                dismissRight = mVelocityTracker.getXVelocity() > 0;
            }
            if (dismiss && mDownPosition != ListView.INVALID_POSITION) {
                // dismiss
                final RowContainer downView = mRowContainer; // mDownView gets null'd before animation ends
                final int downPosition = mDownPosition;
                mRowContainer.getCurrentSwipingView()
                        .animate()
                        .translationX(dismissRight ? mViewWidth : -mViewWidth)
                        .alpha(0)
                        .setDuration(mAnimationTime)
                        .setListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(Animator animation) {
                                performDismiss(downView, downPosition);
                            }
                        });
            } else {
                // cancel
                mRowContainer.getCurrentSwipingView()
                        .animate()
                        .translationX(0)
                        .alpha(1)
                        .setDuration(mAnimationTime)
                        .setListener(null);
            }
            mVelocityTracker.recycle();
            mVelocityTracker = null;
            mDownX = 0;
            mDownY = 0;
            mRowContainer = null;
            mDownPosition = ListView.INVALID_POSITION;
            mSwiping = false;
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            if (mVelocityTracker == null || mPaused) {
                break;
            }

            mVelocityTracker.addMovement(motionEvent);
            float deltaX = motionEvent.getRawX() - mDownX;
            float deltaY = motionEvent.getRawY() - mDownY;
            if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2) {
                mSwiping = true;
                mSwipingSlop = deltaX > 0 ? mSlop : -mSlop;
                mRecyclerView.requestDisallowInterceptTouchEvent(true);

                // Cancel ListView's touch (un-highlighting the item)
                MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
                cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
                        (motionEvent.getActionIndex()
                                << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                mRecyclerView.onTouchEvent(cancelEvent);
                cancelEvent.recycle();
            }

            if (mSwiping) {
                mRowContainer.getCurrentSwipingView().setTranslationX(deltaX - mSwipingSlop);
                mRowContainer.getCurrentSwipingView().setAlpha(Math.max(0f, Math.min(1f,
                        1f - 2f * Math.abs(deltaX) / mViewWidth)));
                return true;
            }
            break;
        }
    }
    return false;
}

class PendingDismissData implements Comparable<PendingDismissData> {
    public int position;
    public RowContainer rowContainer;

    public PendingDismissData(int position, RowContainer rowContainer) {
        this.position = position;
        this.rowContainer= rowContainer;
    }

    @Override
    public int compareTo(@NonNull PendingDismissData other) {
        // Sort by descending position
        return other.position - position;
    }
}

private void performDismiss(RowContainer dismissView, int dismissPosition) {
    // Animate the dismissed list item to zero-height and fire the dismiss callback when
    // all dismissed list item animations have completed. This triggers layout on each animation
    // frame; in the future we may want to do something smarter and more performant.
    if (mPendingDismiss != null) {
        boolean dismissingDifferentRow = mPendingDismiss.position != dismissPosition;
        int newPosition = mPendingDismiss.position < dismissPosition ? dismissPosition-1 : dismissPosition;
        processPendingDismisses();
        if (dismissingDifferentRow) {
            addPendingDismiss(dismissView, newPosition);
        }
    } else {
        addPendingDismiss(dismissView, dismissPosition);
    }
}

private void addPendingDismiss(RowContainer dismissView, int dismissPosition) {
    dismissView.dataContainerHasBeenDismissed = true;
    dismissView.undoContainer.setVisibility(View.VISIBLE);
    mPendingDismiss = new PendingDismissData(dismissPosition, dismissView);
    // Notify the callbacks
    mCallbacks.onPendingDismiss(mRecyclerView, dismissPosition);
    // Automatically dismiss the item after a certain delay
    if(mDismissDelayMillis >= 0)
        mHandler.removeCallbacks(mDismissRunnable);
        mHandler.postDelayed(mDismissRunnable, mDismissDelayMillis);
}

/**
 * If a view was dismissed and the undo container is showing it will proceed with the final
 * dismiss of the item.
 * @return whether there were any pending rows to be dismissed.
 */
public boolean processPendingDismisses() {
    boolean existPendingDismisses = existPendingDismisses();
    if (existPendingDismisses) processPendingDismisses(mPendingDismiss);
    return existPendingDismisses;
}

/**
 * Whether a row has been dismissed and is waiting for confirmation
 * @return whether there are any pending rows to be dismissed.
 */
public boolean existPendingDismisses() {
    return mPendingDismiss != null && mPendingDismiss.rowContainer.dataContainerHasBeenDismissed;
}

/**
 * If a view was dismissed and the undo container is showing it will undo and make the data
 * container reappear.
 * @return whether there were any pending rows to be dismissed.
 */
public boolean undoPendingDismiss() {
    boolean existPendingDismisses = existPendingDismisses();
    if (existPendingDismisses) {
        mPendingDismiss.rowContainer.undoContainer.setVisibility(View.GONE);
        mPendingDismiss.rowContainer.dataContainer
                .animate()
                .translationX(0)
                .alpha(1)
                .setDuration(mAnimationTime)
                .setListener(null);
        mPendingDismiss = null;
    }
    return existPendingDismisses;
}

private void processPendingDismisses(final PendingDismissData pendingDismissData) {
    mPendingDismiss = null;
    final ViewGroup.LayoutParams lp = pendingDismissData.rowContainer.container.getLayoutParams();
    final int originalHeight = pendingDismissData.rowContainer.container.getHeight();

    ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);

    animator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            if (mCallbacks.canDismiss(pendingDismissData.position))
                mCallbacks.onDismiss(mRecyclerView, pendingDismissData.position);
            pendingDismissData.rowContainer.dataContainer.post(new Runnable() {
                @Override
                public void run() {
                    pendingDismissData.rowContainer.dataContainer.setTranslationX(0);
                    pendingDismissData.rowContainer.dataContainer.setAlpha(1);
                    pendingDismissData.rowContainer.undoContainer.setVisibility(View.GONE);
                    pendingDismissData.rowContainer.undoContainer.setTranslationX(0);
                    pendingDismissData.rowContainer.undoContainer.setAlpha(1);

                    lp.height = originalHeight;
                    pendingDismissData.rowContainer.container.setLayoutParams(lp);
                }
            });
        }
    });

    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            lp.height = (Integer) valueAnimator.getAnimatedValue();
            pendingDismissData.rowContainer.container.setLayoutParams(lp);
        }
    });

    animator.start();
}

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM