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
18package com.android.mail.ui;
19
20import android.content.Context;
21import android.support.annotation.Nullable;
22import android.view.MotionEvent;
23import android.view.VelocityTracker;
24import 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 */
59public 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