/*
* 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.nhaarman.listviewanimations.itemmanipulation.swipedismiss.contextualundo;
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.nhaarman.listviewanimations.BaseAdapterDecorator;
import com.nhaarman.listviewanimations.util.AdapterViewUtil;
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;
import static com.nineoldandroids.view.ViewHelper.setAlpha;
import static com.nineoldandroids.view.ViewHelper.setTranslationX;
import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
/**
* 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, the undo button id and a {@link DeleteItemCallback}, optionally a delay time millis,
* a count down TextView res id,
* , a delay in milliseconds before deleting the item.<br>
* * Set your {@link ListView} to this ContextualUndoAdapter, and set this ContextualUndoAdapter to your ListView.<br>
*/
@SuppressWarnings("UnusedDeclaration")
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 static final String X = "x";
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 final Handler mHandler;
private final CountDownRunnable mCountDownRunnable;
private final DeleteItemCallback mDeleteItemCallback;
private final 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
* @param deleteItemCallback The {@link DeleteItemCallback} which is called when an item should be deleted from your collection.
*/
public ContextualUndoAdapter(final BaseAdapter baseAdapter, final int undoLayoutId, final int undoActionId, final DeleteItemCallback deleteItemCallback) {
this(baseAdapter, undoLayoutId, undoActionId, -1, -1, deleteItemCallback, 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
* @param deleteItemCallback The {@link DeleteItemCallback} which is called when an item should be deleted from your collection.
*/
public ContextualUndoAdapter(final BaseAdapter baseAdapter, final int undoLayoutResId, final int undoActionResId, final int autoDeleteTimeMillis, final DeleteItemCallback deleteItemCallback) {
this(baseAdapter, undoLayoutResId, undoActionResId, autoDeleteTimeMillis, -1, deleteItemCallback, 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 deleteItemCallback The {@link DeleteItemCallback} which is called when an item should be deleted from your collection.
* @param countDownFormatter The {@link CountDownFormatter} which provides text to be shown in the {@link TextView} as specified by countDownTextViewResId
*/
public ContextualUndoAdapter(final BaseAdapter baseAdapter, final int undoLayoutResId, final int undoActionResId, final int autoDeleteTime, final int countDownTextViewResId,
final DeleteItemCallback deleteItemCallback, final CountDownFormatter countDownFormatter) {
super(baseAdapter);
mHandler = new Handler();
mCountDownRunnable = new CountDownRunnable();
mUndoLayoutId = undoLayoutResId;
mUndoActionId = undoActionResId;
mCurrentRemovedId = -1;
mAutoDeleteDelayMillis = autoDeleteTime;
mCountDownTextViewResId = countDownTextViewResId;
mDeleteItemCallback = deleteItemCallback;
mCountDownFormatter = countDownFormatter;
}
@Override
public final View getView(final int position, final View convertView, final ViewGroup parent) {
final ViewHolder vh;
ContextualUndoView contextualUndoView = (ContextualUndoView) convertView;
if (contextualUndoView == null) {
contextualUndoView = new ContextualUndoView(parent.getContext(), mUndoLayoutId, mCountDownTextViewResId);
contextualUndoView.findViewById(mUndoActionId).setOnClickListener(new UndoListener(contextualUndoView));
vh = new ViewHolder(contextualUndoView);
} else {
vh = ViewHolder.getViewHolder(contextualUndoView);
}
View contentView = super.getView(position, contextualUndoView.getContentView(), contextualUndoView);
contextualUndoView.updateContentView(contentView);
long itemId = getItemId(position);
vh.mItemId = itemId;
if (itemId == mCurrentRemovedId) {
contextualUndoView.displayUndo();
long millisLeft = mAutoDeleteDelayMillis - (System.currentTimeMillis() - mDismissStartMillis);
if (mCountDownFormatter != null) {
contextualUndoView.updateCountDownTimer(mCountDownFormatter.getCountDownString(millisLeft));
}
} else {
contextualUndoView.displayContentView();
}
contextualUndoView.setItemId(itemId);
return contextualUndoView;
}
@Override
public void setAbsListView(final AbsListView listView) {
super.setAbsListView(listView);
mContextualUndoListViewTouchListener = new ContextualUndoListViewTouchListener(listView, this);
mContextualUndoListViewTouchListener.setIsParentHorizontalScrollContainer(isParentHorizontalScrollContainer());
mContextualUndoListViewTouchListener.setTouchChild(getTouchChild());
listView.setOnTouchListener(mContextualUndoListViewTouchListener);
listView.setOnScrollListener(mContextualUndoListViewTouchListener.makeScrollListener());
listView.setOnHierarchyChangeListener(new HierarchyChangeListener());
}
@Override
public void onViewSwiped(final long dismissViewItemId, final int dismissPosition) {
ContextualUndoView contextualUndoView = getContextualUndoView(dismissViewItemId);
if (contextualUndoView == null) {
removePreviousContextualUndoIfPresent();
mCurrentRemovedView = null;
mCurrentRemovedId = dismissViewItemId;
} else if (contextualUndoView.isContentDisplayed()) {
restoreViewPosition(contextualUndoView);
contextualUndoView.displayUndo();
removePreviousContextualUndoIfPresent();
setCurrentRemovedView(contextualUndoView);
if (mAutoDeleteDelayMillis > 0) {
startAutoDeleteTimer();
}
} else {
performRemovalIfNecessary();
}
}
private ContextualUndoView getContextualUndoView(final long dismissViewItemId) {
ContextualUndoView contextualUndoView = null;
AbsListView listView = getAbsListView();
int childCount = listView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = listView.getChildAt(i);
if (child instanceof ContextualUndoView) {
ContextualUndoView listItem = (ContextualUndoView) child;
if (listItem.getItemId() == dismissViewItemId) {
contextualUndoView = listItem;
}
}
}
return contextualUndoView;
}
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(final View view) {
setAlpha(view, 1f);
setTranslationX(view, 0);
}
private void removePreviousContextualUndoIfPresent() {
if (mCurrentRemovedView != null) {
performRemovalIfNecessary();
}
}
private void setCurrentRemovedView(final 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 (mCurrentRemovedId == -1) {
return;
}
ContextualUndoView currentRemovedView = getCurrentRemovedView(mCurrentRemovedView, mCurrentRemovedId);
if (currentRemovedView != null) {
ValueAnimator animator = ValueAnimator.ofInt(currentRemovedView.getHeight(), 1).setDuration(ANIMATION_DURATION);
RemoveViewAnimatorListenerAdapter listener = new RemoveViewAnimatorListenerAdapter(currentRemovedView, mCurrentRemovedId);
RemoveViewAnimatorUpdateListener updateListener = new RemoveViewAnimatorUpdateListener(listener);
animator.addListener(listener);
animator.addUpdateListener(updateListener);
animator.start();
} else {
// The hard way.
deleteItemGivenId(mCurrentRemovedId);
}
clearCurrentRemovedView();
}
private void deleteItemGivenId(final long deleteItemId) {
int position = -1;
int numItems = getCount();
for (int i = 0; i < numItems; i++) {
long itemId = getItemId(i);
if (itemId == deleteItemId) {
position = i;
break;
}
}
if (position >= 0) {
mDeleteItemCallback.deleteItem(position);
}
}
private ContextualUndoView getCurrentRemovedView(final ContextualUndoView currentRemovedView, final long itemId) {
ContextualUndoView result = currentRemovedView;
if (result == null ||
result.getParent() == null ||
result.getItemId() != itemId ||
AdapterViewUtil.getPositionForView(getAbsListView(), result) < 0) {
result = getContextualUndoView(itemId);
}
return result;
}
/**
* 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(final Bundle outState) {
outState.putLong(EXTRA_ACTIVE_REMOVED_ID, mCurrentRemovedId);
}
/**
* This method should be called in your {@link Activity#onRestoreInstanceState(Bundle)} to remember dismissed statuses.
*/
public void onRestoreInstanceState(final Bundle savedInstanceState) {
if (savedInstanceState != null) {
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(final int position) {
mCurrentRemovedId = getItemId(position);
for (int i = 0; i < getAbsListView().getChildCount(); i++) {
AbsListView absListView = getAbsListView();
View childView = absListView.getChildAt(i);
int positionForView = AdapterViewUtil.getPositionForView(absListView, childView);
if (positionForView == position) {
swipeView(childView, 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(final Animator animator) {
onViewSwiped(((ContextualUndoView) view).getItemId(), dismissPosition);
}
});
animator.start();
}
@Override
public void setIsParentHorizontalScrollContainer(final boolean isParentHorizontalScrollContainer) {
super.setIsParentHorizontalScrollContainer(isParentHorizontalScrollContainer);
if (mContextualUndoListViewTouchListener != null) {
mContextualUndoListViewTouchListener.setIsParentHorizontalScrollContainer(isParentHorizontalScrollContainer);
}
}
@Override
public void setTouchChild(final int childResId) {
super.setTouchChild(childResId);
if (mContextualUndoListViewTouchListener != null) {
mContextualUndoListViewTouchListener.setTouchChild(childResId);
}
}
/**
* Removes any item that was swiped away.
* @param animate If true, animates the removal (collapsing the item).
* If false, removes item immediately without animation.
* @deprecated use {@link #removePendingItem()} or {@link #animateRemovePendingItem()} instead.
*/
@Deprecated
public void removePendingItem(final boolean animate) {
if (animate) {
animateRemovePendingItem();
} else {
removePendingItem();
}
}
/**
* Cancels the count down, and removes any item that was swiped away, without animating. Will cause {@link DeleteItemCallback#deleteItem(int)} to be called.
*/
public void removePendingItem() {
if (mCurrentRemovedView != null || mCurrentRemovedId >= 0) {
new RemoveViewAnimatorListenerAdapter(mCurrentRemovedView, mCurrentRemovedId).onAnimationEnd(null);
clearCurrentRemovedView();
}
}
/**
* Removes any item that was swiped away, animating the removal (collapsing the item). {@link DeleteItemCallback#deleteItem(int)} to be called.
*/
public void animateRemovePendingItem() {
removePreviousContextualUndoIfPresent();
}
/**
* Cancel the count down. This will not cause the {@link DeleteItemCallback#deleteItem(int)} to be called. Use {@link #removePendingItem()} for that instead.
*/
public void cancelCountDown() {
mHandler.removeCallbacks(mCountDownRunnable);
}
/**
* 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 ContextualUndoView mDismissView;
private final long mDismissViewId;
private final int mOriginalHeight;
public RemoveViewAnimatorListenerAdapter(final ContextualUndoView dismissView, final long dismissViewId) {
mDismissView = dismissView;
mDismissViewId = dismissViewId;
mOriginalHeight = dismissView.getHeight();
}
@Override
public void onAnimationEnd(final Animator animation) {
mDismissView = getViewBeingAnimated();
if (mDismissView == null) {
deleteItemGivenId(mDismissViewId);
return;
}
restoreViewPosition(mDismissView);
restoreViewDimension(mDismissView);
deleteCurrentItem(mDismissView);
}
private void restoreViewDimension(final View view) {
ViewGroup.LayoutParams lp;
lp = view.getLayoutParams();
lp.height = mOriginalHeight;
view.setLayoutParams(lp);
}
private void deleteCurrentItem(final View view) {
int position = AdapterViewUtil.getPositionForView(getAbsListView(), view);
mDeleteItemCallback.deleteItem(position);
}
private ContextualUndoView getViewBeingAnimated() {
ContextualUndoView newDismissView = getCurrentRemovedView(mDismissView, mDismissViewId);
if (newDismissView != mDismissView) {
restoreViewPosition(mDismissView);
restoreViewDimension(mDismissView);
mDismissView = newDismissView;
}
return mDismissView;
}
}
private class RemoveViewAnimatorUpdateListener implements ValueAnimator.AnimatorUpdateListener {
final RemoveViewAnimatorListenerAdapter mParentAdapter;
private final ViewGroup.LayoutParams mLayoutParams;
public RemoveViewAnimatorUpdateListener(final RemoveViewAnimatorListenerAdapter parentAdapter) {
mParentAdapter = parentAdapter;
mLayoutParams = parentAdapter.mDismissView.getLayoutParams();
}
@Override
public void onAnimationUpdate(final ValueAnimator valueAnimator) {
ContextualUndoView dismissView = mParentAdapter.getViewBeingAnimated();
if (dismissView != null) {
mLayoutParams.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(mLayoutParams);
}
}
}
private class UndoListener implements View.OnClickListener {
private final ContextualUndoView mContextualUndoView;
public UndoListener(final ContextualUndoView contextualUndoView) {
mContextualUndoView = contextualUndoView;
}
@Override
public void onClick(final 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 HierarchyChangeListener implements ViewGroup.OnHierarchyChangeListener {
@Override
public void onChildViewAdded(final View parent, final View child) {
final ViewHolder vh = ViewHolder.getViewHolder(child);
if (vh != null && mCurrentRemovedId > 0 && vh.mItemId == mCurrentRemovedId) {
mCurrentRemovedView = (ContextualUndoView) child;
}
}
@Override
public void onChildViewRemoved(final View parent, final View child) {
final ViewHolder vh = ViewHolder.getViewHolder(child);
if (vh != null && mCurrentRemovedId > 0 && vh.mItemId == mCurrentRemovedId) {
mCurrentRemovedView = null;
}
}
}
private static class ViewHolder {
final ContextualUndoView mContextualUndoView;
long mItemId;
static ViewHolder getViewHolder(final View view) {
return (ViewHolder) view.getTag();
}
ViewHolder(final ContextualUndoView contextualUndoView) {
mContextualUndoView = contextualUndoView;
mContextualUndoView.setTag(this);
}
}
}