1 /* <lambda>null2 * Copyright 2019 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 androidx.compose.ui.input.pointer 18 19 import android.os.Build 20 import android.util.SparseBooleanArray 21 import android.util.SparseLongArray 22 import android.view.MotionEvent 23 import android.view.MotionEvent.ACTION_CANCEL 24 import android.view.MotionEvent.ACTION_DOWN 25 import android.view.MotionEvent.ACTION_HOVER_ENTER 26 import android.view.MotionEvent.ACTION_HOVER_EXIT 27 import android.view.MotionEvent.ACTION_HOVER_MOVE 28 import android.view.MotionEvent.ACTION_OUTSIDE 29 import android.view.MotionEvent.ACTION_POINTER_DOWN 30 import android.view.MotionEvent.ACTION_POINTER_UP 31 import android.view.MotionEvent.ACTION_SCROLL 32 import android.view.MotionEvent.ACTION_UP 33 import android.view.MotionEvent.TOOL_TYPE_ERASER 34 import android.view.MotionEvent.TOOL_TYPE_FINGER 35 import android.view.MotionEvent.TOOL_TYPE_MOUSE 36 import android.view.MotionEvent.TOOL_TYPE_STYLUS 37 import android.view.MotionEvent.TOOL_TYPE_UNKNOWN 38 import androidx.annotation.RequiresApi 39 import androidx.annotation.VisibleForTesting 40 import androidx.compose.ui.geometry.Offset 41 import androidx.compose.ui.util.fastIsFinite 42 43 /** Converts Android framework [MotionEvent]s into Compose [PointerInputEvent]s. */ 44 internal class MotionEventAdapter { 45 46 private var nextId = 0L 47 48 /** 49 * Whenever a new MotionEvent pointer is added, we create a new PointerId that is associated 50 * with it. This holds that association. 51 */ 52 @VisibleForTesting internal val motionEventToComposePointerIdMap = SparseLongArray() 53 54 private val activeHoverIds = SparseBooleanArray() 55 56 private val pointers = mutableListOf<PointerInputEventData>() 57 58 /** 59 * The previous event's tool type. This is used in combination with [previousSource] to 60 * determine when a different device was used to send events. 61 */ 62 private var previousToolType = -1 63 64 /** 65 * The previous event's source. This is used in combination with [previousToolType] to determine 66 * when a different device was used to send events. 67 */ 68 private var previousSource = -1 69 70 /** 71 * Converts a single [MotionEvent] from an Android event stream into a [PointerInputEvent], or 72 * null if the [MotionEvent.getActionMasked] is [ACTION_CANCEL]. 73 * 74 * All MotionEvents should be passed to this method so that it can correctly maintain it's 75 * internal state. 76 * 77 * @param motionEvent The MotionEvent to process. 78 * @return The PointerInputEvent or null if the event action was ACTION_CANCEL. 79 */ 80 internal fun convertToPointerInputEvent( 81 motionEvent: MotionEvent, 82 positionCalculator: PositionCalculator 83 ): PointerInputEvent? { 84 val action = motionEvent.actionMasked 85 if (action == ACTION_CANCEL || action == ACTION_OUTSIDE) { 86 motionEventToComposePointerIdMap.clear() 87 activeHoverIds.clear() 88 return null 89 } 90 clearOnDeviceChange(motionEvent) 91 92 addFreshIds(motionEvent) 93 94 val isHover = 95 action == ACTION_HOVER_ENTER || 96 action == ACTION_HOVER_MOVE || 97 action == ACTION_HOVER_EXIT 98 99 val isScroll = action == ACTION_SCROLL 100 101 if (isHover) { 102 val hoverId = motionEvent.getPointerId(motionEvent.actionIndex) 103 activeHoverIds.put(hoverId, true) 104 } 105 106 val upIndex = 107 when (action) { 108 ACTION_UP -> 0 109 ACTION_POINTER_UP -> motionEvent.actionIndex 110 else -> -1 111 } 112 113 pointers.clear() 114 115 // This converts the MotionEvent into a list of PointerInputEventData, and updates 116 // internal record keeping. 117 for (i in 0 until motionEvent.pointerCount) { 118 pointers.add( 119 createPointerInputEventData( 120 positionCalculator, 121 motionEvent, 122 i, 123 // "pressed" means: 124 // 1. we're not hovered 125 // 2. we didn't get UP event for a pointer 126 // 3. button on the mouse is pressed BUT it's not a "scroll" simulated button 127 !isHover && i != upIndex && (!isScroll || motionEvent.buttonState != 0) 128 ) 129 ) 130 } 131 132 removeStaleIds(motionEvent) 133 134 return PointerInputEvent(motionEvent.eventTime, pointers, motionEvent) 135 } 136 137 /** 138 * An ACTION_DOWN or ACTION_POINTER_DOWN was received, but not handled, so the stream should be 139 * considered ended. 140 */ 141 fun endStream(pointerId: Int) { 142 activeHoverIds.delete(pointerId) 143 motionEventToComposePointerIdMap.delete(pointerId) 144 } 145 146 /** Add any new pointer IDs. */ 147 private fun addFreshIds(motionEvent: MotionEvent) { 148 when (motionEvent.actionMasked) { 149 ACTION_HOVER_ENTER -> { 150 val pointerId = motionEvent.getPointerId(0) 151 if (motionEventToComposePointerIdMap.indexOfKey(pointerId) < 0) { 152 motionEventToComposePointerIdMap.put(pointerId, nextId++) 153 } 154 } 155 ACTION_DOWN, 156 ACTION_POINTER_DOWN -> { 157 val actionIndex = motionEvent.actionIndex 158 val pointerId = motionEvent.getPointerId(actionIndex) 159 if (motionEventToComposePointerIdMap.indexOfKey(pointerId) < 0) { 160 motionEventToComposePointerIdMap.put(pointerId, nextId++) 161 if (motionEvent.getToolType(actionIndex) == TOOL_TYPE_MOUSE) { 162 activeHoverIds.put(pointerId, true) 163 } 164 } 165 } 166 } 167 } 168 169 /** 170 * Remove any raised pointers if they didn't previously hover. Anything that hovers will stay 171 * until a different event causes it to be removed. 172 */ 173 private fun removeStaleIds(motionEvent: MotionEvent) { 174 when (motionEvent.actionMasked) { 175 ACTION_POINTER_UP, 176 ACTION_UP -> { 177 val actionIndex = motionEvent.actionIndex 178 val pointerId = motionEvent.getPointerId(actionIndex) 179 if (!activeHoverIds.get(pointerId, false)) { 180 motionEventToComposePointerIdMap.delete(pointerId) 181 activeHoverIds.delete(pointerId) 182 } 183 } 184 } 185 186 // Remove any IDs that don't currently exist in the MotionEvent. 187 // This can happen, for example, when a mouse cursor disappears and the next 188 // event is a touch event. 189 if (motionEventToComposePointerIdMap.size() > motionEvent.pointerCount) { 190 for (i in motionEventToComposePointerIdMap.size() - 1 downTo 0) { 191 val pointerId = motionEventToComposePointerIdMap.keyAt(i) 192 if (!motionEvent.hasPointerId(pointerId)) { 193 motionEventToComposePointerIdMap.removeAt(i) 194 activeHoverIds.delete(pointerId) 195 } 196 } 197 } 198 } 199 200 private fun MotionEvent.hasPointerId(pointerId: Int): Boolean { 201 for (i in 0 until pointerCount) { 202 if (getPointerId(i) == pointerId) { 203 return true 204 } 205 } 206 return false 207 } 208 209 private fun getComposePointerId(motionEventPointerId: Int): PointerId { 210 val pointerIndex = motionEventToComposePointerIdMap.indexOfKey(motionEventPointerId) 211 val id = 212 if (pointerIndex >= 0) { 213 motionEventToComposePointerIdMap.valueAt(pointerIndex) 214 } else { 215 // An unexpected pointer was added or we may have previously removed it 216 val newId = nextId++ 217 motionEventToComposePointerIdMap.put(motionEventPointerId, newId) 218 newId 219 } 220 return PointerId(id) 221 } 222 223 /** 224 * When the device has changed (noted by source and tool type), we don't need to track any of 225 * the previous pointers. 226 */ 227 private fun clearOnDeviceChange(motionEvent: MotionEvent) { 228 if (motionEvent.pointerCount != 1) { 229 return 230 } 231 val toolType = motionEvent.getToolType(0) 232 val source = motionEvent.source 233 234 if (toolType != previousToolType || source != previousSource) { 235 previousToolType = toolType 236 previousSource = source 237 activeHoverIds.clear() 238 motionEventToComposePointerIdMap.clear() 239 } 240 } 241 242 /** Creates a new PointerInputEventData. */ 243 private fun createPointerInputEventData( 244 positionCalculator: PositionCalculator, 245 motionEvent: MotionEvent, 246 index: Int, 247 pressed: Boolean 248 ): PointerInputEventData { 249 250 val motionEventPointerId = motionEvent.getPointerId(index) 251 252 val pointerId = getComposePointerId(motionEventPointerId) 253 254 val pressure = motionEvent.getPressure(index) 255 256 var position = Offset(motionEvent.getX(index), motionEvent.getY(index)) 257 val originalPositionEventPosition = position.copy() 258 val rawPosition: Offset 259 if (index == 0) { 260 rawPosition = Offset(motionEvent.rawX, motionEvent.rawY) 261 position = positionCalculator.screenToLocal(rawPosition) 262 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 263 rawPosition = MotionEventHelper.toRawOffset(motionEvent, index) 264 position = positionCalculator.screenToLocal(rawPosition) 265 } else { 266 rawPosition = positionCalculator.localToScreen(position) 267 } 268 val toolType = 269 when (motionEvent.getToolType(index)) { 270 TOOL_TYPE_UNKNOWN -> PointerType.Unknown 271 TOOL_TYPE_FINGER -> PointerType.Touch 272 TOOL_TYPE_STYLUS -> PointerType.Stylus 273 TOOL_TYPE_MOUSE -> PointerType.Mouse 274 TOOL_TYPE_ERASER -> PointerType.Eraser 275 else -> PointerType.Unknown 276 } 277 278 val historical = ArrayList<HistoricalChange>(motionEvent.historySize) 279 with(motionEvent) { 280 repeat(historySize) { pos -> 281 val x = getHistoricalX(index, pos) 282 val y = getHistoricalY(index, pos) 283 if (x.fastIsFinite() && y.fastIsFinite()) { 284 val originalEventPosition = Offset(x, y) // hit path will convert to local 285 val historicalChange = 286 HistoricalChange( 287 getHistoricalEventTime(pos), 288 originalEventPosition, 289 originalEventPosition 290 ) 291 historical.add(historicalChange) 292 } 293 } 294 } 295 val scrollDelta = 296 if (motionEvent.actionMasked == ACTION_SCROLL) { 297 val x = motionEvent.getAxisValue(MotionEvent.AXIS_HSCROLL) 298 val y = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL) 299 // NOTE: we invert the y scroll offset because android is special compared to other 300 // platforms and uses the opposite sign for vertical mouse wheel scrolls. In order 301 // to 302 // support better x-platform mouse scroll, we invert the y-offset to be in line with 303 // desktop and web. 304 // 305 // This looks more natural, because when we scroll mouse wheel up, 306 // we move the wheel point (that touches the finger) up. And if we work in the usual 307 // coordinate system, it means we move that point by "-1". 308 // 309 // Web also behaves this way. See deltaY: 310 // https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event 311 // https://jsfiddle.net/27zwteog 312 // (wheelDelta on the other hand is deprecated and inverted) 313 // 314 // We then add 0f to prevent injecting -0.0f into the pipeline, which can be 315 // problematic when doing comparisons. 316 Offset(x, -y + 0f) 317 } else { 318 Offset.Zero 319 } 320 321 val activeHover = activeHoverIds.get(motionEvent.getPointerId(index), false) 322 return PointerInputEventData( 323 pointerId, 324 motionEvent.eventTime, 325 rawPosition, 326 position, 327 pressed, 328 pressure, 329 toolType, 330 activeHover, 331 historical, 332 scrollDelta, 333 originalPositionEventPosition, 334 ) 335 } 336 } 337 338 /** 339 * This class is here to ensure that the classes that use this API will get verified and can be AOT 340 * compiled. It is expected that this class will soft-fail verification, but the classes which use 341 * this method will pass. 342 */ 343 @RequiresApi(Build.VERSION_CODES.Q) 344 private object MotionEventHelper { toRawOffsetnull345 fun toRawOffset(motionEvent: MotionEvent, index: Int): Offset { 346 return Offset(motionEvent.getRawX(index), motionEvent.getRawY(index)) 347 } 348 } 349