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

Commit a8085ae0 authored by Mike Schneider's avatar Mike Schneider
Browse files

Add overdrag effect

Bug: 401500734
Test: Unit tests
Flag: EXEMPT not yet used in production
Change-Id: Ie4200a9f5122c329905feea73403b6b70b6b2cfa
parent 11fc2857
Loading
Loading
Loading
Loading
+69 −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.effects

import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.mechanics.spec.BreakpointKey
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spec.builder.Effect
import com.android.mechanics.spec.builder.EffectApplyScope
import com.android.mechanics.spec.builder.EffectPlacement
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.with

/** Gesture effect to soft-limit. */
class Overdrag(
    private val overdragLimit: SemanticKey<Float?> = Defaults.OverdragLimit,
    private val maxOverdrag: Dp = Defaults.MaxOverdrag,
    private val tilt: Float = Defaults.tilt,
) : Effect.PlaceableBefore, Effect.PlaceableAfter {

    override fun MotionBuilderContext.intrinsicSize() = Float.POSITIVE_INFINITY

    override fun EffectApplyScope.createSpec(
        minLimit: Float,
        minLimitKey: BreakpointKey,
        maxLimit: Float,
        maxLimitKey: BreakpointKey,
        placement: EffectPlacement,
    ) {

        val maxOverdragPx = maxOverdrag.toPx()

        val limitValue = baseValue(placement.start)
        val mapping = Mapping { input ->
            val baseMapped = baseMapping.map(input)

            maxOverdragPx * kotlin.math.tanh((baseMapped - limitValue) / (maxOverdragPx * tilt)) +
                limitValue
        }

        unidirectional(mapping, listOf(overdragLimit with limitValue)) {
            if (!placement.isForward) {
                after(semantics = listOf(overdragLimit with null))
            }
        }
    }

    object Defaults {
        val OverdragLimit = SemanticKey<Float?>()
        val MaxOverdrag = 30.dp
        val tilt = 3f
    }
}
+279 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    272,
    288,
    304,
    320
  ],
  "features": [
    {
      "name": "input",
      "type": "float",
      "data_points": [
        0,
        5,
        10,
        15,
        20,
        25,
        30,
        35,
        40,
        45,
        50,
        55,
        60,
        65,
        70,
        75,
        80,
        85,
        90,
        95,
        100
      ]
    },
    {
      "name": "gestureDirection",
      "type": "string",
      "data_points": [
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max"
      ]
    },
    {
      "name": "output",
      "type": "float",
      "data_points": [
        0,
        5,
        10,
        11.662819,
        13.302809,
        14.898373,
        16.430256,
        17.88237,
        19.242344,
        20.501678,
        21.655659,
        22.70298,
        23.645235,
        24.486334,
        25.231884,
        25.88864,
        26.464012,
        26.965673,
        27.401234,
        27.77803,
        28.102966
      ]
    },
    {
      "name": "outputTarget",
      "type": "float",
      "data_points": [
        0,
        5,
        10,
        11.662819,
        13.302809,
        14.898373,
        16.430256,
        17.88237,
        19.242344,
        20.501678,
        21.655659,
        22.70298,
        23.645235,
        24.486334,
        25.231884,
        25.88864,
        26.464012,
        26.965673,
        27.401234,
        27.77803,
        28.102966
      ]
    },
    {
      "name": "outputSpring",
      "type": "springParameters",
      "data_points": [
        {
          "stiffness": 100000,
          "dampingRatio": 1
        },
        {
          "stiffness": 100000,
          "dampingRatio": 1
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        }
      ]
    },
    {
      "name": "isStable",
      "type": "boolean",
      "data_points": [
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true
      ]
    },
    {
      "name": "overdragLimit",
      "type": "float",
      "data_points": [
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10,
        10
      ]
    }
  ]
}
 No newline at end of file
+279 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    272,
    288,
    304,
    320
  ],
  "features": [
    {
      "name": "input",
      "type": "float",
      "data_points": [
        0,
        -5,
        -10,
        -15,
        -20,
        -25,
        -30,
        -35,
        -40,
        -45,
        -50,
        -55,
        -60,
        -65,
        -70,
        -75,
        -80,
        -85,
        -90,
        -95,
        -100
      ]
    },
    {
      "name": "gestureDirection",
      "type": "string",
      "data_points": [
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min",
        "Min"
      ]
    },
    {
      "name": "output",
      "type": "float",
      "data_points": [
        0,
        -5,
        -10,
        -11.662819,
        -13.302809,
        -14.898373,
        -16.430256,
        -17.88237,
        -19.242344,
        -20.501678,
        -21.655659,
        -22.70298,
        -23.645235,
        -24.486334,
        -25.231884,
        -25.88864,
        -26.464012,
        -26.965673,
        -27.401234,
        -27.77803,
        -28.102966
      ]
    },
    {
      "name": "outputTarget",
      "type": "float",
      "data_points": [
        0,
        -5,
        -10,
        -11.662819,
        -13.302809,
        -14.898373,
        -16.430256,
        -17.88237,
        -19.242344,
        -20.501678,
        -21.655659,
        -22.70298,
        -23.645235,
        -24.486334,
        -25.231884,
        -25.88864,
        -26.464012,
        -26.965673,
        -27.401234,
        -27.77803,
        -28.102966
      ]
    },
    {
      "name": "outputSpring",
      "type": "springParameters",
      "data_points": [
        {
          "stiffness": 100000,
          "dampingRatio": 1
        },
        {
          "stiffness": 100000,
          "dampingRatio": 1
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        }
      ]
    },
    {
      "name": "isStable",
      "type": "boolean",
      "data_points": [
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true
      ]
    },
    {
      "name": "overdragLimit",
      "type": "float",
      "data_points": [
        null,
        null,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10
      ]
    }
  ]
}
 No newline at end of file
+268 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    272,
    288,
    304
  ],
  "features": [
    {
      "name": "input",
      "type": "float",
      "data_points": [
        5,
        10,
        15,
        20,
        25,
        30,
        35,
        40,
        45,
        50,
        55,
        60,
        65,
        70,
        75,
        80,
        85,
        90,
        95,
        100
      ]
    },
    {
      "name": "gestureDirection",
      "type": "string",
      "data_points": [
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max"
      ]
    },
    {
      "name": "output",
      "type": "float",
      "data_points": [
        -5,
        -10,
        -11.662819,
        -13.302809,
        -14.898373,
        -16.430256,
        -17.88237,
        -19.242344,
        -20.501678,
        -21.655659,
        -22.70298,
        -23.645235,
        -24.486334,
        -25.231884,
        -25.88864,
        -26.464012,
        -26.965673,
        -27.401234,
        -27.77803,
        -28.102966
      ]
    },
    {
      "name": "outputTarget",
      "type": "float",
      "data_points": [
        -5,
        -10,
        -11.662819,
        -13.302809,
        -14.898373,
        -16.430256,
        -17.88237,
        -19.242344,
        -20.501678,
        -21.655659,
        -22.70298,
        -23.645235,
        -24.486334,
        -25.231884,
        -25.88864,
        -26.464012,
        -26.965673,
        -27.401234,
        -27.77803,
        -28.102966
      ]
    },
    {
      "name": "outputSpring",
      "type": "springParameters",
      "data_points": [
        {
          "stiffness": 100000,
          "dampingRatio": 1
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        }
      ]
    },
    {
      "name": "isStable",
      "type": "boolean",
      "data_points": [
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true
      ]
    },
    {
      "name": "overdragLimit",
      "type": "float",
      "data_points": [
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10,
        -10
      ]
    }
  ]
}
 No newline at end of file
+125 −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.effects

import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.spatialMotionSpec
import com.android.mechanics.testing.CapturedSemantics
import com.android.mechanics.testing.ComposeMotionValueToolkit
import com.android.mechanics.testing.FakeMotionSpecBuilderContext
import com.android.mechanics.testing.VerifyTimeSeriesResult
import com.android.mechanics.testing.animateValueTo
import com.android.mechanics.testing.goldenTest
import com.android.mechanics.testing.input
import com.android.mechanics.testing.nullableDataPoints
import com.android.mechanics.testing.output
import com.google.common.truth.Truth.assertThat
import kotlin.math.abs
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import platform.test.motion.MotionTestRule
import platform.test.motion.golden.DataPointTypes
import platform.test.motion.testing.createGoldenPathManager

@RunWith(AndroidJUnit4::class)
class OverdragTest : MotionBuilderContext by FakeMotionSpecBuilderContext.Default {
    private val goldenPathManager =
        createGoldenPathManager("frameworks/libs/systemui/mechanics/tests/goldens")

    @get:Rule val motion = MotionTestRule(ComposeMotionValueToolkit, goldenPathManager)

    @Test
    fun overdrag_maxDirection_neverExceedsMaxOverdrag() {
        motion.goldenTest(
            spatialMotionSpec { after(10f, Overdrag(maxOverdrag = 20.dp)) },
            semantics = CaptureOverdragSemantics,
            verifyTimeSeries = {
                assertThat(output.filter { it > 30 }).isEmpty()
                VerifyTimeSeriesResult.AssertTimeSeriesMatchesGolden
            },
        ) {
            animateValueTo(100f, changePerFrame = 5f)
        }
    }

    @Test
    fun overdrag_minDirection_neverExceedsMaxOverdrag() {
        motion.goldenTest(
            spatialMotionSpec { before(-10f, Overdrag(maxOverdrag = 20.dp)) },
            semantics = CaptureOverdragSemantics,
            initialDirection = InputDirection.Min,
            verifyTimeSeries = {
                assertThat(output.filter { it < -30 }).isEmpty()

                VerifyTimeSeriesResult.AssertTimeSeriesMatchesGolden
            },
        ) {
            animateValueTo(-100f, changePerFrame = 5f)
        }
    }

    @Test
    fun overdrag_nonStandardBaseFunction() {
        motion.goldenTest(
            spatialMotionSpec(baseMapping = { -it }) { after(10f, Overdrag(maxOverdrag = 20.dp)) },
            semantics = CaptureOverdragSemantics,
            initialValue = 5f,
            verifyTimeSeries = {
                assertThat(output.filter { it < -30 }).isEmpty()
                VerifyTimeSeriesResult.AssertTimeSeriesMatchesGolden
            },
        ) {
            animateValueTo(100f, changePerFrame = 5f)
        }
    }

    @Test
    fun semantics_exposesOverdragLimitWhileOverdragging() {
        motion.goldenTest(
            spatialMotionSpec {
                before(-10f, Overdrag())
                after(10f, Overdrag())
            },
            semantics = CaptureOverdragSemantics,
            verifyTimeSeries = {
                val isOverdragging = input.map { abs(it) >= 10 }
                val hasOverdragLimit = nullableDataPoints<Float>("overdragLimit").map { it != null }
                assertThat(hasOverdragLimit).isEqualTo(isOverdragging)
                VerifyTimeSeriesResult.SkipGoldenVerification
            },
        ) {
            animateValueTo(20f, changePerFrame = 5f)
            reset(0f, InputDirection.Min)
            animateValueTo(-20f, changePerFrame = 5f)
        }
    }

    companion object {
        val CaptureOverdragSemantics =
            listOf(
                CapturedSemantics(
                    Overdrag.Defaults.OverdragLimit,
                    DataPointTypes.float,
                    "overdragLimit",
                )
            )
    }
}