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