Home | History | Annotate | Download | only in ui
      1 /**
      2  * Copyright (C) 2014 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.ui;
     19 
     20 import android.content.Context;
     21 import android.support.annotation.Nullable;
     22 import android.view.MotionEvent;
     23 import android.view.VelocityTracker;
     24 import android.view.ViewConfiguration;
     25 
     26 /**
     27  * Generic utility class that deals with capturing drag events in a particular horizontal direction
     28  * and calls the callback interface for drag events.
     29  *
     30  * Usage:
     31  *
     32  * <code>
     33  *
     34  *  class CustomView extends ... {
     35  *      private boolean mShouldInterceptDrag;
     36  *      private int mDragMode;
     37  *
     38  *      public boolean onInterceptTouchEvent(MotionEvent ev) {
     39  *          switch (ev.getAction()) {
     40  *              case MotionEvent.ACTION_DOWN:
     41  *                  // Check if the event is in the draggable area
     42  *                  mShouldInterceptDrag = ...;
     43  *                  mDragMode = ...;
     44  *          }
     45  *          return mShouldInterceptDrag && GmailDragHelper.processTouchEvent(ev, mDragMode);
     46  *      }
     47  *
     48  *      public boolean onTouchEvent(MotionEvent ev) {
     49  *          if (mShouldInterceptDrag) {
     50  *              GmailDragHelper.processTouchEvent(ev, mDragMode);
     51  *              return true;
     52  *          }
     53  *          return super.onTouchEvent(ev);
     54  *      }
     55  *  }
     56  *
     57  * </code>
     58  */
     59 public class GmailDragHelper {
     60     public static final int CAPTURE_LEFT_TO_RIGHT = 0;
     61     public static final int CAPTURE_RIGHT_TO_LEFT = 1;
     62 
     63     private final GmailDragHelperCallback mCallback;
     64     private final ViewConfiguration mConfiguration;
     65 
     66     private boolean mDragging;
     67     private VelocityTracker mVelocityTracker;
     68 
     69     private float mInitialInterceptedX;
     70     private float mInitialInterceptedY;
     71 
     72     private float mStartDragX;
     73 
     74     public interface GmailDragHelperCallback {
     75         public void onDragStarted();
     76         public void onDrag(float deltaX);
     77         public void onDragEnded(float deltaX, float velocityX, boolean isFling);
     78     }
     79 
     80     /**
     81      */
     82     public GmailDragHelper(Context context, GmailDragHelperCallback callback) {
     83         mCallback = callback;
     84         mConfiguration = ViewConfiguration.get(context);
     85     }
     86 
     87     /**
     88      * Process incoming MotionEvent to compute the new drag state and coordinates.
     89      *
     90      * @param ev the captured MotionEvent
     91      * @param dragMode either {@link GmailDragHelper#CAPTURE_LEFT_TO_RIGHT} or
     92      *   {@link GmailDragHelper#CAPTURE_RIGHT_TO_LEFT}
     93      * @return whether if drag is happening
     94      */
     95     public boolean processTouchEvent(MotionEvent ev, int dragMode) {
     96         return processTouchEvent(ev, dragMode, null);
     97     }
     98 
     99     /**
    100      * @param xThreshold optional parameter to specify that the drag can only happen if it crosses
    101      *   the threshold coordinate. This can be used to only start the drag once the user hits the
    102      *   edge of the view.
    103      */
    104     public boolean processTouchEvent(MotionEvent ev, int dragMode, @Nullable Float xThreshold) {
    105         if (mVelocityTracker == null) {
    106             mVelocityTracker = VelocityTracker.obtain();
    107         }
    108         mVelocityTracker.addMovement(ev);
    109 
    110         switch (ev.getAction()) {
    111             case MotionEvent.ACTION_DOWN:
    112                 mInitialInterceptedX = ev.getX();
    113                 mInitialInterceptedY = ev.getY();
    114                 break;
    115             case MotionEvent.ACTION_MOVE:
    116                 if (mDragging) {
    117                     mCallback.onDrag(ev.getX() - mStartDragX);
    118                 } else {
    119                     // Try to start dragging
    120                     final float evX = ev.getX();
    121                     // Check for directional drag
    122                     if ((dragMode == CAPTURE_LEFT_TO_RIGHT && evX <= mInitialInterceptedX) ||
    123                             (dragMode == CAPTURE_RIGHT_TO_LEFT && evX >= mInitialInterceptedX)) {
    124                         break;
    125                     }
    126 
    127                     // Check for optional threshold
    128                     boolean passedThreshold = true;
    129                     if (xThreshold != null) {
    130                         if (dragMode == CAPTURE_LEFT_TO_RIGHT) {
    131                             passedThreshold = evX > xThreshold;
    132                         } else {
    133                             passedThreshold = evX < xThreshold;
    134                         }
    135                     }
    136 
    137                     // Check for drag threshold
    138                     final float deltaX = Math.abs(evX - mInitialInterceptedX);
    139                     final float deltaY = Math.abs(ev.getY() - mInitialInterceptedY);
    140                     if (deltaX >= mConfiguration.getScaledTouchSlop() && deltaX >= deltaY
    141                             && passedThreshold) {
    142                         setDragging(true, evX);
    143                     }
    144                 }
    145                 break;
    146             case MotionEvent.ACTION_UP:
    147                 if (mDragging) {
    148                     setDragging(false, ev.getX());
    149                 }
    150                 break;
    151         }
    152 
    153         return mDragging;
    154     }
    155 
    156     /**
    157      * Set the internal dragging state and calls the appropriate callbacks.
    158      */
    159     private void setDragging(boolean dragging, float evX) {
    160         mDragging = dragging;
    161 
    162         if (mDragging) {
    163             mStartDragX = evX;
    164             mCallback.onDragStarted();
    165         } else {
    166             // Here velocity is in pixel/second, let's take that into account for evX.
    167             mVelocityTracker.computeCurrentVelocity(1000);
    168             // Check for fling
    169             final float xVelocity = mVelocityTracker.getXVelocity();
    170             final boolean isFling =
    171                     Math.abs(xVelocity) > mConfiguration.getScaledMinimumFlingVelocity();
    172             mVelocityTracker.clear();
    173 
    174             mCallback.onDragEnded(evX - mStartDragX, xVelocity, isFling);
    175         }
    176     }
    177 }
    178