• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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 
18 package com.android.systemui.common.ui.view
19 
20 import android.content.Context
21 import android.graphics.Point
22 import android.view.GestureDetector
23 import android.view.MotionEvent
24 import android.view.ViewConfiguration
25 import com.android.systemui.log.TouchHandlingViewLogger
26 import kotlin.math.pow
27 import kotlin.math.sqrt
28 import kotlin.properties.Delegates
29 import kotlinx.coroutines.DisposableHandle
30 
31 /** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */
32 class TouchHandlingViewInteractionHandler(
33     context: Context,
34     /**
35      * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
36      * allowing the delayed runnable to be canceled before it is run.
37      */
38     private val postDelayed: (block: Runnable, delayMs: Long) -> DisposableHandle,
39     /** Callback to be queried to check if the view is attached to its window. */
40     private val isAttachedToWindow: () -> Boolean,
41     /** Callback reporting the a long-press gesture was detected at the given coordinates. */
42     private val onLongPressDetected: (x: Int, y: Int) -> Unit,
43     /** Callback reporting the a single tap gesture was detected at the given coordinates. */
44     private val onSingleTapDetected: (x: Int, y: Int) -> Unit,
45     /** Callback reporting that a double tap gesture was detected. */
46     private val onDoubleTapDetected: () -> Unit,
47     /** Time for the touch to be considered a long-press in ms */
48     var longPressDuration: () -> Long,
49     /**
50      * Default touch slop that is allowed, if the movement between [MotionEventModel.Down] and
51      * [MotionEventModel.Up] is more than [allowedTouchSlop] then the touch is not processed as
52      * single tap or a long press.
53      */
54     val allowedTouchSlop: Int,
55     /** Optional logger that can be passed in to log touch events */
56     val logger: TouchHandlingViewLogger? = null,
57 ) {
58     sealed class MotionEventModel {
59         object Other : MotionEventModel()
60 
61         data class Down(val x: Int, val y: Int) : MotionEventModel()
62 
63         data class Move(val distanceMoved: Float) : MotionEventModel()
64 
65         data class Up(val distanceMoved: Float, val gestureDuration: Long) : MotionEventModel()
66 
67         object Cancel : MotionEventModel()
68     }
69 
70     var isLongPressHandlingEnabled: Boolean = false
71     var isDoubleTapHandlingEnabled: Boolean = false
72     var scheduledLongPressHandle: DisposableHandle? = null
73 
74     private var doubleTapAwaitingUp: Boolean = false
75     private var lastDoubleTapDownEventTime: Long? = null
76 
77     /** Record coordinate for last DOWN event for single tap */
78     val lastEventDownCoordinate = Point(-1, -1)
79 
80     private val gestureDetector =
81         GestureDetector(
82             context,
83             object : GestureDetector.SimpleOnGestureListener() {
84                 override fun onDoubleTap(event: MotionEvent): Boolean {
85                     if (isDoubleTapHandlingEnabled) {
86                         doubleTapAwaitingUp = true
87                         lastDoubleTapDownEventTime = event.eventTime
88                         return true
89                     }
90                     return false
91                 }
92             },
93         )
94 
95     fun onTouchEvent(event: MotionEvent): Boolean {
96         if (isDoubleTapHandlingEnabled) {
97             gestureDetector.onTouchEvent(event)
98             if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) {
99                 lastDoubleTapDownEventTime?.let { time ->
100                     if (
101                         event.eventTime - time < ViewConfiguration.getDoubleTapTimeout()
102                     ) {
103                         cancelScheduledLongPress()
104                         onDoubleTapDetected()
105                     }
106                 }
107                 doubleTapAwaitingUp = false
108             } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) {
109                 doubleTapAwaitingUp = false
110             }
111         }
112 
113         if (isLongPressHandlingEnabled) {
114             val motionEventModel = event.toModel()
115 
116             return when (motionEventModel) {
117                 is MotionEventModel.Down -> {
118                     scheduleLongPress(motionEventModel.x, motionEventModel.y)
119                     lastEventDownCoordinate.x = motionEventModel.x
120                     lastEventDownCoordinate.y = motionEventModel.y
121                     true
122                 }
123 
124                 is MotionEventModel.Move -> {
125                     if (motionEventModel.distanceMoved > allowedTouchSlop) {
126                         logger?.cancelingLongPressDueToTouchSlop(
127                             motionEventModel.distanceMoved,
128                             allowedTouchSlop,
129                         )
130                         cancelScheduledLongPress()
131                     }
132                     false
133                 }
134 
135                 is MotionEventModel.Up -> {
136                     logger?.onUpEvent(
137                         motionEventModel.distanceMoved,
138                         allowedTouchSlop,
139                         motionEventModel.gestureDuration,
140                     )
141                     cancelScheduledLongPress()
142                     if (
143                         motionEventModel.distanceMoved <= allowedTouchSlop &&
144                             motionEventModel.gestureDuration < longPressDuration()
145                     ) {
146                         logger?.dispatchingSingleTap()
147                         dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
148                     }
149                     false
150                 }
151 
152                 is MotionEventModel.Cancel -> {
153                     logger?.motionEventCancelled()
154                     cancelScheduledLongPress()
155                     false
156                 }
157 
158                 else -> false
159             }
160         }
161 
162         return false
163     }
164 
165     private fun scheduleLongPress(x: Int, y: Int) {
166         val duration = longPressDuration()
167         logger?.schedulingLongPress(duration)
168         scheduledLongPressHandle =
169             postDelayed(
170                 {
171                     logger?.longPressTriggered()
172                     dispatchLongPress(x = x, y = y)
173                 },
174                 duration,
175             )
176     }
177 
178     private fun dispatchLongPress(x: Int, y: Int) {
179         if (!isAttachedToWindow()) {
180             return
181         }
182 
183         onLongPressDetected(x, y)
184     }
185 
186     private fun cancelScheduledLongPress() {
187         scheduledLongPressHandle?.dispose()
188     }
189 
190     private fun dispatchSingleTap(x: Int, y: Int) {
191         if (!isAttachedToWindow()) {
192             return
193         }
194 
195         onSingleTapDetected(x, y)
196     }
197 
198     private fun MotionEvent.toModel(): MotionEventModel {
199         return when (actionMasked) {
200             MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt())
201             MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved())
202             MotionEvent.ACTION_UP ->
203                 MotionEventModel.Up(
204                     distanceMoved = distanceMoved(),
205                     gestureDuration = gestureDuration(),
206                 )
207             MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel
208             else -> MotionEventModel.Other
209         }
210     }
211 
212     private fun MotionEvent.distanceMoved(): Float {
213         return if (historySize > 0) {
214             sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
215         } else {
216             0f
217         }
218     }
219 
220     private fun MotionEvent.gestureDuration(): Long {
221         return eventTime - downTime
222     }
223 }
224