Created
August 17, 2014 15:21
-
-
Save huteri/25b7813e543a6b9ad283 to your computer and use it in GitHub Desktop.
Gist for Cardslib
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* ****************************************************************************** | |
* Copyright (c) 2013 Roman Nurik, 2013-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.view.listener; | |
import android.animation.Animator; | |
import android.animation.AnimatorListenerAdapter; | |
import android.animation.ValueAnimator; | |
import android.graphics.Rect; | |
import android.os.SystemClock; | |
import android.view.MotionEvent; | |
import android.view.VelocityTracker; | |
import android.view.View; | |
import android.view.ViewConfiguration; | |
import android.view.ViewGroup; | |
import android.view.ViewPropertyAnimator; | |
import android.widget.AbsListView; | |
import android.widget.ListView; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.List; | |
import it.gmariotti.cardslib.library.R; | |
import it.gmariotti.cardslib.library.internal.Card; | |
/** | |
* It is based on Roman Nurik code. See this link for original code | |
* https://github.com/romannurik/Android-SwipeToDismiss </p> It provides a | |
* SwipeDismissViewTouchListener for a CardList. </p> | |
* | |
* A {@link View.OnTouchListener} that makes the list items in a | |
* {@link ListView} dismissable. {@link ListView} is given special treatment | |
* because by default it handles touches for its list items... i.e. it's in | |
* charge of drawing the pressed state (the list selector), handling list item | |
* clicks, etc. | |
* | |
* <p> | |
* After creating the listener, the caller should also call | |
* {@link ListView#setOnScrollListener(AbsListView.OnScrollListener)}, using a | |
* {@link it.gmariotti.cardslib.library.view.listener.SwipeOnScrollListener}. | |
* | |
* If a scroll listener is already assigned, the caller should still pass scroll | |
* changes through to this listener. This will ensure that this | |
* {@link SwipeDismissListViewTouchListener} is paused during list view | |
* scrolling. | |
* </p> | |
* | |
* <p> | |
* Example usage: | |
* </p> | |
* | |
* <pre> | |
* SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener( | |
* listView, new SwipeDismissListViewTouchListener.OnDismissCallback() { | |
* public void onDismiss(ListView listView, | |
* int[] reverseSortedPositions) { | |
* for (int position : reverseSortedPositions) { | |
* adapter.remove(adapter.getItem(position)); | |
* } | |
* adapter.notifyDataSetChanged(); | |
* } | |
* }); | |
* listView.setOnTouchListener(touchListener); | |
* listView.setOnScrollListener(touchListener.makeScrollListener()); | |
* </pre> | |
* | |
* <p> | |
* This class Requires API level 12 or later due to use of | |
* {@link ViewPropertyAnimator}. | |
* </p> | |
* | |
*/ | |
public class SwipeDismissListViewTouchListener implements View.OnTouchListener { | |
// Cached ViewConfiguration and system-wide constant values | |
private int mSlop; | |
private int mMinFlingVelocity; | |
private int mMaxFlingVelocity; | |
private long mAnimationTime; | |
// Fixed properties | |
private ListView mListView; | |
private DismissCallbacks mCallbacks; | |
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero | |
// Transient properties | |
private List<PendingDismissData> mPendingDismisses = new ArrayList<PendingDismissData>(); | |
private int mDismissAnimationRefCount = 0; | |
private float mDownX; | |
private float mDownY; | |
private boolean mSwiping; | |
private int mSwipingSlop; | |
private VelocityTracker mVelocityTracker; | |
private int mDownPosition; | |
private View mDownView; | |
private boolean mPaused; | |
private int mSwipeDirection = SwipeDirection.BOTH; | |
private int swipeDistanceDivisor = 2; | |
/** | |
* 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); | |
} | |
/** | |
* Constructs a new swipe-to-dismiss touch listener for the given list view. | |
* | |
* @param listView | |
* 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 SwipeDismissListViewTouchListener(ListView listView, | |
DismissCallbacks callbacks) { | |
ViewConfiguration vc = ViewConfiguration.get(listView.getContext()); | |
mSlop = vc.getScaledTouchSlop(); | |
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16; | |
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); | |
mAnimationTime = listView.getContext().getResources() | |
.getInteger(android.R.integer.config_shortAnimTime); | |
mListView = listView; | |
mCallbacks = callbacks; | |
swipeDistanceDivisor = listView.getContext().getResources() | |
.getInteger(R.integer.list_card_swipe_distance_divisor); | |
} | |
/** | |
* 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; | |
} | |
/** | |
* Returns an {@link AbsListView.OnScrollListener} to be added to the | |
* {@link ListView} using | |
* {@link ListView#setOnScrollListener(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 SwipeDismissListViewTouchListener} is paused during list view | |
* scrolling.</p> | |
* | |
* @see SwipeDismissListViewTouchListener | |
*/ | |
/* | |
* public AbsListView.OnScrollListener makeScrollListener() { return new | |
* AbsListView.OnScrollListener() { | |
* | |
* @Override public void onScrollStateChanged(AbsListView absListView, int | |
* scrollState) { 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 = mListView.getWidth(); | |
} | |
switch (motionEvent.getActionMasked()) { | |
case MotionEvent.ACTION_DOWN: { | |
if (mPaused) { | |
return false; | |
} | |
if (mSwiping) { | |
return true; | |
} | |
// 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 = mListView.getChildCount(); | |
int headerCount = mListView.getHeaderViewsCount(); | |
int footerCount = mListView.getFooterViewsCount(); | |
int[] listViewCoords = new int[2]; | |
mListView.getLocationOnScreen(listViewCoords); | |
int x = (int) motionEvent.getRawX() - listViewCoords[0]; | |
int y = (int) motionEvent.getRawY() - listViewCoords[1]; | |
View child = null; | |
for (int i = headerCount; i < (childCount - footerCount); i++) { | |
child = mListView.getChildAt(i); | |
child.getHitRect(rect); | |
if (rect.contains(x, y)) { | |
mDownView = child; | |
break; | |
} | |
} | |
if (mDownView != null) { | |
mDownX = motionEvent.getRawX(); | |
mDownY = motionEvent.getRawY(); | |
mDownPosition = mListView.getPositionForView(mDownView); | |
if (mListView.getAdapter().getItem(mDownPosition) instanceof Card) { | |
if (mCallbacks.canDismiss(mDownPosition, (Card) mListView | |
.getAdapter().getItem(mDownPosition))) { | |
mVelocityTracker = VelocityTracker.obtain(); | |
mVelocityTracker.addMovement(motionEvent); | |
} else { | |
mDownView = null; | |
} | |
} else { | |
mDownView = null; | |
} | |
} | |
view.onTouchEvent(motionEvent); | |
return true; | |
// return false; | |
} | |
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 / swipeDistanceDivisor | |
&& 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 | |
dismiss(mDownView, | |
mDownPosition - mListView.getHeaderViewsCount(), | |
dismissRight); | |
} else { | |
// cancel | |
mDownView.animate().translationX(0).alpha(1) | |
.setDuration(mAnimationTime).setListener(null); | |
} | |
mVelocityTracker.recycle(); | |
mVelocityTracker = null; | |
mDownX = 0; | |
mDownY = 0; | |
mDownView = null; | |
mDownPosition = ListView.INVALID_POSITION; | |
if (mSwiping) { | |
// To prevent onClick event with a fast swipe | |
mSwiping = false; | |
return true; | |
} | |
mSwiping = false; | |
break; | |
} | |
case MotionEvent.ACTION_CANCEL: { | |
if (mVelocityTracker == null) { | |
break; | |
} | |
if (mDownView != null) { | |
// cancel | |
mDownView.animate().translationX(0).alpha(1) | |
.setDuration(mAnimationTime).setListener(null); | |
} | |
mVelocityTracker.recycle(); | |
mVelocityTracker = null; | |
mDownX = 0; | |
mDownY = 0; | |
mDownView = 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; | |
float condDeltaX = deltaX; | |
switch (getSwipeDirection()) { | |
case SwipeDirection.LEFT: | |
condDeltaX = deltaX * -1; | |
break; | |
case SwipeDirection.RIGHT: | |
condDeltaX = deltaX * 1; | |
break; | |
case SwipeDirection.BOTH: | |
condDeltaX = Math.abs(deltaX); | |
break; | |
} | |
if (condDeltaX > mSlop | |
&& Math.abs(deltaY) < Math.abs(deltaX) / 2) { | |
mSwiping = true; | |
mSwipingSlop = (deltaX > 0 ? mSlop : -mSlop); | |
mListView.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)); | |
mListView.onTouchEvent(cancelEvent); | |
view.onTouchEvent(cancelEvent); | |
cancelEvent.recycle(); | |
} | |
if (mSwiping) { | |
mDownView.setTranslationX(deltaX - mSwipingSlop); | |
mDownView.setAlpha(Math.max(0f, | |
Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth))); | |
return true; | |
} | |
break; | |
} | |
} | |
return false; | |
} | |
private void dismiss(final View view, final int position, | |
boolean dismissRight) { | |
++mDismissAnimationRefCount; | |
if (view == null) { | |
// No view, shortcut to calling onDismiss to let it deal with | |
// adapter | |
// updates and all that. | |
mCallbacks.onDismiss(mListView, new int[] { position }); | |
return; | |
} | |
view.animate().translationX(dismissRight ? mViewWidth : -mViewWidth) | |
.alpha(0).setDuration(mAnimationTime) | |
.setListener(new AnimatorListenerAdapter() { | |
@Override | |
public void onAnimationEnd(Animator animation) { | |
performDismiss(view, position); | |
} | |
}); | |
} | |
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; | |
} | |
} | |
private void performDismiss(final View dismissView, | |
final 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. | |
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(mListView, 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); | |
mListView.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(); | |
} | |
public int getSwipeDirection() { | |
return mSwipeDirection; | |
} | |
public void setSwipeDirection(int mSwipeDirection) { | |
this.mSwipeDirection = mSwipeDirection; | |
} | |
public class SwipeDirection { | |
public static final int LEFT = 1; | |
public static final int RIGHT = 2; | |
public static final int BOTH = 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment