code: /*
   * Copyright 2013 Google Inc.
   *
   * 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 org.physical_web.physicalweb;
  
  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.widget.AbsListView;
  import android.widget.ListView;
  
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * 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.
   *
   **/
  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;
  
      // Transient properties
      private List<PendingDismissData> mPendingDismisses = new ArrayList<PendingDismissData>();
      private int mDismissAnimationRefCount = 0;
      private float mDownX;
      private float mDownY;
      private static boolean mSwiping;
      private int mSwipingSlop;
      private VelocityTracker mVelocityTracker;
      private int mDownPosition;
      private View mDownView;
      private boolean mPaused;
      private static boolean mAnimating;
  
      /**
       * 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);
  
          /**
           * 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 position               Position of listview dismiss
           */
          void onDismiss(ListView listView, int position);
      }
  
      /**
       * 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;
      }
  
      /**
       * 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;
      }
  
      public static boolean isLocked() {
          return mSwiping || mAnimating;
      }
  
      /**
       * 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.
       *
       * @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) {
          int viewWidth = mListView.getWidth();
  
          switch (motionEvent.getActionMasked()) {
              case MotionEvent.ACTION_DOWN: {
                  if (mPaused) {
                      return false;
                  }
                  // 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();
                      mDownY = motionEvent.getRawY();
                      mDownPosition = mListView.getPositionForView(mDownView);
                      if (mCallbacks.canDismiss(mDownPosition)) {
                          mVelocityTracker = VelocityTracker.obtain();
                          mVelocityTracker.addMovement(motionEvent);
                      } else {
                          mDownView = null;
                      }
                  }
                  return false;
              }
  
              case MotionEvent.ACTION_CANCEL: {
                  if (mVelocityTracker == null) {
                      break;
                  }
  
                  if (mDownView != null && mSwiping) {
                      // 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_UP: {
                  view.performClick();
                  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) > viewWidth / 2 && 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
                      final View downView = mDownView; // mDownView gets null'd before animation ends
                      final int downPosition = mDownPosition;
                      ++mDismissAnimationRefCount;
                      mAnimating = true;
                      mDownView.animate()
                              .translationX(dismissRight ? viewWidth : -viewWidth)
                              .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.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;
                  if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 1.5) {
                      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);
                      cancelEvent.recycle();
                  }
  
                  if (mSwiping) {
                      mDownView.setTranslationX(deltaX - mSwipingSlop);
                      mDownView.setAlpha(Math.max(0f, Math.min(1f,
                              1f - 2f * Math.abs(deltaX) / (viewWidth == 0 ? 1 : viewWidth))));
                      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;
          }
      }
  
      public 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 params = 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) {
                      mAnimating = false;
                      mCallbacks.onDismiss(mListView, mPendingDismisses.get(0).position);
  
                      // Reset mDownPosition to avoid MotionEvent.ACTION_UP trying to start a dismiss
                      // animation with a stale position
                      mDownPosition = ListView.INVALID_POSITION;
  
                      // 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) {
                  params.height = (Integer) valueAnimator.getAnimatedValue();
                  dismissView.setLayoutParams(params);
              }
          });
  
          mPendingDismisses.add(new PendingDismissData(dismissPosition, dismissView));
          animator.start();
      }
  }
  