• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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