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