Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8a22cd58 authored by Michal Brzezinski's avatar Michal Brzezinski
Browse files

Refactoring gesture recognition logic

1/n of introducing gesture live progress tracking

Main motivation for refactoring is making gesture recognition more composition-friendly and extendable, currently ThreeFingerDistanceBasedGestureMonitor is counter-example of that. Goal is to easily filter by number of gestures and react on distance changes and - in the future - to velocity at the end. And to be able to have any combination of that.
Also I think the new code is easier to understand because we avoid inheritance.

Incoming further changes:
- Rename *GestureMonitor to *GestureRecognizer
- Refactor Recent apps gesture logic to follow similar design of compositing features

Bug: 369817369
Test: All tests are passing
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Change-Id: I690b8a95d93a4a18f41ade98e4b214d14153cd6a
parent 88dcc78c
Loading
Loading
Loading
Loading
+17 −19
Original line number Diff line number Diff line
@@ -16,26 +16,24 @@

package com.android.systemui.touchpad.tutorial.ui.gesture

import android.view.MotionEvent
import kotlin.math.abs

/** Monitors for touchpad back gesture, that is three fingers swiping left or right */
class BackGestureMonitor(
    override val gestureDistanceThresholdPx: Int,
    override val gestureStateChangedCallback: (GestureState) -> Unit
) :
    TouchpadGestureMonitor by ThreeFingerDistanceBasedGestureMonitor(
        gestureDistanceThresholdPx = gestureDistanceThresholdPx,
        gestureStateChangedCallback = gestureStateChangedCallback,
        donePredicate =
            object : GestureDonePredicate {
                override fun wasGestureDone(
                    startX: Float,
                    startY: Float,
                    endX: Float,
                    endY: Float
                ): Boolean {
                    val distance = abs(endX - startX)
                    return distance >= gestureDistanceThresholdPx
    private val gestureDistanceThresholdPx: Int,
    override val gestureStateChangedCallback: (GestureState) -> Unit,
) : TouchpadGestureMonitor {
    private val distanceTracker = DistanceTracker()

    override fun processTouchpadEvent(event: MotionEvent) {
        if (!isThreeFingerTouchpadSwipe(event)) return
        val distanceState = distanceTracker.processEvent(event)
        updateGestureStateBasedOnDistance(
            gestureStateChangedCallback,
            distanceState,
            isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
            progress = { 0f },
        )
    }
}
    )
+47 −0
Original line number Diff line number Diff line
@@ -18,46 +18,30 @@ package com.android.systemui.touchpad.tutorial.ui.gesture

import android.view.MotionEvent

interface GestureDonePredicate {
/**
     * Should return if gesture was finished. The only events this predicate receives are ACTION_UP.
 * Tracks distance change for processed MotionEvents. Useful for recognizing gestures based on
 * distance travelled instead of specific position on the screen.
 */
    fun wasGestureDone(startX: Float, startY: Float, endX: Float, endY: Float): Boolean
}

/**
 * Common implementation for three-finger gesture monitors that are only distance-based. E.g. recent
 * apps gesture is not only distance-based because it requires going over threshold distance and
 * slowing down the movement.
 */
class ThreeFingerDistanceBasedGestureMonitor(
    override val gestureDistanceThresholdPx: Int,
    override val gestureStateChangedCallback: (GestureState) -> Unit,
    private val donePredicate: GestureDonePredicate
) : TouchpadGestureMonitor {

    private var xStart = 0f
    private var yStart = 0f

    override fun processTouchpadEvent(event: MotionEvent) {
class DistanceTracker(var startX: Float = 0f, var startY: Float = 0f) {
    fun processEvent(event: MotionEvent): DistanceGestureState? {
        val action = event.actionMasked
        when (action) {
        return when (action) {
            MotionEvent.ACTION_DOWN -> {
                if (isThreeFingerTouchpadSwipe(event)) {
                    xStart = event.x
                    yStart = event.y
                    gestureStateChangedCallback(GestureState.InProgress())
                }
            }
            MotionEvent.ACTION_UP -> {
                if (isThreeFingerTouchpadSwipe(event)) {
                    if (donePredicate.wasGestureDone(xStart, yStart, event.x, event.y)) {
                        gestureStateChangedCallback(GestureState.Finished)
                    } else {
                        gestureStateChangedCallback(GestureState.NotStarted)
                    }
                }
                startX = event.x
                startY = event.y
                Started(event.x, event.y)
            }
            MotionEvent.ACTION_MOVE -> Moving(event.x - startX, event.y - startY)
            MotionEvent.ACTION_UP -> Finished(event.x - startX, event.y - startY)
            else -> null
        }
    }
}

sealed interface DistanceGestureState

class Started(val deltaX: Float, val deltaY: Float) : DistanceGestureState

class Moving(val deltaX: Float, val deltaY: Float) : DistanceGestureState

class Finished(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+43 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.touchpad.tutorial.ui.gesture

/**
 * Helper function for gesture recognizers to have common state triggering logic based on distance
 * only.
 */
inline fun updateGestureStateBasedOnDistance(
    gestureStateChangedCallback: (GestureState) -> Unit,
    gestureState: DistanceGestureState?,
    isFinished: (Finished) -> Boolean,
    progress: (Moving) -> Float,
) {
    when (gestureState) {
        is Finished -> {
            if (isFinished(gestureState)) {
                gestureStateChangedCallback(GestureState.Finished)
            } else {
                gestureStateChangedCallback(GestureState.NotStarted)
            }
        }
        is Moving -> {
            gestureStateChangedCallback(GestureState.InProgress(progress(gestureState)))
        }
        is Started -> gestureStateChangedCallback(GestureState.InProgress())
        else -> {}
    }
}
+18 −19
Original line number Diff line number Diff line
@@ -16,24 +16,23 @@

package com.android.systemui.touchpad.tutorial.ui.gesture

import android.view.MotionEvent

/** Monitors for touchpad home gesture, that is three fingers swiping up */
class HomeGestureMonitor(
    override val gestureDistanceThresholdPx: Int,
    override val gestureStateChangedCallback: (GestureState) -> Unit
) :
    TouchpadGestureMonitor by ThreeFingerDistanceBasedGestureMonitor(
        gestureDistanceThresholdPx = gestureDistanceThresholdPx,
        gestureStateChangedCallback = gestureStateChangedCallback,
        donePredicate =
            object : GestureDonePredicate {
                override fun wasGestureDone(
                    startX: Float,
                    startY: Float,
                    endX: Float,
                    endY: Float
                ): Boolean {
                    val distance = startY - endY
                    return distance >= gestureDistanceThresholdPx
    private val gestureDistanceThresholdPx: Int,
    override val gestureStateChangedCallback: (GestureState) -> Unit,
) : TouchpadGestureMonitor {
    private val distanceTracker = DistanceTracker()

    override fun processTouchpadEvent(event: MotionEvent) {
        if (!isThreeFingerTouchpadSwipe(event)) return
        val distanceState = distanceTracker.processEvent(event)
        updateGestureStateBasedOnDistance(
            gestureStateChangedCallback,
            distanceState,
            isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
            progress = { 0f },
        )
    }
}
    )
+1 −1
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@ import kotlin.math.abs
 * is based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker]
 */
class RecentAppsGestureMonitor(
    override val gestureDistanceThresholdPx: Int,
    private val gestureDistanceThresholdPx: Int,
    override val gestureStateChangedCallback: (GestureState) -> Unit,
    private val velocityThresholdPxPerMs: Float,
    private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false),
Loading