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 androidx.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 { onDragStarted()75 public void onDragStarted(); onDrag(float deltaX)76 public void onDrag(float deltaX); onDragEnded(float deltaX, float velocityX, boolean isFling)77 public void onDragEnded(float deltaX, float velocityX, boolean isFling); 78 } 79 80 /** 81 */ GmailDragHelper(Context context, GmailDragHelperCallback callback)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 */ processTouchEvent(MotionEvent ev, int dragMode)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 */ processTouchEvent(MotionEvent ev, int dragMode, @Nullable Float xThreshold)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 */ setDragging(boolean dragging, float evX)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