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

Commit 68bbaee0 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introducing the FVF model in motion mechanics." into main

parents 3e7be5b4 53ad621b
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import com.android.mechanics.haptics.HapticPlayer
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.rememberMotionBuilderContext
@@ -33,15 +34,17 @@ fun rememberMotionValue(
    spec: () -> MotionSpec,
    label: String? = null,
    stableThreshold: Float = 0.01f,
    hapticPlayer: HapticPlayer = HapticPlayer.NoPlayer,
): MotionValue {
    val motionValue =
        remember(input) {
        remember(input, hapticPlayer) {
            MotionValue(
                input = input,
                gestureContext = gestureContext,
                spec = spec,
                label = label,
                stableThreshold = stableThreshold,
                hapticPlayer = hapticPlayer,
            )
        }

@@ -56,6 +59,7 @@ fun rememberMotionValue(
    spec: State<MotionSpec>,
    label: String? = null,
    stableThreshold: Float = 0.01f,
    hapticPlayer: HapticPlayer = HapticPlayer.NoPlayer,
): MotionValue {
    return rememberMotionValue(
        input = input,
@@ -63,6 +67,7 @@ fun rememberMotionValue(
        spec = spec::value,
        label = label,
        stableThreshold = stableThreshold,
        hapticPlayer = hapticPlayer,
    )
}

+39 −0
Original line number Diff line number Diff line
@@ -29,6 +29,9 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameNanos
import com.android.mechanics.debug.DebugInspector
import com.android.mechanics.debug.FrameData
import com.android.mechanics.haptics.BreakpointHaptics
import com.android.mechanics.haptics.HapticPlayer
import com.android.mechanics.haptics.SegmentHaptics
import com.android.mechanics.impl.Computations
import com.android.mechanics.impl.DiscontinuityAnimation
import com.android.mechanics.impl.GuaranteeState
@@ -122,6 +125,8 @@ import kotlinx.coroutines.withContext
 * @param label An optional label to aid in debugging.
 * @param stableThreshold A threshold value (in output units) that determines when the
 *   [MotionValue]'s internal spring animation is considered stable.
 * @param hapticPlayer When specifying segment and breakpoint haptics, this player will be used to
 *   deliver haptic feedback.
 */
class MotionValue(
    input: () -> Float,
@@ -129,6 +134,7 @@ class MotionValue(
    spec: () -> MotionSpec,
    label: String? = null,
    stableThreshold: Float = StableThresholdEffect,
    hapticPlayer: HapticPlayer = HapticPlayer.NoPlayer,
) : MotionValueState {
    private val impl =
        ObservableComputations(
@@ -137,6 +143,7 @@ class MotionValue(
            specProvider = spec,
            stableThreshold = stableThreshold,
            label = label,
            hapticPlayer = hapticPlayer,
        )

    /** The [MotionSpec] describing the mapping of this [MotionValue]'s input to the output. */
@@ -291,6 +298,7 @@ private class ObservableComputations(
    private val specProvider: () -> MotionSpec,
    override val stableThreshold: Float,
    override val label: String?,
    private val hapticPlayer: HapticPlayer,
) : Computations() {

    // ----  CurrentFrameInput ---------------------------------------------------------------------
@@ -309,6 +317,8 @@ private class ObservableComputations(

    override var currentAnimationTimeNanos by mutableLongStateOf(-1L)

    override var lastHapticsTimeNanos by mutableLongStateOf(-1L)

    // ----  LastFrameState ---------------------------------------------------------------------

    override var lastSegment: SegmentData by
@@ -406,12 +416,14 @@ private class ObservableComputations(
                }

                var scheduleNextFrame = false
                var breakpointHaptics: BreakpointHaptics? = null
                if (!isSameSegmentAndAtRest) {
                    // Read currentComputedValues only once and update it, if necessary
                    val currentValues = currentComputedValues

                    if (capturedSegment != currentValues.segment) {
                        capturedSegment = currentValues.segment
                        breakpointHaptics = currentValues.breakpointHaptics
                        scheduleNextFrame = true
                    }

@@ -446,6 +458,13 @@ private class ObservableComputations(
                    scheduleNextFrame = true
                }

                // Perform haptics
                if (breakpointHaptics != null) {
                    performBreakpointHapticFeedback(breakpointHaptics)
                } else {
                    performSegmentHapticFeedback(capturedSegment.haptics)
                }

                capturedFrameTimeNanos = currentAnimationTimeNanos

                debugInspector?.run {
@@ -504,4 +523,24 @@ private class ObservableComputations(
        }

    var debugInspector: DebugInspector? = null

    private fun performSegmentHapticFeedback(segmentHaptics: SegmentHaptics) {
        val timeDelta = currentAnimationTimeNanos - lastHapticsTimeNanos
        if (timeDelta < hapticPlayer.getPlaybackIntervalNanos()) return

        val spatialInputPx = computedOutput
        val velocityPxPerSec = directMappedVelocity // we assume this is always in px/sec.
        lastHapticsTimeNanos = currentAnimationTimeNanos
        hapticPlayer.playSegmentHaptics(segmentHaptics, spatialInputPx, velocityPxPerSec)
    }

    private fun performBreakpointHapticFeedback(breakpointHaptics: BreakpointHaptics) {
        val timeDelta = currentAnimationTimeNanos - lastHapticsTimeNanos
        if (timeDelta < hapticPlayer.getPlaybackIntervalNanos()) return

        val spatialInputPx = computedOutput
        val velocityPxPerSec = directMappedVelocity // we assume this is always in px/sec.
        lastHapticsTimeNanos = currentAnimationTimeNanos
        hapticPlayer.playBreakpointHaptics(breakpointHaptics, spatialInputPx, velocityPxPerSec)
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.mechanics
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.setValue
@@ -330,6 +331,8 @@ internal class ManagedMotionComputation(
    override val lastGestureDragOffset
        get() = owner.lastGestureDragOffset

    override var lastHapticsTimeNanos: Long by mutableLongStateOf(-1L)

    // ---- Computations ---------------------------------------------------------------------------

    var debugInspector: DebugInspector? = null
+27 −1
Original line number Diff line number Diff line
@@ -21,6 +21,9 @@ package com.android.mechanics.effects
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.mechanics.haptics.BreakpointHaptics
import com.android.mechanics.haptics.HapticsExperimentalApi
import com.android.mechanics.haptics.SegmentHaptics
import com.android.mechanics.spec.BreakpointKey
import com.android.mechanics.spec.ChangeSegmentHandlers.DirectionChangePreservesCurrentValue
import com.android.mechanics.spec.ChangeSegmentHandlers.PreventDirectionChangeWithinCurrentSegment
@@ -56,6 +59,7 @@ class MagneticDetach(
    private val attachScale: Float = Defaults.AttachDetachScale * (attachPosition / detachPosition),
    private val detachSpring: SpringParameters = Defaults.Spring,
    private val attachSpring: SpringParameters = Defaults.Spring,
    private val enableHaptics: Boolean = false,
) : Effect.PlaceableAfter, Effect.PlaceableBefore {

    init {
@@ -96,6 +100,7 @@ class MagneticDetach(
    }

    /* Effect is attached at minLimit, and detaches at maxLimit. */
    @OptIn(HapticsExperimentalApi::class)
    private fun EffectApplyScope.createPlacedAfterSpec(
        minLimit: Float,
        minLimitKey: BreakpointKey,
@@ -115,12 +120,32 @@ class MagneticDetach(
        val scaledDetachValue = attachedValue + (detachedValue - attachedValue) * detachScale
        val scaledReattachValue = attachedValue + (reattachValue - attachedValue) * attachScale

        // Haptic specs
        val tensionHaptics =
            if (enableHaptics) {
                SegmentHaptics.SpringTension(anchorPointPx = minLimit)
            } else {
                SegmentHaptics.None
            }
        val thresholdHaptics =
            if (enableHaptics) {
                BreakpointHaptics.GenericThreshold
            } else {
                BreakpointHaptics.None
            }

        val attachKey = BreakpointKey("attach")

        forward(
            initialMapping = Mapping.Linear(minLimit, attachedValue, maxLimit, scaledDetachValue),
            initialSegmentHaptics = tensionHaptics,
            semantics = attachedSemantics,
        ) {
            after(spring = detachSpring, semantics = detachedSemantics)
            after(
                spring = detachSpring,
                semantics = detachedSemantics,
                breakpointHaptics = thresholdHaptics,
            )
            before(semantics = listOf(semanticAttachedValue with null))
        }

@@ -135,6 +160,7 @@ class MagneticDetach(
                spring = attachSpring,
                semantics = detachedSemantics,
                mapping = baseMapping,
                breakpointHaptics = thresholdHaptics,
            )
            before(semantics = listOf(semanticAttachedValue with null))
            after(semantics = listOf(semanticAttachedValue with null))
+52 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.mechanics.haptics

interface HapticPlayer {

    fun playSegmentHaptics(
        segmentHaptics: SegmentHaptics,
        spatialInput: Float,
        spatialVelocity: Float,
    )

    fun playBreakpointHaptics(
        breakpointHaptics: BreakpointHaptics,
        spatialInput: Float,
        spatialVelocity: Float,
    )

    /** Get the minimum interval required for haptics to play */
    fun getPlaybackIntervalNanos(): Long = 0L

    companion object {
        val NoPlayer =
            object : HapticPlayer {
                override fun playSegmentHaptics(
                    segmentHaptics: SegmentHaptics,
                    spatialInput: Float,
                    spatialVelocity: Float,
                ) {}

                override fun playBreakpointHaptics(
                    breakpointHaptics: BreakpointHaptics,
                    spatialInput: Float,
                    spatialVelocity: Float,
                ) {}
            }
    }
}
Loading