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