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.wm.shell.common.bubbles
18 
19 import android.graphics.PointF
20 import android.view.MotionEvent
21 import android.view.VelocityTracker
22 import android.view.View
23 import android.view.ViewConfiguration
24 import kotlin.math.hypot
25 
26 /**
27  * Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about
28  * the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the
29  * view's initial position.
30  */
31 abstract class RelativeTouchListener : View.OnTouchListener {
32 
33     /**
34      * Called when an ACTION_DOWN event is received for the given view.
35      *
36      * @return False if the object is not interested in MotionEvents at this time, or true if we
37      * should consume this event and subsequent events, and begin calling [onMove].
38      */
onDownnull39     abstract fun onDown(v: View, ev: MotionEvent): Boolean
40 
41     /**
42      * Called when an ACTION_MOVE event is received for the given view. This signals that the view
43      * is being dragged.
44      *
45      * @param viewInitialX The view's translationX value when this touch gesture started.
46      * @param viewInitialY The view's translationY value when this touch gesture started.
47      * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
48      * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
49      */
50     abstract fun onMove(
51         v: View,
52         ev: MotionEvent,
53         viewInitialX: Float,
54         viewInitialY: Float,
55         dx: Float,
56         dy: Float
57     )
58 
59     /**
60      * Called when an ACTION_UP event is received for the given view. This signals that a drag or
61      * fling gesture has completed.
62      *
63      * @param viewInitialX The view's translationX value when this touch gesture started.
64      * @param viewInitialY The view's translationY value when this touch gesture started.
65      * @param dx Horizontal distance covered, in pixels.
66      * @param dy Vertical distance covered, in pixels.
67      * @param velX The final horizontal velocity of the gesture, in pixels/second.
68      * @param velY The final vertical velocity of the gesture, in pixels/second.
69      */
70     abstract fun onUp(
71         v: View,
72         ev: MotionEvent,
73         viewInitialX: Float,
74         viewInitialY: Float,
75         dx: Float,
76         dy: Float,
77         velX: Float,
78         velY: Float
79     )
80 
81     open fun onCancel(
82         v: View,
83         ev: MotionEvent,
84         viewInitialX: Float,
85         viewInitialY: Float
86     ) {}
87 
88     /** The raw coordinates of the last ACTION_DOWN event. */
89     private var touchDown: PointF? = null
90 
91     /** The coordinates of the view, at the time of the last ACTION_DOWN event. */
92     private val viewPositionOnTouchDown = PointF()
93 
94     private val velocityTracker = VelocityTracker.obtain()
95 
96     private var touchSlop: Int = -1
97     private var movedEnough = false
98 
99     private var performedLongClick = false
100 
onTouchnull101     override fun onTouch(v: View, ev: MotionEvent): Boolean {
102         addMovement(ev)
103 
104         val dx = touchDown?.let { ev.rawX - it.x } ?: 0f
105         val dy = touchDown?.let { ev.rawY - it.y } ?: 0f
106 
107         when (ev.action) {
108             MotionEvent.ACTION_DOWN -> {
109                 if (!onDown(v, ev)) {
110                     return false
111                 }
112 
113                 // Grab the touch slop, it might have changed if the config changed since the
114                 // last gesture.
115                 touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop
116 
117                 touchDown = PointF(ev.rawX, ev.rawY)
118                 viewPositionOnTouchDown.set(v.translationX, v.translationY)
119 
120                 performedLongClick = false
121                 v.handler?.postDelayed({
122                     if (v.isLongClickable) {
123                         performedLongClick = v.performLongClick()
124                     }
125                 }, ViewConfiguration.getLongPressTimeout().toLong())
126             }
127 
128             MotionEvent.ACTION_MOVE -> {
129                 if (touchDown == null) return false
130                 if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
131                     movedEnough = true
132                     v.handler?.removeCallbacksAndMessages(null)
133                 }
134 
135                 if (movedEnough) {
136                     onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
137                 }
138             }
139 
140             MotionEvent.ACTION_UP -> {
141                 if (touchDown == null) return false
142                 if (movedEnough) {
143                     velocityTracker.computeCurrentVelocity(1000 /* units */)
144                     onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy,
145                             velocityTracker.xVelocity, velocityTracker.yVelocity)
146                 } else if (!performedLongClick) {
147                     v.performClick()
148                 } else {
149                     v.handler?.removeCallbacksAndMessages(null)
150                 }
151 
152                 velocityTracker.clear()
153                 movedEnough = false
154                 touchDown = null
155             }
156 
157             MotionEvent.ACTION_CANCEL -> {
158                 if (touchDown == null) return false
159                 v.handler?.removeCallbacksAndMessages(null)
160                 velocityTracker.clear()
161                 movedEnough = false
162                 touchDown = null
163                 onCancel(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y)
164             }
165         }
166 
167         return true
168     }
169 
170     /**
171      * Adds a movement to the velocity tracker using raw screen coordinates.
172      */
addMovementnull173     private fun addMovement(event: MotionEvent) {
174         val deltaX = event.rawX - event.x
175         val deltaY = event.rawY - event.y
176         event.offsetLocation(deltaX, deltaY)
177         velocityTracker.addMovement(event)
178         event.offsetLocation(-deltaX, -deltaY)
179     }
180 }