1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility; 18 19 import android.annotation.DisplayContext; 20 import android.annotation.NonNull; 21 import android.content.Context; 22 import android.graphics.PointF; 23 import android.os.Handler; 24 import android.view.Display; 25 import android.view.MotionEvent; 26 import android.view.ViewConfiguration; 27 28 /** 29 * Detects single tap and drag gestures using the supplied {@link MotionEvent}s. The {@link 30 * OnGestureListener} callback will notify users when a particular motion event has occurred. This 31 * class should only be used with {@link MotionEvent}s reported via touch (don't use for trackball 32 * events). 33 */ 34 class MagnificationGestureDetector { 35 36 interface OnGestureListener { 37 /** 38 * Called when a tap is completed within {@link ViewConfiguration#getLongPressTimeout()} and 39 * the offset between {@link MotionEvent}s and the down event doesn't exceed {@link 40 * ViewConfiguration#getScaledTouchSlop()}. 41 * 42 * @return {@code true} if this gesture is handled. 43 */ onSingleTap()44 boolean onSingleTap(); 45 46 /** 47 * Called when the user is performing dragging gesture. It is started after the offset 48 * between the down location and the move event location exceed 49 * {@link ViewConfiguration#getScaledTouchSlop()}. 50 * 51 * @param offsetX The X offset in screen coordinate. 52 * @param offsetY The Y offset in screen coordinate. 53 * @return {@code true} if this gesture is handled. 54 */ onDrag(float offsetX, float offsetY)55 boolean onDrag(float offsetX, float offsetY); 56 57 /** 58 * Notified when a tap occurs with the down {@link MotionEvent} that triggered it. This will 59 * be triggered immediately for every down event. All other events should be preceded by 60 * this. 61 * 62 * @param x The X coordinate of the down event. 63 * @param y The Y coordinate of the down event. 64 * @return {@code true} if the down event is handled, otherwise the events won't be sent to 65 * the view. 66 */ onStart(float x, float y)67 boolean onStart(float x, float y); 68 69 /** 70 * Called when the detection is finished. In other words, it is called when up/cancel {@link 71 * MotionEvent} is received. It will be triggered after single-tap 72 * 73 * @param x The X coordinate on the screen of the up event or the cancel event. 74 * @param y The Y coordinate on the screen of the up event or the cancel event. 75 * @return {@code true} if the event is handled. 76 */ onFinish(float x, float y)77 boolean onFinish(float x, float y); 78 } 79 80 private final PointF mPointerDown = new PointF(); 81 private final PointF mPointerLocation = new PointF(Float.NaN, Float.NaN); 82 private final Handler mHandler; 83 private final Runnable mCancelTapGestureRunnable; 84 private final OnGestureListener mOnGestureListener; 85 private int mTouchSlopSquare; 86 // Assume the gesture default is a single-tap. Set it to false if the gesture couldn't be a 87 // single-tap anymore. 88 private boolean mDetectSingleTap = true; 89 private boolean mDraggingDetected = false; 90 91 /** 92 * @param context {@link Context} that is from {@link Context#createDisplayContext(Display)}. 93 * @param handler The handler to post the runnable. 94 * @param listener The listener invoked for all the callbacks. 95 */ MagnificationGestureDetector(@isplayContext Context context, @NonNull Handler handler, @NonNull OnGestureListener listener)96 MagnificationGestureDetector(@DisplayContext Context context, @NonNull Handler handler, 97 @NonNull OnGestureListener listener) { 98 final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 99 mTouchSlopSquare = touchSlop * touchSlop; 100 mHandler = handler; 101 mOnGestureListener = listener; 102 mCancelTapGestureRunnable = () -> mDetectSingleTap = false; 103 } 104 105 /** 106 * Analyzes the given motion event and if applicable to trigger the appropriate callbacks on the 107 * {@link OnGestureListener} supplied. 108 * 109 * @param event The current motion event. 110 * @return {@code True} if the {@link OnGestureListener} consumes the event, else false. 111 */ onTouch(MotionEvent event)112 boolean onTouch(MotionEvent event) { 113 final float rawX = event.getRawX(); 114 final float rawY = event.getRawY(); 115 boolean handled = false; 116 switch (event.getActionMasked()) { 117 case MotionEvent.ACTION_DOWN: 118 mPointerDown.set(rawX, rawY); 119 mHandler.postAtTime(mCancelTapGestureRunnable, 120 event.getDownTime() + ViewConfiguration.getLongPressTimeout()); 121 handled |= mOnGestureListener.onStart(rawX, rawY); 122 break; 123 case MotionEvent.ACTION_POINTER_DOWN: 124 stopSingleTapDetection(); 125 break; 126 case MotionEvent.ACTION_MOVE: 127 stopSingleTapDetectionIfNeeded(rawX, rawY); 128 handled |= notifyDraggingGestureIfNeeded(rawX, rawY); 129 break; 130 case MotionEvent.ACTION_UP: 131 stopSingleTapDetectionIfNeeded(rawX, rawY); 132 if (mDetectSingleTap) { 133 handled |= mOnGestureListener.onSingleTap(); 134 } 135 // Fall through 136 case MotionEvent.ACTION_CANCEL: 137 handled |= mOnGestureListener.onFinish(rawX, rawY); 138 reset(); 139 break; 140 } 141 return handled; 142 } 143 stopSingleTapDetectionIfNeeded(float x, float y)144 private void stopSingleTapDetectionIfNeeded(float x, float y) { 145 if (mDraggingDetected) { 146 return; 147 } 148 if (!isLocationValid(mPointerDown)) { 149 return; 150 } 151 152 final int deltaX = (int) (mPointerDown.x - x); 153 final int deltaY = (int) (mPointerDown.y - y); 154 final int distanceSquare = (deltaX * deltaX) + (deltaY * deltaY); 155 if (distanceSquare > mTouchSlopSquare) { 156 mDraggingDetected = true; 157 stopSingleTapDetection(); 158 } 159 } 160 stopSingleTapDetection()161 private void stopSingleTapDetection() { 162 mHandler.removeCallbacks(mCancelTapGestureRunnable); 163 mDetectSingleTap = false; 164 } 165 notifyDraggingGestureIfNeeded(float x, float y)166 private boolean notifyDraggingGestureIfNeeded(float x, float y) { 167 if (!mDraggingDetected) { 168 return false; 169 } 170 if (!isLocationValid(mPointerLocation)) { 171 mPointerLocation.set(mPointerDown); 172 } 173 final float offsetX = x - mPointerLocation.x; 174 final float offsetY = y - mPointerLocation.y; 175 mPointerLocation.set(x, y); 176 return mOnGestureListener.onDrag(offsetX, offsetY); 177 } 178 reset()179 private void reset() { 180 resetPointF(mPointerDown); 181 resetPointF(mPointerLocation); 182 mHandler.removeCallbacks(mCancelTapGestureRunnable); 183 mDetectSingleTap = true; 184 mDraggingDetected = false; 185 } 186 resetPointF(PointF pointF)187 private static void resetPointF(PointF pointF) { 188 pointF.x = Float.NaN; 189 pointF.y = Float.NaN; 190 } 191 isLocationValid(PointF location)192 private static boolean isLocationValid(PointF location) { 193 return !Float.isNaN(location.x) && !Float.isNaN(location.y); 194 } 195 } 196