1 /*
2  * Copyright (C) 2022 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.wm.shell.windowdecor;
18 
19 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
20 import static android.view.MotionEvent.ACTION_CANCEL;
21 import static android.view.MotionEvent.ACTION_DOWN;
22 import static android.view.MotionEvent.ACTION_MOVE;
23 import static android.view.MotionEvent.ACTION_UP;
24 
25 import android.graphics.PointF;
26 import android.view.MotionEvent;
27 import android.view.View;
28 
29 import androidx.annotation.Nullable;
30 
31 /**
32  * A detector for touch inputs that differentiates between drag and click inputs. It receives a flow
33  * of {@link MotionEvent} and generates a new flow of motion events with slop in consideration to
34  * the event handler. In particular, it always passes down, up and cancel events. It'll pass move
35  * events only when there is at least one move event that's beyond the slop threshold. For the
36  * purpose of convenience it also passes all events of other actions.
37  *
38  * All touch events must be passed through this class to track a drag event.
39  */
40 class DragDetector {
41     private final MotionEventHandler mEventHandler;
42 
43     private final PointF mInputDownPoint = new PointF();
44     private int mTouchSlop;
45     private boolean mIsDragEvent;
46     private int mDragPointerId;
47 
48     private boolean mResultOfDownAction;
49 
DragDetector(MotionEventHandler eventHandler)50     DragDetector(MotionEventHandler eventHandler) {
51         resetState();
52         mEventHandler = eventHandler;
53     }
54 
55     /**
56      * The receiver of the {@link MotionEvent} flow.
57      *
58      * @return the result returned by {@link #mEventHandler}, or the result when
59      * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
60      */
onMotionEvent(MotionEvent ev)61     boolean onMotionEvent(MotionEvent ev) {
62         return onMotionEvent(null /* view */, ev);
63     }
64 
65     /**
66      * The receiver of the {@link MotionEvent} flow.
67      *
68      * @return the result returned by {@link #mEventHandler}, or the result when
69      * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
70     */
onMotionEvent(View v, MotionEvent ev)71     boolean onMotionEvent(View v, MotionEvent ev) {
72         final boolean isTouchScreen =
73                 (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
74         if (!isTouchScreen) {
75             // Only touches generate noisy moves, so mouse/trackpad events don't need to filtered
76             // to take the slop threshold into consideration.
77             return mEventHandler.handleMotionEvent(v, ev);
78         }
79         switch (ev.getActionMasked()) {
80             case ACTION_DOWN: {
81                 mDragPointerId = ev.getPointerId(0);
82                 float rawX = ev.getRawX(0);
83                 float rawY = ev.getRawY(0);
84                 mInputDownPoint.set(rawX, rawY);
85                 mResultOfDownAction = mEventHandler.handleMotionEvent(v, ev);
86                 return mResultOfDownAction;
87             }
88             case ACTION_MOVE: {
89                 if (ev.findPointerIndex(mDragPointerId) == -1) {
90                     mDragPointerId = ev.getPointerId(0);
91                 }
92                 final int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
93                 if (!mIsDragEvent) {
94                     float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
95                     float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
96                     // Touches generate noisy moves, so only once the move is past the touch
97                     // slop threshold should it be considered a drag.
98                     mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop;
99                 }
100                 // The event handler should only be notified about 'move' events if a drag has been
101                 // detected.
102                 if (mIsDragEvent) {
103                     return mEventHandler.handleMotionEvent(v, ev);
104                 } else {
105                     return mResultOfDownAction;
106                 }
107             }
108             case ACTION_UP:
109             case ACTION_CANCEL: {
110                 resetState();
111                 return mEventHandler.handleMotionEvent(v, ev);
112             }
113             default:
114                 return mEventHandler.handleMotionEvent(v, ev);
115         }
116     }
117 
setTouchSlop(int touchSlop)118     void setTouchSlop(int touchSlop) {
119         mTouchSlop = touchSlop;
120     }
121 
resetState()122     private void resetState() {
123         mIsDragEvent = false;
124         mInputDownPoint.set(0, 0);
125         mDragPointerId = -1;
126         mResultOfDownAction = false;
127     }
128 
129     interface MotionEventHandler {
handleMotionEvent(@ullable View v, MotionEvent ev)130         boolean handleMotionEvent(@Nullable View v, MotionEvent ev);
131     }
132 }
133