/*
* ******************************************************************************
* Copyright (c) 2014 Gabriele Mariotti.
*
* 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 it.gmariotti.cardslib.library.internal.dismissanimation;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import it.gmariotti.cardslib.library.internal.Card;
import it.gmariotti.cardslib.library.internal.CardArrayAdapter;
import it.gmariotti.cardslib.library.internal.base.BaseCardArrayAdapter;
import it.gmariotti.cardslib.library.view.CardListView;
import it.gmariotti.cardslib.library.view.base.CardViewWrapper;
import it.gmariotti.cardslib.library.view.listener.SwipeDismissListViewTouchListener;
/**
* @author Gabriele Mariotti (gabri.mariotti@gmail.com)
*/
public abstract class BaseDismissAnimation {
protected BaseCardArrayAdapter mBaseAdapter;
protected CardListView mCardListView;
protected int mListWidth;
protected int mAnimationTime = 200;
protected boolean mDismissRight = true;
protected Context mContext;
private List<PendingDismissData> mPendingDismisses = new ArrayList<PendingDismissData>();
private int mDownPosition;
private int mDismissAnimationRefCount = 0;
//--------------------------------------------------------------------------
// Constructor
//--------------------------------------------------------------------------
public BaseDismissAnimation(Context context) {
mContext = context;
}
//--------------------------------------------------------------------------
// Init
//--------------------------------------------------------------------------
public BaseDismissAnimation setup(BaseCardArrayAdapter adapter) {
mBaseAdapter = adapter;
return this;
}
//--------------------------------------------------------------------------
// Callbacks
//--------------------------------------------------------------------------
/**
* The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
* about a successful dismissal of one or more list item positions.
*/
public interface DismissCallbacks {
/**
* Called to determine whether the given position can be dismissed.
*/
boolean canDismiss(int position,Card card);
/**
* Called when the user has indicated they she would like to dismiss one or more list item
* positions.
*
* @param listView The originating {@link ListView}.
* @param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onDismiss(ListView listView, int[] reverseSortedPositions);
}
public void animateDismissPosition(int position) {
if (position > AbsListView.INVALID_POSITION)
animateDismissPosition(Arrays.asList(position));
}
public void animateDismissPosition(final Collection<Integer> positions) {
if (mBaseAdapter == null) {
throw new IllegalStateException("Call setup method before animate!");
}
prepareAnimation();
final List<Integer> positionsCopy = new ArrayList<Integer>(positions);
List<CardViewWrapper> views = getVisibleViewsForPositions(positionsCopy);
for (CardViewWrapper cardView:views){
dismissiCardWithAnimation(cardView);
}
}
public void animateDismiss(Card card) {
if (card!=null)
animateDismiss(Arrays.asList(card));
}
public void animateDismiss(Collection<Card> cards) {
if (mBaseAdapter == null) {
throw new IllegalStateException("Call setup method before animate!");
}
prepareAnimation();
final List<Card> cardsCopy = new ArrayList<Card>(cards);
List<CardViewWrapper> views = getVisibleViewsForCards(cardsCopy);
for (CardViewWrapper cardView:views){
dismissiCardWithAnimation(cardView);
}
}
/**
* Prepare the animation
*/
private void prepareAnimation() {
setupCardListView();
if (mCardListView != null) {
mListWidth = mCardListView.getWidth();
}
}
public abstract void animate(Card card, CardViewWrapper cardView);//, Card.OnDismissAnimationListener onDismissAnimationListener);
/**
* Retrieves the cardListView
*/
private void setupCardListView() {
if (mBaseAdapter != null) {
if (mBaseAdapter instanceof CardArrayAdapter) {
mCardListView = ((CardArrayAdapter) mBaseAdapter).getCardListView();
}
}
if (mCardListView == null) {
throw new IllegalStateException("BaseDismissAnimation works with a CardListView");
}
}
/**
* Returns the visible view for the positions
*
* @param positions
* @return
*/
private List<CardViewWrapper> getVisibleViewsForPositions(final Collection<Integer> positions) {
List<CardViewWrapper> views = new ArrayList<CardViewWrapper>();
for (int i = 0; i < mCardListView.getChildCount(); i++) {
View child = mCardListView.getChildAt(i);
if (positions.contains(mCardListView.getPositionForView(child))) {
views.add((CardViewWrapper) child);
}
}
return views;
}
/**
* Returns the visible view for the cards
*
* @param cardsCopy
* @return
*/
private List<CardViewWrapper> getVisibleViewsForCards(List<Card> cardsCopy) {
List<CardViewWrapper> originalViews = new ArrayList<CardViewWrapper>();
for (Card card:cardsCopy){
originalViews.add(card.getCardView());
}
/*List<CardView> views = new ArrayList<CardView>();
for (int i = 0; i < mCardListView.getChildCount(); i++) {
View child = mCardListView.getChildAt(i);
if (cardsCopy.contains(views)){
views.add((CardView) child);
}
}*/
return originalViews;
}
private void dismissiCardWithAnimation(final CardViewWrapper cardView) {
++mDismissAnimationRefCount;
int mDownPosition = mCardListView.getPositionForView((View)cardView);
animate(cardView.getCard(),cardView);
}
protected void invokeCallbak(final View dismissView) {
// 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.
final int dismissPosition= mBaseAdapter.getPosition(((CardViewWrapper) dismissView).getCard());
final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
final int originalHeight = dismissView.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
--mDismissAnimationRefCount;
if (mDismissAnimationRefCount == 0) {
// No active animations, process all pending dismisses.
// Sort by descending position
Collections.sort(mPendingDismisses);
int[] dismissPositions = new int[mPendingDismisses.size()];
for (int i = mPendingDismisses.size() - 1; i >= 0; i--) {
dismissPositions[i] = mPendingDismisses.get(i).position;
}
mCallbacks.onDismiss(mCardListView, dismissPositions);
// Reset mDownPosition to avoid MotionEvent.ACTION_UP trying to start a dismiss
// animation with a stale position
mDownPosition = ListView.INVALID_POSITION;
ViewGroup.LayoutParams lp;
for (PendingDismissData pendingDismiss : mPendingDismisses) {
// Reset view presentation
pendingDismiss.view.setAlpha(1f);
pendingDismiss.view.setTranslationX(0);
lp = pendingDismiss.view.getLayoutParams();
lp.height = 0;
pendingDismiss.view.setLayoutParams(lp);
}
// Send a cancel event
long time = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(time, time,
MotionEvent.ACTION_CANCEL, 0, 0, 0);
mCardListView.dispatchTouchEvent(cancelEvent);
mPendingDismisses.clear();
}
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(lp);
}
});
mPendingDismisses.add(new PendingDismissData(dismissPosition, dismissView));
animator.start();
}
class PendingDismissData implements Comparable<PendingDismissData> {
public int position;
public View view;
public PendingDismissData(int position, View view) {
this.position = position;
this.view = view;
}
@Override
public int compareTo(PendingDismissData other) {
// Sort by descending position
return other.position - position;
}
}
DismissCallbacks mCallbacks = new DismissCallbacks() {
@Override
public boolean canDismiss(int position, Card card) {
return card.isSwipeable();
}
@Override
public void onDismiss(ListView listView, int[] reverseSortedPositions) {
int[] itemPositions=new int[reverseSortedPositions.length];
String[] itemIds=new String[reverseSortedPositions.length];
int i=0;
//Remove cards and notifyDataSetChanged
for (int position : reverseSortedPositions) {
Card card = mBaseAdapter.getItem(position);
if (card!=null){
itemPositions[i]=position;
itemIds[i]=card.getId();
i++;
mBaseAdapter.remove(card);
//TODO CHANGE
if (card.getOnSwipeListener() != null){
card.getOnSwipeListener().onSwipe(card);
}
}
}
mBaseAdapter.notifyDataSetChanged();
}
};
private Animator createAnimatorForView(final View view) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
final int originalHeight = view.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 0);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animator) {
lp.height = 0;
view.setLayoutParams(lp);
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(final ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
view.setLayoutParams(lp);
}
});
return animator;
}
//--------------------------------------------------------------------------
// Getters and setters
//--------------------------------------------------------------------------
public boolean isDismissRight() {
return mDismissRight;
}
public void setDismissRight(boolean dismissRight) {
mDismissRight = dismissRight;
}
public int getAnimationTime() {
return mAnimationTime;
}
}