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

Commit 84d543d5 authored by Mike Schneider's avatar Mike Schneider Committed by Android (Google) Code Review
Browse files

Merge "Initial commit for #MotionMechanics library" into main

parents 249787c2 d72c58f7
Loading
Loading
Loading
Loading

mechanics/Android.bp

0 → 100644
+40 −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 {
    default_team: "trendy_team_motion",
    default_applicable_licenses: ["Android-Apache-2.0"],
}

filegroup {
    name: "mechanics-srcs",
    srcs: [
        "src/**/*.kt",
    ],
}

android_library {
    name: "mechanics",
    manifest: "AndroidManifest.xml",
    sdk_version: "system_current",
    min_sdk_version: "current",
    static_libs: [
        "androidx.compose.runtime_runtime",
        "androidx.compose.ui_ui-util",
    ],
    srcs: [
        ":mechanics-srcs",
    ],
    kotlincflags: ["-Xjvm-default=all"],
}
+19 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
     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.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.mechanics">
</manifest>

mechanics/TEST_MAPPING

0 → 100644
+7 −0
Original line number Diff line number Diff line
{
  "postsubmit": [
    {
      "name": "mechanics_tests"
    }
  ]
}
+78 −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.mechanics.spring

import androidx.compose.ui.util.lerp
import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2
import kotlin.math.pow

/**
 * Describes the parameters of a spring.
 *
 * Note: This is conceptually compatible with the Compose [SpringSpec]. In contrast to the compose
 * implementation, these [SpringParameters] are intended to be continuously updated.
 *
 * @see SpringParameters function to create this value.
 */
@JvmInline
value class SpringParameters(private val packedValue: Long) {
    val stiffness: Float
        get() = unpackFloat1(packedValue)

    val dampingRatio: Float
        get() = unpackFloat2(packedValue)

    /** Whether the spring is expected to immediately end movement. */
    val isSnapSpring: Boolean
        get() = stiffness >= snapStiffness && dampingRatio == snapDamping

    override fun toString(): String {
        return "MechanicsSpringSpec(stiffness=$stiffness, dampingRatio=$dampingRatio)"
    }

    companion object {
        private val snapStiffness = 100_000f
        private val snapDamping = 1f

        /** A spring so stiff it completes the motion almost immediately. */
        val Snap = SpringParameters(snapStiffness, snapDamping)
    }
}

/** Creates a [SpringParameters] with the given [stiffness] and [dampingRatio]. */
fun SpringParameters(stiffness: Float, dampingRatio: Float): SpringParameters {
    require(stiffness > 0) { "Spring stiffness constant must be positive." }
    require(dampingRatio >= 0) { "Spring damping constant must be positive." }
    return SpringParameters(packFloats(stiffness, dampingRatio))
}

/**
 * Return interpolated [SpringParameters], based on the [fraction] between [start] and [stop].
 *
 * The [SpringParameters.dampingRatio] is interpolated linearly, the [SpringParameters.stiffness] is
 * interpolated logarithmically.
 *
 * The [fraction] is clamped to a `0..1` range.
 */
fun lerp(start: SpringParameters, stop: SpringParameters, fraction: Float): SpringParameters {
    val f = fraction.coerceIn(0f, 1f)
    val stiffness = start.stiffness.pow(1 - f) * stop.stiffness.pow(f)
    val dampingRatio = lerp(start.dampingRatio, stop.dampingRatio, f)
    return SpringParameters(packFloats(stiffness, dampingRatio))
}
+129 −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.mechanics.spring

import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.sin
import kotlin.math.sqrt

/**
 * Describes the motion state of a spring.
 *
 * @see calculateUpdatedState to simulate the springs movement
 * @see SpringState function to create this value.
 */
@JvmInline
value class SpringState(private val packedValue: Long) {
    val displacement: Float
        get() = unpackFloat1(packedValue)

    val velocity: Float
        get() = unpackFloat2(packedValue)

    /**
     * Whether the state is considered stable.
     *
     * The amplitude of the remaining movement, for a spring with [parameters] is less than
     * [stableThreshold]
     */
    fun isStable(parameters: SpringParameters, stableThreshold: Float): Boolean {
        if (this == AtRest) return true
        val currentEnergy = parameters.stiffness * displacement * displacement + velocity * velocity
        val maxStableEnergy = parameters.stiffness * stableThreshold * stableThreshold
        return currentEnergy <= maxStableEnergy
    }

    override fun toString(): String {
        return "MechanicsSpringState(displacement=$displacement, velocity=$velocity)"
    }

    companion object {
        /** Spring at rest. */
        val AtRest = SpringState(displacement = 0f, velocity = 0f)
    }
}

/** Creates a [SpringState] given [displacement] and [velocity] */
fun SpringState(displacement: Float, velocity: Float = 0f) =
    SpringState(packFloats(displacement, velocity))

/**
 * Computes the updated [SpringState], after letting the spring with the specified [parameters]
 * settle for [elapsedNanos].
 *
 * This implementation is based on Compose's [SpringSimulation].
 */
fun SpringState.calculateUpdatedState(
    elapsedNanos: Long,
    parameters: SpringParameters,
): SpringState {
    if (parameters.isSnapSpring || this == SpringState.AtRest) {
        return SpringState.AtRest
    }

    val stiffness = parameters.stiffness.toDouble()
    val naturalFreq = sqrt(stiffness)

    val dampingRatio = parameters.dampingRatio
    val displacement = displacement
    val velocity = velocity
    val deltaT = elapsedNanos / 1_000_000_000.0 // unit: seconds
    val dampingRatioSquared = dampingRatio * dampingRatio.toDouble()
    val r = -dampingRatio * naturalFreq

    val currentDisplacement: Double
    val currentVelocity: Double

    if (dampingRatio > 1) {
        // Over damping
        val s = naturalFreq * sqrt(dampingRatioSquared - 1)
        val gammaPlus = r + s
        val gammaMinus = r - s

        val coeffB = (gammaMinus * displacement - velocity) / (gammaMinus - gammaPlus)
        val coeffA = displacement - coeffB
        currentDisplacement = (coeffA * exp(gammaMinus * deltaT) + coeffB * exp(gammaPlus * deltaT))
        currentVelocity =
            (coeffA * gammaMinus * exp(gammaMinus * deltaT) +
                coeffB * gammaPlus * exp(gammaPlus * deltaT))
    } else if (dampingRatio == 1.0f) {
        // Critically damped
        val coeffA = displacement
        val coeffB = velocity + naturalFreq * displacement
        val nFdT = -naturalFreq * deltaT
        currentDisplacement = (coeffA + coeffB * deltaT) * exp(nFdT)
        currentVelocity =
            (((coeffA + coeffB * deltaT) * exp(nFdT) * (-naturalFreq)) + coeffB * exp(nFdT))
    } else {
        // Underdamped
        val dampedFreq = naturalFreq * sqrt(1 - dampingRatioSquared)
        val cosCoeff = displacement
        val sinCoeff = ((1 / dampedFreq) * (((-r * displacement) + velocity)))
        val dFdT = dampedFreq * deltaT
        currentDisplacement = (exp(r * deltaT) * ((cosCoeff * cos(dFdT) + sinCoeff * sin(dFdT))))
        currentVelocity =
            (currentDisplacement * r +
                (exp(r * deltaT) *
                    ((-dampedFreq * cosCoeff * sin(dFdT) + dampedFreq * sinCoeff * cos(dFdT)))))
    }

    return SpringState(currentDisplacement.toFloat(), currentVelocity.toFloat())
}
Loading