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      */
39     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     /** The raw coordinates of the last ACTION_DOWN event. */
82     private val touchDown = PointF()
83 
84     /** The coordinates of the view, at the time of the last ACTION_DOWN event. */
85     private val viewPositionOnTouchDown = PointF()
86 
87     private val velocityTracker = VelocityTracker.obtain()
88 
89     private var touchSlop: Int = -1
90     private var movedEnough = false
91 
92     private var performedLongClick = false
93 
94     @Suppress("UNCHECKED_CAST")
95     override fun onTouch(v: View, ev: MotionEvent): Boolean {
96         addMovement(ev)
97 
98         val dx = ev.rawX - touchDown.x
99         val dy = ev.rawY - touchDown.y
100 
101         when (ev.action) {
102             MotionEvent.ACTION_DOWN -> {
103                 if (!onDown(v, ev)) {
104                     return false
105                 }
106 
107                 // Grab the touch slop, it might have changed if the config changed since the
108                 // last gesture.
109                 touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop
110 
111                 touchDown.set(ev.rawX, ev.rawY)
112                 viewPositionOnTouchDown.set(v.translationX, v.translationY)
113 
114                 performedLongClick = false
115                 v.handler.postDelayed({
116                     if (v.isLongClickable) {
117                         performedLongClick = v.performLongClick()
118                     }
119                 }, ViewConfiguration.getLongPressTimeout().toLong())
120             }
121 
122             MotionEvent.ACTION_MOVE -> {
123                 if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
124                     movedEnough = true
125                     v.handler.removeCallbacksAndMessages(null)
126                 }
127 
128                 if (movedEnough) {
129                     onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
130                 }
131             }
132 
133             MotionEvent.ACTION_UP -> {
134                 if (movedEnough) {
135                     velocityTracker.computeCurrentVelocity(1000 /* units */)
136                     onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy,
137                             velocityTracker.xVelocity, velocityTracker.yVelocity)
138                 } else if (!performedLongClick) {
139                     v.performClick()
140                 } else {
141                     v.handler.removeCallbacksAndMessages(null)
142                 }
143 
144                 velocityTracker.clear()
145                 movedEnough = false
146             }
147 
148             MotionEvent.ACTION_CANCEL -> {
149                 v.handler.removeCallbacksAndMessages(null)
150                 velocityTracker.clear()
151                 movedEnough = false
152             }
153         }
154 
155         return true
156     }
157 
158     /**
159      * Adds a movement to the velocity tracker using raw screen coordinates.
160      */
161     private fun addMovement(event: MotionEvent) {
162         val deltaX = event.rawX - event.x
163         val deltaY = event.rawY - event.y
164         event.offsetLocation(deltaX, deltaY)
165         velocityTracker.addMovement(event)
166         event.offsetLocation(-deltaX, -deltaY)
167     }
168 }