Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save VladSumtsov/390e0bccf53e5fdbfc81 to your computer and use it in GitHub Desktop.
Save VladSumtsov/390e0bccf53e5fdbfc81 to your computer and use it in GitHub Desktop.
Roman Nurik's SwipeDismissListViewTouchListener with like in gmail functionality
// THIS IS A BETA! I DON'T RECOMMEND USING IT IN PRODUCTION CODE JUST YET
/*
* Copyright 2012 Roman Nurik
*
* 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.iamon.utils;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ListView;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.nineoldandroids.view.ViewHelper.setAlpha;
import static com.nineoldandroids.view.ViewHelper.setTranslationX;
import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
/**
* A {@link android.view.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/>
* <p>After creating the listener, the caller should also call
* {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
* in the scroll listener returned by {@link #makeScrollListener()}. 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/>
* <p>Example usage:</p>
* <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());
* listView.onPause() in activity on pause to reset pending undos
*/
public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
private static final long SHOW_UNDO_DURATION = 100;
private final int contentLayoutId;
// Cached ViewConfiguration and system-wide constant values
private int slop;
private int minFlingVelocity;
private int maxFlingVelocity;
private long animationTime;
// Fixed properties
private ListView listView;
private OnDismissCallback onDismissCallback;
private int viewWidth = 1; // 1 and not 0 to prevent dividing by zero
// Transient properties
private List<PendingDismissData> pendingDismisses = new ArrayList<PendingDismissData>();
private List<PendingDismissData> pendingUndoDismisses = new ArrayList<PendingDismissData>();
private int dismissAnimationRefCount = 0;
private float downX;
private boolean swiping;
private VelocityTracker velocityTracker;
private int downPosition;
private View downView;
private boolean paused;
private int undoLayoutId = -1;
private int undoButtonId = -1;
private View undoView;
private View contentView;
/**
* The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
* about a successful dismissal of one or more list item positions.
*/
public interface OnDismissCallback {
/**
* Called when the user has indicated they she would like to dismiss one or more list item
* positions.
*
* @param listView The originating {@link android.widget.ListView}.
* @param reverseSortedPositions An array of positions to dismiss, sorted in descending
*/
void onDismiss(ListView listView, List<Integer> 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 onDismissCallback The callback to trigger when the user has indicated that she would like to
*/
public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback onDismissCallback) {
this(listView, onDismissCallback, -1, -1, -1);
}
public void onPause() {
dismissPendingUndos();
}
/**
* Constructs a new swipe-to-dismiss touch listener for the given list view.
*
* @param listView The list view whose items should be dismissable.
* @param onDismissCallback The callback to trigger when the user has indicated that she would like to
* @param undoLayoutId the id of undo layout already created in list view row. Undo layout must be setVisibility(View.GONE);
* @param contentLayoutId the id of content layout already created in list view row
* @param undoButtonId The id of the undo button already created in list view row
*/
public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback onDismissCallback, int undoLayoutId, int contentLayoutId, int undoButtonId) {
ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
slop = vc.getScaledTouchSlop();
minFlingVelocity = vc.getScaledMinimumFlingVelocity();
maxFlingVelocity = vc.getScaledMaximumFlingVelocity();
animationTime = listView.getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
this.listView = listView;
this.onDismissCallback = onDismissCallback;
this.undoLayoutId = undoLayoutId;
this.undoButtonId = undoButtonId;
this.contentLayoutId = contentLayoutId;
}
/**
* 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) {
paused = !enabled;
}
/**
* Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
* {@link ListView} using
* {@link 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 SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
*
* @see {@link 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);
dismissPendingUndos();
}
@Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
}
};
}
private void dismissPendingUndos() {
if (!pendingUndoDismisses.isEmpty()) {
for (PendingDismissData pd : pendingUndoDismisses) {
performDismiss(pd.view, pd.position, null);
}
pendingUndoDismisses.clear();
}
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (viewWidth < 2) {
viewWidth = listView.getWidth();
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (paused) {
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 = listView.getChildCount();
int[] listViewCoords = new int[2];
listView.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 = listView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
downView = child;
break;
}
}
if (downView != null) {
downX = motionEvent.getRawX();
downPosition = listView.getPositionForView(downView);
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(motionEvent);
if (undoLayoutId > 0) {
undoView = downView.findViewById(undoLayoutId);
undoView.setClickable(true);
contentView = downView.findViewById(contentLayoutId);
} else {
contentView = downView;
}
}
view.onTouchEvent(motionEvent);
return true;
}
case MotionEvent.ACTION_UP: {
if (velocityTracker == null) {
break;
}
float deltaX = motionEvent.getRawX() - downX;
velocityTracker.addMovement(motionEvent);
velocityTracker.computeCurrentVelocity(1000);
float velocityX = Math.abs(velocityTracker.getXVelocity());
float velocityY = Math.abs(velocityTracker.getYVelocity());
boolean dismiss = false;
boolean dismissRight = false;
if (Math.abs(deltaX) > viewWidth / 2) {
dismiss = true;
dismissRight = deltaX > 0;
} else if (minFlingVelocity <= velocityX && velocityX <= maxFlingVelocity
&& velocityY < velocityX) {
dismiss = true;
dismissRight = velocityTracker.getXVelocity() > 0;
}
if (dismiss) {
// dismiss
final View contentView = this.contentView; // downView gets null'd before animation ends
final View downView = this.downView;
final View undoView = this.undoView; // downView gets null'd before animation ends
final int downPosition = this.downPosition;
++dismissAnimationRefCount;
animate(contentView)
.translationX(dismissRight ? viewWidth : -viewWidth)
.alpha(0)
.setDuration(animationTime)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (undoLayoutId == -1) {
performDismiss(downView, downPosition, null);
} else {
final PendingDismissData pendingDismiss = new PendingDismissData(downPosition, downView);
initUndoButton(pendingDismiss, undoView, contentView);
pendingUndoDismisses.add(pendingDismiss);
dismissMorePendingUndos(pendingDismiss, downPosition);
}
}
});
} else {
returnToDefault(contentView, 0);
}
velocityTracker = null;
downX = 0;
contentView = null;
downView = null;
downPosition = ListView.INVALID_POSITION;
swiping = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (velocityTracker == null || paused) {
break;
}
velocityTracker.addMovement(motionEvent);
float deltaX = motionEvent.getRawX() - downX;
if (Math.abs(deltaX) > slop) {
swiping = true;
listView.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));
listView.onTouchEvent(cancelEvent);
}
if (swiping) {
setTranslationX(contentView, deltaX);
setAlpha(contentView, Math.max(0f, Math.min(1f,
1f - 2f * Math.abs(deltaX) / viewWidth)));
return true;
}
break;
}
}
return false;
}
private void dismissMorePendingUndos(final PendingDismissData pendingDismiss, final int downPosition) {
if (pendingUndoDismisses.size() > 1) {
final PendingDismissData pd = pendingUndoDismisses.get(0);
pendingUndoDismisses.remove(pd);
performDismiss(pd.view, pd.position, new OnDismissCallback() {
@Override
public void onDismiss(ListView listView, List<Integer> reverseSortedPositions) {
if (downPosition > pd.position) {
moveUndoUp(pendingDismiss);
}
}
});
}
}
private void moveUndoUp(final PendingDismissData pd) {
int upPosition = pd.position - 1;
final View upView = listView.getChildAt(upPosition - listView.getFirstVisiblePosition());
final View view = pd.view;
float contentX = ViewHelper.getTranslationX(view.findViewById(contentLayoutId));
resetContentPresentation(pd);
pd.view = upView;
pd.position = upPosition;
showUndoPresentation(pd, contentX);
}
private void showUndoPresentation(PendingDismissData pendingDismiss, float contentX) {
View undoView = pendingDismiss.view.findViewById(undoLayoutId);
undoView.setVisibility(View.VISIBLE);
setAlpha(undoView, 1);
View contentView = pendingDismiss.view.findViewById(contentLayoutId);
setTranslationX(contentView, contentX);
}
private void initUndoButton(final PendingDismissData pendingDismiss, final View undoView, final View contentView) {
ObjectAnimator.ofFloat(undoView, "alpha", 0, 1).setDuration(SHOW_UNDO_DURATION).start();
undoView.setVisibility(View.VISIBLE);
undoView.findViewById(undoButtonId).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
returnToDefault(contentView, SHOW_UNDO_DURATION);
ObjectAnimator alpha = ObjectAnimator.ofFloat(undoView, "alpha", 1, 0);
alpha.setDuration(SHOW_UNDO_DURATION);
pendingUndoDismisses.remove(pendingDismiss);
alpha.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
undoView.setVisibility(View.GONE);
}
});
alpha.start();
}
});
}
private void returnToDefault(View view, long startDelay) {
animate(view)
.translationX(0)
.alpha(1)
.setStartDelay(startDelay)
.setDuration(animationTime)
.setListener(null);
}
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;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PendingDismissData that = (PendingDismissData) o;
if (view != null ? !view.equals(that.view) : that.view != null) return false;
return true;
}
}
private void performDismiss(final View dismissView, final int dismissPosition, final OnDismissCallback callback) {
// 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(animationTime);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
--dismissAnimationRefCount;
if (dismissAnimationRefCount == 0 || contentLayoutId > 0) {
// No active animations, process all pending dismisses.
// Sort by descending position
Collections.sort(pendingDismisses);
List<Integer> dismissPositions = new ArrayList<Integer>(pendingDismisses.size());
for (int i = pendingDismisses.size() - 1; i >= 0; i--) {
int pos = pendingDismisses.get(i).position;
if (pos < listView.getAdapter().getCount()) {
dismissPositions.add(pos);
}
}
onDismissCallback.onDismiss(listView, dismissPositions);
ViewGroup.LayoutParams lp;
for (PendingDismissData pendingDismiss : pendingDismisses) {
// Reset view presentation
resetContentPresentation(pendingDismiss);
lp = pendingDismiss.view.getLayoutParams();
lp.height = originalHeight;
pendingDismiss.view.setLayoutParams(lp);
}
pendingDismisses.clear();
if (callback != null) {
callback.onDismiss(listView, dismissPositions);
}
}
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(lp);
}
});
pendingDismisses.add(new PendingDismissData(dismissPosition, dismissView));
animator.start();
}
private void resetContentPresentation(PendingDismissData pendingDismiss) {
View contentView = pendingDismiss.view;
if (contentLayoutId > 0) {
contentView.findViewById(undoLayoutId).setVisibility(View.GONE);
contentView = contentView.findViewById(contentLayoutId);
}
setAlpha(contentView, 1f);
setTranslationX(contentView, 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment