-
-
Notifications
You must be signed in to change notification settings - Fork 22
Description
I was thinking about changing gesture handling from pointerInput to pointerInteropFilter.
pointerInput: https://developer.android.com/develop/ui/compose/touch-input/pointer-inputpointerInteropFilter: https://developer.android.com/reference/kotlin/androidx/compose/ui/input/pointer/pointerinteropfilter
pointerInput is the Compose pointer system and it doesn't forward Android MotionEvent objects to lower layers of the app. Because of that, stylus/pen MotionEvent handling in my renderer must be proxied through a MutableSharedFlow today, which feels like the wrong abstraction:
-
EditorGestureReceiver.kt (current
pointerInputusage):
notable/app/src/main/java/com/ethran/notable/editor/ui/EditorGestureReceiver.kt
Lines 63 to 68 in 53a1ec6
Box( modifier = Modifier // TODO: Change to // .pointerInteropFilter { ev -> ……} // for now it consumes all gestures - even stylus one. .pointerInput(Unit) { awaitEachGesture { -
MutableSharedFlowused to pass events toDrawCanvas:
var eraserTouchPoint = MutableSharedFlow<Offset?>() //TODO: replace with proper solution -
Current event forwarding in
EditorGestureReceiver:
notable/app/src/main/java/com/ethran/notable/editor/ui/EditorGestureReceiver.kt
Lines 77 to 89 in 53a1ec6
// TODO: It's only temporary workaround. // Track all moves until the stylus is lifted do { val event = awaitPointerEvent() val stylus = event.changes.firstOrNull { it.type == PointerType.Stylus } stylus?.let { coroutineScope.launch { DrawCanvas.eraserTouchPoint.emit(it.position) } it.consume() } } while (stylus?.pressed == true)
I would like to handle drawing-related MotionEvent (stylus/pen) directly in the rendering class, something like what the OpenGL renderer currently expects:
OpenGLRenderer.kt(desired target for rawMotionEventhandling):
notable/app/src/main/java/com/ethran/notable/editor/drawing/OpenGLRenderer.kt
Lines 182 to 228 in 53a1ec6
// THIS DOES NOT GET ANY EVENTS, see EditorGestureReceiver. @SuppressLint("ClickableViewAccessibility") val onTouchListener = View.OnTouchListener { view, event -> val point = getStrokePoint(event) motionEventPredictor?.record(event) if (event.getToolType(0) != MotionEvent.TOOL_TYPE_STYLUS) return@OnTouchListener true Log.d("MotionEvent", event.toString()) when (event?.action) { MotionEvent.ACTION_DOWN -> { // Ask that the input system not batch MotionEvents // but instead deliver them as soon as they're available view.requestUnbufferedDispatch(event) frontBufferRenderer?.renderFrontBufferedLayer(point) } MotionEvent.ACTION_MOVE -> { previousX = currentX previousY = currentY currentX = event.x currentY = event.y // Send the short line to front buffered layer: fast rendering frontBufferRenderer?.renderFrontBufferedLayer(point) } MotionEvent.ACTION_UP -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && (event.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED ) { frontBufferRenderer?.cancel() } else { frontBufferRenderer?.commit() } } MotionEvent.ACTION_CANCEL -> { frontBufferRenderer?.cancel() } } true } }
The pointerInteropFilter modifier receives Android MotionEvent and returns a Boolean indicating whether the event was consumed; by returning false you can allow the event to continue to lower-level handlers. That should make it possible to let the renderer (or an Android view) receive raw MotionEvent objects without going through a MutableSharedFlow proxy.
I don't have much Kotlin experience, so if someone knows more about Compose pointer handling (pointerInput vs pointerInteropFilter or maybe there is something even better in this situation) I'd appreciate any guidance. I don't want to start a large redesign unless it's actually the better approach.