Skip to content

Instantly share code, notes, and snippets.

@romannurik
Last active May 1, 2021 10:16

Revisions

  1. romannurik revised this gist Aug 15, 2013. 4 changed files with 2 additions and 771 deletions.
    108 changes: 0 additions & 108 deletions MainActivity.java
    Original file line number Diff line number Diff line change
    @@ -1,108 +0,0 @@
    // 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.example.android.swipedismiss;

    import android.app.ListActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.Button;
    import android.widget.ListView;
    import android.widget.Toast;

    import java.util.ArrayList;
    import java.util.Arrays;

    public class MainActivity extends ListActivity {
    ArrayAdapter<String> mAdapter;

    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Set up ListView example
    String[] items = new String[20];
    for (int i = 0; i < items.length; i++) {
    items[i] = "Item " + (i + 1);
    }

    mAdapter = new ArrayAdapter<String>(this,
    android.R.layout.simple_list_item_1,
    android.R.id.text1,
    new ArrayList<String>(Arrays.asList(items)));
    setListAdapter(mAdapter);

    ListView listView = getListView();
    // Create a ListView-specific touch listener. ListViews are given special treatment because
    // by default they handle touches for their list items... i.e. they're in charge of drawing
    // the pressed state (the list selector), handling list item clicks, etc.
    SwipeDismissListViewTouchListener touchListener =
    new SwipeDismissListViewTouchListener(
    listView,
    new SwipeDismissListViewTouchListener.OnDismissCallback() {
    @Override
    public void onDismiss(ListView listView, int[] reverseSortedPositions) {
    for (int position : reverseSortedPositions) {
    mAdapter.remove(mAdapter.getItem(position));
    }
    mAdapter.notifyDataSetChanged();
    }
    });
    listView.setOnTouchListener(touchListener);
    // Setting this scroll listener is required to ensure that during ListView scrolling,
    // we don't look for swipes.
    listView.setOnScrollListener(touchListener.makeScrollListener());

    // Set up normal ViewGroup example
    final ViewGroup dismissableContainer = (ViewGroup) findViewById(R.id.dismissable_container);
    for (int i = 0; i < items.length; i++) {
    final Button dismissableButton = new Button(this);
    dismissableButton.setLayoutParams(new ViewGroup.LayoutParams(
    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    dismissableButton.setText("Button " + (i + 1));
    dismissableButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Toast.makeText(MainActivity.this,
    "Clicked " + ((Button) view).getText(),
    Toast.LENGTH_SHORT).show();
    }
    });
    // Create a generic swipe-to-dismiss touch listener.
    dismissableButton.setOnTouchListener(new SwipeDismissTouchListener(
    dismissableButton,
    null,
    new SwipeDismissTouchListener.OnDismissCallback() {
    @Override
    public void onDismiss(View view, Object token) {
    dismissableContainer.removeView(dismissableButton);
    }
    }));
    dismissableContainer.addView(dismissableButton);
    }
    }

    @Override
    protected void onListItemClick(ListView listView, View view, int position, long id) {
    Toast.makeText(this,
    "Clicked " + getListAdapter().getItem(position).toString(),
    Toast.LENGTH_SHORT).show();
    }
    }
    358 changes: 2 additions & 356 deletions SwipeDismissListViewTouchListener.java
    Original file line number Diff line number Diff line change
    @@ -1,356 +1,2 @@
    // 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.example.android.swipedismiss;

    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.ValueAnimator;
    import android.graphics.Rect;
    import android.util.Log;
    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 java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;

    /**
    * 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>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>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
    * android.view.ViewPropertyAnimator}.</p>
    *
    * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
    * see {@link SwipeDismissTouchListener}.</p>
    *
    * @see SwipeDismissTouchListener
    */
    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 OnDismissCallback mCallback;
    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 boolean mSwiping;
    private VelocityTracker mVelocityTracker;
    private int mDownPosition;
    private View mDownView;
    private boolean mPaused;

    /**
    * 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 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 callback The callback to trigger when the user has indicated that she would like to
    * dismiss one or more list items.
    */
    public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
    ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
    mSlop = vc.getScaledTouchSlop();
    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    mAnimationTime = listView.getContext().getResources().getInteger(
    android.R.integer.config_shortAnimTime);
    mListView = listView;
    mCallback = callback;
    }

    /**
    * 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 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);
    }

    @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;
    }

    // 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[] listViewCoords = new int[2];
    mListView.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 = mListView.getChildAt(i);
    child.getHitRect(rect);
    if (rect.contains(x, y)) {
    mDownView = child;
    break;
    }
    }

    if (mDownView != null) {
    mDownX = motionEvent.getRawX();
    mDownPosition = mListView.getPositionForView(mDownView);

    mVelocityTracker = VelocityTracker.obtain();
    mVelocityTracker.addMovement(motionEvent);
    }
    view.onTouchEvent(motionEvent);
    return true;
    }

    case MotionEvent.ACTION_UP: {
    if (mVelocityTracker == null) {
    break;
    }

    float deltaX = motionEvent.getRawX() - mDownX;
    mVelocityTracker.addMovement(motionEvent);
    mVelocityTracker.computeCurrentVelocity(1000);
    float velocityX = Math.abs(mVelocityTracker.getXVelocity());
    float velocityY = Math.abs(mVelocityTracker.getYVelocity());
    boolean dismiss = false;
    boolean dismissRight = false;
    if (Math.abs(deltaX) > mViewWidth / 2) {
    dismiss = true;
    dismissRight = deltaX > 0;
    } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
    && velocityY < velocityX) {
    dismiss = true;
    dismissRight = mVelocityTracker.getXVelocity() > 0;
    }
    if (dismiss) {
    // dismiss
    final View downView = mDownView; // mDownView gets null'd before animation ends
    final int downPosition = mDownPosition;
    ++mDismissAnimationRefCount;
    mDownView.animate()
    .translationX(dismissRight ? mViewWidth : -mViewWidth)
    .alpha(0)
    .setDuration(mAnimationTime)
    .setListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    performDismiss(downView, downPosition);
    }
    });
    } else {
    // cancel
    mDownView.animate()
    .translationX(0)
    .alpha(1)
    .setDuration(mAnimationTime)
    .setListener(null);
    }
    mVelocityTracker = null;
    mDownX = 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;
    if (Math.abs(deltaX) > mSlop) {
    mSwiping = true;
    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);
    }

    if (mSwiping) {
    mDownView.setTranslationX(deltaX);
    mDownView.setAlpha(Math.max(0f, Math.min(1f,
    1f - 2f * Math.abs(deltaX) / mViewWidth)));
    return true;
    }
    break;
    }
    }
    return false;
    }

    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;
    }
    mCallback.onDismiss(mListView, dismissPositions);

    ViewGroup.LayoutParams lp;
    for (PendingDismissData pendingDismiss : mPendingDismisses) {
    // Reset view presentation
    pendingDismiss.view.setAlpha(1f);
    pendingDismiss.view.setTranslationX(0);
    lp = pendingDismiss.view.getLayoutParams();
    lp.height = originalHeight;
    pendingDismiss.view.setLayoutParams(lp);
    }

    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();
    }
    }
    Moved to
    https://github.com/romannurik/android-swipetodismiss
    241 changes: 0 additions & 241 deletions SwipeDismissTouchListener.java
    Original file line number Diff line number Diff line change
    @@ -1,241 +0,0 @@
    // 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.example.android.swipedismiss;

    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.ValueAnimator;
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.ViewGroup;

    /**
    * A {@link android.view.View.OnTouchListener} that makes any {@link View} dismissable when the
    * user swipes (drags her finger) horizontally across the view.
    *
    * <p><em>For {@link android.widget.ListView} list items that don't manage their own touch events
    * (i.e. you're using
    * {@link android.widget.ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)}
    * or an equivalent listener on {@link android.app.ListActivity} or
    * {@link android.app.ListFragment}, use {@link SwipeDismissListViewTouchListener} instead.</em></p>
    *
    * <p>Example usage:</p>
    *
    * <pre>
    * view.setOnTouchListener(new SwipeDismissTouchListener(
    * view,
    * null, // Optional token/cookie object
    * new SwipeDismissTouchListener.OnDismissCallback() {
    * public void onDismiss(View view, Object token) {
    * parent.removeView(view);
    * }
    * }));
    * </pre>
    *
    * <p>This class Requires API level 12 or later due to use of {@link
    * android.view.ViewPropertyAnimator}.</p>
    *
    * @see SwipeDismissListViewTouchListener
    */
    public class SwipeDismissTouchListener 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 View mView;
    private OnDismissCallback mCallback;
    private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero

    // Transient properties
    private float mDownX;
    private boolean mSwiping;
    private Object mToken;
    private VelocityTracker mVelocityTracker;
    private float mTranslationX;

    /**
    * The callback interface used by {@link SwipeDismissTouchListener} to inform its client
    * about a successful dismissal of the view for which it was created.
    */
    public interface OnDismissCallback {
    /**
    * Called when the user has indicated they she would like to dismiss the view.
    *
    * @param view The originating {@link View} to be dismissed.
    * @param token The optional token passed to this object's constructor.
    */
    void onDismiss(View view, Object token);
    }

    /**
    * Constructs a new swipe-to-dismiss touch listener for the given view.
    *
    * @param view The view to make dismissable.
    * @param token An optional token/cookie object to be passed through to the callback.
    * @param callback The callback to trigger when the user has indicated that she would like to
    * dismiss this view.
    */
    public SwipeDismissTouchListener(View view, Object token, OnDismissCallback callback) {
    ViewConfiguration vc = ViewConfiguration.get(view.getContext());
    mSlop = vc.getScaledTouchSlop();
    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    mAnimationTime = view.getContext().getResources().getInteger(
    android.R.integer.config_shortAnimTime);
    mView = view;
    mToken = token;
    mCallback = callback;
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
    // offset because the view is translated during swipe
    motionEvent.offsetLocation(mTranslationX, 0);

    if (mViewWidth < 2) {
    mViewWidth = mView.getWidth();
    }

    switch (motionEvent.getActionMasked()) {
    case MotionEvent.ACTION_DOWN: {
    // TODO: ensure this is a finger, and set a flag
    mDownX = motionEvent.getRawX();
    mVelocityTracker = VelocityTracker.obtain();
    mVelocityTracker.addMovement(motionEvent);
    view.onTouchEvent(motionEvent);
    return false;
    }

    case MotionEvent.ACTION_UP: {
    if (mVelocityTracker == null) {
    break;
    }

    float deltaX = motionEvent.getRawX() - mDownX;
    mVelocityTracker.addMovement(motionEvent);
    mVelocityTracker.computeCurrentVelocity(1000);
    float velocityX = Math.abs(mVelocityTracker.getXVelocity());
    float velocityY = Math.abs(mVelocityTracker.getYVelocity());
    boolean dismiss = false;
    boolean dismissRight = false;
    if (Math.abs(deltaX) > mViewWidth / 2) {
    dismiss = true;
    dismissRight = deltaX > 0;
    } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
    && velocityY < velocityX) {
    dismiss = true;
    dismissRight = mVelocityTracker.getXVelocity() > 0;
    }
    if (dismiss) {
    // dismiss
    mView.animate()
    .translationX(dismissRight ? mViewWidth : -mViewWidth)
    .alpha(0)
    .setDuration(mAnimationTime)
    .setListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    performDismiss();
    }
    });
    } else {
    // cancel
    mView.animate()
    .translationX(0)
    .alpha(1)
    .setDuration(mAnimationTime)
    .setListener(null);
    }
    mVelocityTracker = null;
    mTranslationX = 0;
    mDownX = 0;
    mSwiping = false;
    break;
    }

    case MotionEvent.ACTION_MOVE: {
    if (mVelocityTracker == null) {
    break;
    }

    mVelocityTracker.addMovement(motionEvent);
    float deltaX = motionEvent.getRawX() - mDownX;
    if (Math.abs(deltaX) > mSlop) {
    mSwiping = true;
    mView.getParent().requestDisallowInterceptTouchEvent(true);

    // Cancel listview's touch
    MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
    cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
    (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
    mView.onTouchEvent(cancelEvent);
    }

    if (mSwiping) {
    mTranslationX = deltaX;
    mView.setTranslationX(deltaX);
    // TODO: use an ease-out interpolator or such
    mView.setAlpha(Math.max(0f, Math.min(1f,
    1f - 2f * Math.abs(deltaX) / mViewWidth)));
    return true;
    }
    break;
    }
    }
    return false;
    }

    private void performDismiss() {
    // Animate the dismissed view to zero-height and then fire the dismiss callback.
    // This triggers layout on each animation frame; in the future we may want to do something
    // smarter and more performant.

    final ViewGroup.LayoutParams lp = mView.getLayoutParams();
    final int originalHeight = mView.getHeight();

    ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);

    animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    mCallback.onDismiss(mView, mToken);
    // Reset view presentation
    mView.setAlpha(1f);
    mView.setTranslationX(0);
    lp.height = originalHeight;
    mView.setLayoutParams(lp);
    }
    });

    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
    lp.height = (Integer) valueAnimator.getAnimatedValue();
    mView.setLayoutParams(lp);
    }
    });

    animator.start();
    }
    }
    66 changes: 0 additions & 66 deletions activity_main.xml
    Original file line number Diff line number Diff line change
    @@ -1,66 +0,0 @@
    <!-- 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.
    -->

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:baselineAligned="false"
    android:padding="16dp">

    <LinearLayout android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"
    android:layout_marginRight="8dp"
    android:orientation="vertical">

    <TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="?android:listSeparatorTextViewStyle"
    android:text="Simple Views" />

    <ScrollView android:fillViewport="true"
    android:layout_width="match_parent"
    android:layout_weight="1"
    android:layout_height="0dp">
    <LinearLayout android:id="@+id/dismissable_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" />
    </ScrollView>

    </LinearLayout>

    <LinearLayout android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"
    android:layout_marginLeft="8dp"
    android:orientation="vertical">

    <TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="?android:listSeparatorTextViewStyle"
    android:text="ListView" />

    <ListView
    android:id="@android:id/list"
    android:layout_weight="1"
    android:layout_width="match_parent"
    android:layout_height="0dp" />
    </LinearLayout>
    </LinearLayout>
  2. romannurik renamed this gist Jun 23, 2012. 1 changed file with 0 additions and 0 deletions.
  3. romannurik created this gist Jun 23, 2012.
    108 changes: 108 additions & 0 deletions MainActivity.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,108 @@
    // 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.example.android.swipedismiss;

    import android.app.ListActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.Button;
    import android.widget.ListView;
    import android.widget.Toast;

    import java.util.ArrayList;
    import java.util.Arrays;

    public class MainActivity extends ListActivity {
    ArrayAdapter<String> mAdapter;

    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Set up ListView example
    String[] items = new String[20];
    for (int i = 0; i < items.length; i++) {
    items[i] = "Item " + (i + 1);
    }

    mAdapter = new ArrayAdapter<String>(this,
    android.R.layout.simple_list_item_1,
    android.R.id.text1,
    new ArrayList<String>(Arrays.asList(items)));
    setListAdapter(mAdapter);

    ListView listView = getListView();
    // Create a ListView-specific touch listener. ListViews are given special treatment because
    // by default they handle touches for their list items... i.e. they're in charge of drawing
    // the pressed state (the list selector), handling list item clicks, etc.
    SwipeDismissListViewTouchListener touchListener =
    new SwipeDismissListViewTouchListener(
    listView,
    new SwipeDismissListViewTouchListener.OnDismissCallback() {
    @Override
    public void onDismiss(ListView listView, int[] reverseSortedPositions) {
    for (int position : reverseSortedPositions) {
    mAdapter.remove(mAdapter.getItem(position));
    }
    mAdapter.notifyDataSetChanged();
    }
    });
    listView.setOnTouchListener(touchListener);
    // Setting this scroll listener is required to ensure that during ListView scrolling,
    // we don't look for swipes.
    listView.setOnScrollListener(touchListener.makeScrollListener());

    // Set up normal ViewGroup example
    final ViewGroup dismissableContainer = (ViewGroup) findViewById(R.id.dismissable_container);
    for (int i = 0; i < items.length; i++) {
    final Button dismissableButton = new Button(this);
    dismissableButton.setLayoutParams(new ViewGroup.LayoutParams(
    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    dismissableButton.setText("Button " + (i + 1));
    dismissableButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Toast.makeText(MainActivity.this,
    "Clicked " + ((Button) view).getText(),
    Toast.LENGTH_SHORT).show();
    }
    });
    // Create a generic swipe-to-dismiss touch listener.
    dismissableButton.setOnTouchListener(new SwipeDismissTouchListener(
    dismissableButton,
    null,
    new SwipeDismissTouchListener.OnDismissCallback() {
    @Override
    public void onDismiss(View view, Object token) {
    dismissableContainer.removeView(dismissableButton);
    }
    }));
    dismissableContainer.addView(dismissableButton);
    }
    }

    @Override
    protected void onListItemClick(ListView listView, View view, int position, long id) {
    Toast.makeText(this,
    "Clicked " + getListAdapter().getItem(position).toString(),
    Toast.LENGTH_SHORT).show();
    }
    }
    356 changes: 356 additions & 0 deletions SwipeDismissListViewTouchListener
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,356 @@
    // 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.example.android.swipedismiss;

    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.ValueAnimator;
    import android.graphics.Rect;
    import android.util.Log;
    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 java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;

    /**
    * 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>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>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
    * android.view.ViewPropertyAnimator}.</p>
    *
    * <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
    * see {@link SwipeDismissTouchListener}.</p>
    *
    * @see SwipeDismissTouchListener
    */
    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 OnDismissCallback mCallback;
    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 boolean mSwiping;
    private VelocityTracker mVelocityTracker;
    private int mDownPosition;
    private View mDownView;
    private boolean mPaused;

    /**
    * 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 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 callback The callback to trigger when the user has indicated that she would like to
    * dismiss one or more list items.
    */
    public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
    ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
    mSlop = vc.getScaledTouchSlop();
    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    mAnimationTime = listView.getContext().getResources().getInteger(
    android.R.integer.config_shortAnimTime);
    mListView = listView;
    mCallback = callback;
    }

    /**
    * 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 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);
    }

    @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;
    }

    // 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[] listViewCoords = new int[2];
    mListView.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 = mListView.getChildAt(i);
    child.getHitRect(rect);
    if (rect.contains(x, y)) {
    mDownView = child;
    break;
    }
    }

    if (mDownView != null) {
    mDownX = motionEvent.getRawX();
    mDownPosition = mListView.getPositionForView(mDownView);

    mVelocityTracker = VelocityTracker.obtain();
    mVelocityTracker.addMovement(motionEvent);
    }
    view.onTouchEvent(motionEvent);
    return true;
    }

    case MotionEvent.ACTION_UP: {
    if (mVelocityTracker == null) {
    break;
    }

    float deltaX = motionEvent.getRawX() - mDownX;
    mVelocityTracker.addMovement(motionEvent);
    mVelocityTracker.computeCurrentVelocity(1000);
    float velocityX = Math.abs(mVelocityTracker.getXVelocity());
    float velocityY = Math.abs(mVelocityTracker.getYVelocity());
    boolean dismiss = false;
    boolean dismissRight = false;
    if (Math.abs(deltaX) > mViewWidth / 2) {
    dismiss = true;
    dismissRight = deltaX > 0;
    } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
    && velocityY < velocityX) {
    dismiss = true;
    dismissRight = mVelocityTracker.getXVelocity() > 0;
    }
    if (dismiss) {
    // dismiss
    final View downView = mDownView; // mDownView gets null'd before animation ends
    final int downPosition = mDownPosition;
    ++mDismissAnimationRefCount;
    mDownView.animate()
    .translationX(dismissRight ? mViewWidth : -mViewWidth)
    .alpha(0)
    .setDuration(mAnimationTime)
    .setListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    performDismiss(downView, downPosition);
    }
    });
    } else {
    // cancel
    mDownView.animate()
    .translationX(0)
    .alpha(1)
    .setDuration(mAnimationTime)
    .setListener(null);
    }
    mVelocityTracker = null;
    mDownX = 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;
    if (Math.abs(deltaX) > mSlop) {
    mSwiping = true;
    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);
    }

    if (mSwiping) {
    mDownView.setTranslationX(deltaX);
    mDownView.setAlpha(Math.max(0f, Math.min(1f,
    1f - 2f * Math.abs(deltaX) / mViewWidth)));
    return true;
    }
    break;
    }
    }
    return false;
    }

    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;
    }
    mCallback.onDismiss(mListView, dismissPositions);

    ViewGroup.LayoutParams lp;
    for (PendingDismissData pendingDismiss : mPendingDismisses) {
    // Reset view presentation
    pendingDismiss.view.setAlpha(1f);
    pendingDismiss.view.setTranslationX(0);
    lp = pendingDismiss.view.getLayoutParams();
    lp.height = originalHeight;
    pendingDismiss.view.setLayoutParams(lp);
    }

    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();
    }
    }
    241 changes: 241 additions & 0 deletions SwipeDismissTouchListener.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,241 @@
    // 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.example.android.swipedismiss;

    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.ValueAnimator;
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.ViewGroup;

    /**
    * A {@link android.view.View.OnTouchListener} that makes any {@link View} dismissable when the
    * user swipes (drags her finger) horizontally across the view.
    *
    * <p><em>For {@link android.widget.ListView} list items that don't manage their own touch events
    * (i.e. you're using
    * {@link android.widget.ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)}
    * or an equivalent listener on {@link android.app.ListActivity} or
    * {@link android.app.ListFragment}, use {@link SwipeDismissListViewTouchListener} instead.</em></p>
    *
    * <p>Example usage:</p>
    *
    * <pre>
    * view.setOnTouchListener(new SwipeDismissTouchListener(
    * view,
    * null, // Optional token/cookie object
    * new SwipeDismissTouchListener.OnDismissCallback() {
    * public void onDismiss(View view, Object token) {
    * parent.removeView(view);
    * }
    * }));
    * </pre>
    *
    * <p>This class Requires API level 12 or later due to use of {@link
    * android.view.ViewPropertyAnimator}.</p>
    *
    * @see SwipeDismissListViewTouchListener
    */
    public class SwipeDismissTouchListener 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 View mView;
    private OnDismissCallback mCallback;
    private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero

    // Transient properties
    private float mDownX;
    private boolean mSwiping;
    private Object mToken;
    private VelocityTracker mVelocityTracker;
    private float mTranslationX;

    /**
    * The callback interface used by {@link SwipeDismissTouchListener} to inform its client
    * about a successful dismissal of the view for which it was created.
    */
    public interface OnDismissCallback {
    /**
    * Called when the user has indicated they she would like to dismiss the view.
    *
    * @param view The originating {@link View} to be dismissed.
    * @param token The optional token passed to this object's constructor.
    */
    void onDismiss(View view, Object token);
    }

    /**
    * Constructs a new swipe-to-dismiss touch listener for the given view.
    *
    * @param view The view to make dismissable.
    * @param token An optional token/cookie object to be passed through to the callback.
    * @param callback The callback to trigger when the user has indicated that she would like to
    * dismiss this view.
    */
    public SwipeDismissTouchListener(View view, Object token, OnDismissCallback callback) {
    ViewConfiguration vc = ViewConfiguration.get(view.getContext());
    mSlop = vc.getScaledTouchSlop();
    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    mAnimationTime = view.getContext().getResources().getInteger(
    android.R.integer.config_shortAnimTime);
    mView = view;
    mToken = token;
    mCallback = callback;
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
    // offset because the view is translated during swipe
    motionEvent.offsetLocation(mTranslationX, 0);

    if (mViewWidth < 2) {
    mViewWidth = mView.getWidth();
    }

    switch (motionEvent.getActionMasked()) {
    case MotionEvent.ACTION_DOWN: {
    // TODO: ensure this is a finger, and set a flag
    mDownX = motionEvent.getRawX();
    mVelocityTracker = VelocityTracker.obtain();
    mVelocityTracker.addMovement(motionEvent);
    view.onTouchEvent(motionEvent);
    return false;
    }

    case MotionEvent.ACTION_UP: {
    if (mVelocityTracker == null) {
    break;
    }

    float deltaX = motionEvent.getRawX() - mDownX;
    mVelocityTracker.addMovement(motionEvent);
    mVelocityTracker.computeCurrentVelocity(1000);
    float velocityX = Math.abs(mVelocityTracker.getXVelocity());
    float velocityY = Math.abs(mVelocityTracker.getYVelocity());
    boolean dismiss = false;
    boolean dismissRight = false;
    if (Math.abs(deltaX) > mViewWidth / 2) {
    dismiss = true;
    dismissRight = deltaX > 0;
    } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
    && velocityY < velocityX) {
    dismiss = true;
    dismissRight = mVelocityTracker.getXVelocity() > 0;
    }
    if (dismiss) {
    // dismiss
    mView.animate()
    .translationX(dismissRight ? mViewWidth : -mViewWidth)
    .alpha(0)
    .setDuration(mAnimationTime)
    .setListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    performDismiss();
    }
    });
    } else {
    // cancel
    mView.animate()
    .translationX(0)
    .alpha(1)
    .setDuration(mAnimationTime)
    .setListener(null);
    }
    mVelocityTracker = null;
    mTranslationX = 0;
    mDownX = 0;
    mSwiping = false;
    break;
    }

    case MotionEvent.ACTION_MOVE: {
    if (mVelocityTracker == null) {
    break;
    }

    mVelocityTracker.addMovement(motionEvent);
    float deltaX = motionEvent.getRawX() - mDownX;
    if (Math.abs(deltaX) > mSlop) {
    mSwiping = true;
    mView.getParent().requestDisallowInterceptTouchEvent(true);

    // Cancel listview's touch
    MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
    cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
    (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
    mView.onTouchEvent(cancelEvent);
    }

    if (mSwiping) {
    mTranslationX = deltaX;
    mView.setTranslationX(deltaX);
    // TODO: use an ease-out interpolator or such
    mView.setAlpha(Math.max(0f, Math.min(1f,
    1f - 2f * Math.abs(deltaX) / mViewWidth)));
    return true;
    }
    break;
    }
    }
    return false;
    }

    private void performDismiss() {
    // Animate the dismissed view to zero-height and then fire the dismiss callback.
    // This triggers layout on each animation frame; in the future we may want to do something
    // smarter and more performant.

    final ViewGroup.LayoutParams lp = mView.getLayoutParams();
    final int originalHeight = mView.getHeight();

    ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);

    animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    mCallback.onDismiss(mView, mToken);
    // Reset view presentation
    mView.setAlpha(1f);
    mView.setTranslationX(0);
    lp.height = originalHeight;
    mView.setLayoutParams(lp);
    }
    });

    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
    lp.height = (Integer) valueAnimator.getAnimatedValue();
    mView.setLayoutParams(lp);
    }
    });

    animator.start();
    }
    }
    66 changes: 66 additions & 0 deletions activity_main.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,66 @@
    <!-- 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.
    -->

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:baselineAligned="false"
    android:padding="16dp">

    <LinearLayout android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"
    android:layout_marginRight="8dp"
    android:orientation="vertical">

    <TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="?android:listSeparatorTextViewStyle"
    android:text="Simple Views" />

    <ScrollView android:fillViewport="true"
    android:layout_width="match_parent"
    android:layout_weight="1"
    android:layout_height="0dp">
    <LinearLayout android:id="@+id/dismissable_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" />
    </ScrollView>

    </LinearLayout>

    <LinearLayout android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"
    android:layout_marginLeft="8dp"
    android:orientation="vertical">

    <TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="?android:listSeparatorTextViewStyle"
    android:text="ListView" />

    <ListView
    android:id="@android:id/list"
    android:layout_weight="1"
    android:layout_width="match_parent"
    android:layout_height="0dp" />
    </LinearLayout>
    </LinearLayout>