/*
* Copyright 2013 Frankie Sardo
* Copyright 2013 Niek Haarman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.haarman.listviewanimations.itemmanipulation.contextualundo;
import static com.nineoldandroids.view.ViewHelper.setAlpha;
import static com.nineoldandroids.view.ViewHelper.setTranslationX;
import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.haarman.listviewanimations.BaseAdapterDecorator;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.view.ViewHelper;
/**
* Warning: a stable id for each item in the adapter is required. The decorated
* adapter should not try to cast convertView to a particular view. The
* undoLayout should have the same height as the content row.
* <p>
* Usage: <br>
* * Create a new instance of this class providing the {@link BaseAdapter} to wrap, the undo layout, and the undo button id, optionally a delay time millis, a count down TextView res id, and a delay in milliseconds before deleting the item .<br>
* * Call {@link #setDeleteItemCallback(DeleteItemCallback)} to be notified of when items should be removed from your collection.<br>
* * Set your {@link ListView} to this ContextualUndoAdapter, and set this ContextualUndoAdapter to your ListView.<br>
*/
public class ContextualUndoAdapter extends BaseAdapterDecorator implements ContextualUndoListViewTouchListener.Callback {
private static final int ANIMATION_DURATION = 150;
private static final String EXTRA_ACTIVE_REMOVED_ID = "removedId";
private final int mUndoLayoutId;
private final int mUndoActionId;
private final int mCountDownTextViewResId;
private final int mAutoDeleteDelayMillis;
private long mDismissStartMillis;
private ContextualUndoView mCurrentRemovedView;
private long mCurrentRemovedId;
private Map<View, Animator> mActiveAnimators = new ConcurrentHashMap<View, Animator>();
private Handler mHandler;
private CountDownRunnable mCountDownRunnable;
private DeleteItemCallback mDeleteItemCallback;
private CountDownFormatter mCountDownFormatter;
private ContextualUndoListViewTouchListener mContextualUndoListViewTouchListener;
/**
* Create a new ContextualUndoAdapter based on given parameters.
*
* @param baseAdapter The {@link BaseAdapter} to wrap
* @param undoLayoutId The layout resource id to show as undo
* @param undoActionId The id of the component which undoes the dismissal
* The layout resource id to show as undo
* @param undoActionId
* The id of the component which undoes the dismissal
*/
public ContextualUndoAdapter(BaseAdapter baseAdapter, int undoLayoutId, int undoActionId) {
this(baseAdapter, undoLayoutId, undoActionId, -1, -1, null);
}
/**
* Create a new ContextualUndoAdapter based on given parameters.
* Will automatically remove the swiped item after autoDeleteTimeMillis milliseconds.
*
* @param baseAdapter The {@link BaseAdapter} to wrap
* @param undoLayoutResId The layout resource id to show as undo
* @param undoActionResId The id of the component which undoes the dismissal
* @param autoDeleteTimeMillis The time in milliseconds that the adapter will wait for he user to hit undo before automatically deleting the item
*/
public ContextualUndoAdapter(BaseAdapter baseAdapter, int undoLayoutResId, int undoActionResId, int autoDeleteTimeMillis) {
this(baseAdapter, undoLayoutResId, undoActionResId, autoDeleteTimeMillis, -1, null);
}
/**
* Create a new ContextualUndoAdapter based on given parameters.
* Will automatically remove the swiped item after autoDeleteTimeMillis milliseconds.
*
* @param baseAdapter The {@link BaseAdapter} to wrap
* @param undoLayoutResId The layout resource id to show as undo
* @param undoActionResId The resource id of the component which undoes the dismissal
* @param autoDeleteTime The time in milliseconds that adapter will wait for user to hit undo before automatically deleting item
* @param countDownTextViewResId The resource id of the {@link TextView} in the undoLayoutResId that will show the time left
* @param countDownFormatter the {@link CountDownFormatter} which provides text to be shown in the {@link TextView} as specified by countDownTextViewResId
*/
public ContextualUndoAdapter(BaseAdapter baseAdapter, int undoLayoutResId, int undoActionResId, int autoDeleteTime, int countDownTextViewResId, CountDownFormatter countDownFormatter) {
super(baseAdapter);
mHandler = new Handler();
mCountDownRunnable = new CountDownRunnable();
mUndoLayoutId = undoLayoutResId;
mUndoActionId = undoActionResId;
mCurrentRemovedId = -1;
mAutoDeleteDelayMillis = autoDeleteTime;
mCountDownTextViewResId = countDownTextViewResId;
mCountDownFormatter = countDownFormatter;
}
@Override
public final View getView(int position, View convertView, ViewGroup parent) {
ContextualUndoView contextualUndoView = (ContextualUndoView) convertView;
if (contextualUndoView == null) {
contextualUndoView = new ContextualUndoView(parent.getContext(), mUndoLayoutId, mCountDownTextViewResId);
contextualUndoView.findViewById(mUndoActionId).setOnClickListener(new UndoListener(contextualUndoView));
}
View contentView = super.getView(position, contextualUndoView.getContentView(), contextualUndoView);
contextualUndoView.updateContentView(contentView);
long itemId = getItemId(position);
if (itemId == mCurrentRemovedId) {
contextualUndoView.displayUndo();
mCurrentRemovedView = contextualUndoView;
long millisLeft = mAutoDeleteDelayMillis - (System.currentTimeMillis() - mDismissStartMillis);
if (mCountDownFormatter != null) {
mCurrentRemovedView.updateCountDownTimer(mCountDownFormatter.getCountDownString(millisLeft));
}
} else {
contextualUndoView.displayContentView();
}
contextualUndoView.setItemId(itemId);
return contextualUndoView;
}
@Override
public void setAbsListView(AbsListView listView) {
super.setAbsListView(listView);
mContextualUndoListViewTouchListener = new ContextualUndoListViewTouchListener(listView, this);
mContextualUndoListViewTouchListener.setIsParentHorizontalScrollContainer(isParentHorizontalScrollContainer());
mContextualUndoListViewTouchListener.setTouchChild(getTouchChild());
listView.setOnTouchListener(mContextualUndoListViewTouchListener);
listView.setOnScrollListener(mContextualUndoListViewTouchListener.makeScrollListener());
listView.setRecyclerListener(new RecycleViewListener());
}
@Override
public void onViewSwiped(View dismissView, int dismissPosition) {
ContextualUndoView contextualUndoView = (ContextualUndoView) dismissView;
if (contextualUndoView.isContentDisplayed()) {
restoreViewPosition(contextualUndoView);
contextualUndoView.displayUndo();
removePreviousContextualUndoIfPresent();
setCurrentRemovedView(contextualUndoView);
if (mAutoDeleteDelayMillis > 0) {
startAutoDeleteTimer();
}
} else {
performRemovalIfNecessary();
}
}
private void startAutoDeleteTimer() {
mHandler.removeCallbacks(mCountDownRunnable);
if (mCountDownFormatter != null) {
mCurrentRemovedView.updateCountDownTimer(mCountDownFormatter.getCountDownString(mAutoDeleteDelayMillis));
}
mDismissStartMillis = System.currentTimeMillis();
mHandler.postDelayed(mCountDownRunnable, Math.min(1000, mAutoDeleteDelayMillis));
}
private void restoreViewPosition(View view) {
setAlpha(view, 1f);
setTranslationX(view, 0);
}
private void removePreviousContextualUndoIfPresent() {
if (mCurrentRemovedView != null) {
performRemovalIfNecessary();
}
}
private void setCurrentRemovedView(ContextualUndoView currentRemovedView) {
mCurrentRemovedView = currentRemovedView;
mCurrentRemovedId = currentRemovedView.getItemId();
}
private void clearCurrentRemovedView() {
mCurrentRemovedView = null;
mCurrentRemovedId = -1;
mHandler.removeCallbacks(mCountDownRunnable);
}
@Override
public void onListScrolled() {
performRemovalIfNecessary();
}
private void performRemovalIfNecessary() {
if (mCurrentRemovedView != null && mCurrentRemovedView.getParent() != null) {
ValueAnimator animator = ValueAnimator.ofInt(mCurrentRemovedView.getHeight(), 1).setDuration(ANIMATION_DURATION);
animator.addListener(new RemoveViewAnimatorListenerAdapter(mCurrentRemovedView));
animator.addUpdateListener(new RemoveViewAnimatorUpdateListener(mCurrentRemovedView));
animator.start();
mActiveAnimators.put(mCurrentRemovedView, animator);
clearCurrentRemovedView();
}
}
/**
* Set the DeleteItemCallback for this ContextualUndoAdapter. This is called when an item should be deleted from your collection.
*/
public void setDeleteItemCallback(DeleteItemCallback deleteItemCallback) {
mDeleteItemCallback = deleteItemCallback;
}
/**
* This method should be called in your {@link Activity}'s {@link Activity#onSaveInstanceState(Bundle)} to remember dismissed statuses.
* @param outState the {@link Bundle} provided by Activity.onSaveInstanceState(Bundle).
*/
public void onSaveInstanceState(Bundle outState) {
outState.putLong(EXTRA_ACTIVE_REMOVED_ID, mCurrentRemovedId);
}
/**
* This method should be called in your {@link Activity#onRestoreInstanceState(Bundle)} to remember dismissed statuses.
* @param savedInstanceState
*/
public void onRestoreInstanceState(Bundle savedInstanceState) {
mCurrentRemovedId = savedInstanceState.getLong(EXTRA_ACTIVE_REMOVED_ID, -1);
}
/**
* Animate the item at given position away and show the undo {@link View}.
* @param position the position.
*/
public void swipeViewAtPosition(int position) {
mCurrentRemovedId = getItemId(position);
for (int i = 0; i < getAbsListView().getChildCount(); i++) {
int positionForView = getAbsListView().getPositionForView(getAbsListView().getChildAt(i));
if (positionForView == position) {
swipeView(getAbsListView().getChildAt(i), positionForView);
}
}
}
private void swipeView(final View view, final int dismissPosition) {
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "x", view.getMeasuredWidth());
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
onViewSwiped(view, dismissPosition);
}
});
animator.start();
}
@Override
public void setIsParentHorizontalScrollContainer(boolean isParentHorizontalScrollContainer) {
super.setIsParentHorizontalScrollContainer(isParentHorizontalScrollContainer);
if (mContextualUndoListViewTouchListener != null) {
mContextualUndoListViewTouchListener.setIsParentHorizontalScrollContainer(isParentHorizontalScrollContainer);
}
}
@Override
public void setTouchChild(int childResId) {
super.setTouchChild(childResId);
if (mContextualUndoListViewTouchListener != null) {
mContextualUndoListViewTouchListener.setTouchChild(childResId);
}
}
/**
* A callback interface which is used to notify when items should be removed from the collection.
*/
public interface DeleteItemCallback {
/**
* Called when an item should be removed from the collection.
*
* @param position
* the position of the item that should be removed.
*/
public void deleteItem(int position);
}
/**
* A callback interface which is used to provide the text to display when counting down.
*/
public interface CountDownFormatter {
/**
* Called each tick of the CountDownTimer
* @param millisLeft time in milliseconds remaining before the item is automatically removed
*/
public String getCountDownString(final long millisLeft);
}
private class CountDownRunnable implements Runnable {
@Override
public void run() {
long millisRemaining = mAutoDeleteDelayMillis - (System.currentTimeMillis() - mDismissStartMillis);
if (mCountDownFormatter != null) {
mCurrentRemovedView.updateCountDownTimer(mCountDownFormatter.getCountDownString(millisRemaining));
}
if (millisRemaining <= 0) {
performRemovalIfNecessary();
} else {
mHandler.postDelayed(this, Math.min(millisRemaining, 1000));
}
}
}
private class RemoveViewAnimatorListenerAdapter extends AnimatorListenerAdapter {
private final View mDismissView;
private final int mOriginalHeight;
public RemoveViewAnimatorListenerAdapter(View dismissView) {
mDismissView = dismissView;
mOriginalHeight = dismissView.getHeight();
}
@Override
public void onAnimationEnd(Animator animation) {
mActiveAnimators.remove(mDismissView);
restoreViewPosition(mDismissView);
restoreViewDimension(mDismissView);
deleteCurrentItem();
}
private void restoreViewDimension(View view) {
ViewGroup.LayoutParams lp;
lp = view.getLayoutParams();
lp.height = mOriginalHeight;
view.setLayoutParams(lp);
}
private void deleteCurrentItem() {
int position = getAbsListView().getPositionForView(mDismissView);
if (getAbsListView() instanceof ListView) {
position -= ((ListView) getAbsListView()).getHeaderViewsCount();
}
mDeleteItemCallback.deleteItem(position);
}
}
private class RemoveViewAnimatorUpdateListener implements ValueAnimator.AnimatorUpdateListener {
private final View mDismissView;
private final ViewGroup.LayoutParams mLayoutParams;
public RemoveViewAnimatorUpdateListener(View dismissView) {
mDismissView = dismissView;
mLayoutParams = dismissView.getLayoutParams();
}
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mLayoutParams.height = (Integer) valueAnimator.getAnimatedValue();
mDismissView.setLayoutParams(mLayoutParams);
}
}
private class UndoListener implements View.OnClickListener {
private final ContextualUndoView mContextualUndoView;
public UndoListener(ContextualUndoView contextualUndoView) {
mContextualUndoView = contextualUndoView;
}
@Override
public void onClick(View v) {
clearCurrentRemovedView();
mContextualUndoView.displayContentView();
moveViewOffScreen();
animateViewComingBack();
}
private void moveViewOffScreen() {
ViewHelper.setTranslationX(mContextualUndoView, mContextualUndoView.getWidth());
}
private void animateViewComingBack() {
animate(mContextualUndoView).translationX(0).setDuration(ANIMATION_DURATION).setListener(null);
}
}
private class RecycleViewListener implements AbsListView.RecyclerListener {
@Override
public void onMovedToScrapHeap(View view) {
Animator animator = mActiveAnimators.get(view);
if (animator != null) {
animator.cancel();
}
}
}
}