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

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

Add ViewMotionBuilderContext (and material spring definitions)

Exposes the standard / expressive motion theme springs, by copying the
definitions from the compose world.

Added a test to guarantee the tokens remain in sync with what Material
publishes through Android.

Bug: 389081766
Test: ViewMotionBuilderContextTest
Flag: EXEMPT not yet used
Change-Id: I60a88905f8c13805cea7ecde78367aac4aa4199d
parent 21e72d19
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -31,6 +31,10 @@ import com.android.mechanics.spring.SpringParameters
 * Device / scheme specific context for building motion specs.
 *
 * See go/motion-system.
 *
 * @see rememberMotionBuilderContext for Compose
 * @see standardViewMotionBuilderContext for Views
 * @see expressiveViewMotionBuilderContext for Views
 */
interface MotionBuilderContext : Density {
    /**
+127 −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.view

import android.content.Context
import androidx.compose.ui.unit.Density
import com.android.mechanics.spec.builder.MaterialSprings
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spring.SpringParameters
import com.android.mechanics.view.ViewMaterialSprings.Default

/**
 * Creates a [MotionBuilderContext] using the **standard** motion spec.
 *
 * See go/motion-system.
 *
 * @param context The context to derive the density from.
 */
fun standardViewMotionBuilderContext(context: Context): MotionBuilderContext {
    return standardViewMotionBuilderContext(context.resources.displayMetrics.density)
}

/**
 * Creates a [MotionBuilderContext] using the **standard** motion spec.
 *
 * See go/motion-system.
 *
 * @param density The density of the display, as a scaling factor for the dp to px conversion.
 */
fun standardViewMotionBuilderContext(density: Float): MotionBuilderContext {
    return with(ViewMaterialSprings.Default) {
        ViewMotionBuilderContext(Spatial, Effects, Density(density))
    }
}

/**
 * Creates a [MotionBuilderContext] using the **expressive** motion spec.
 *
 * See go/motion-system.
 *
 * @param context The context to derive the density from.
 */
fun expressiveViewMotionBuilderContext(context: Context): MotionBuilderContext {
    return expressiveViewMotionBuilderContext(context.resources.displayMetrics.density)
}

/**
 * Creates a [MotionBuilderContext] using the **expressive** motion spec.
 *
 * See go/motion-system.
 *
 * @param density The density of the display, as a scaling factor for the dp to px conversion.
 */
fun expressiveViewMotionBuilderContext(density: Float): MotionBuilderContext {
    return with(ViewMaterialSprings.Expressive) {
        ViewMotionBuilderContext(Spatial, Effects, Density(density))
    }
}

/**
 * Material motion system spring definitions.
 *
 * See go/motion-system.
 *
 * NOTE: These are only defined here since material spring parameters are not available for View
 * based APIs. There might be a delay in updating these values, should the material tokens be
 * updated in the future.
 *
 * @see rememberMotionBuilderContext for Compose
 */
object ViewMaterialSprings {
    object Default {
        val Spatial =
            MaterialSprings(
                SpringParameters(700.0f, 0.9f),
                SpringParameters(1400.0f, 0.9f),
                SpringParameters(300.0f, 0.9f),
                MotionBuilderContext.StableThresholdSpatial,
            )

        val Effects =
            MaterialSprings(
                SpringParameters(1600.0f, 1.0f),
                SpringParameters(3800.0f, 1.0f),
                SpringParameters(800.0f, 1.0f),
                MotionBuilderContext.StableThresholdEffects,
            )
    }

    object Expressive {
        val Spatial =
            MaterialSprings(
                SpringParameters(380.0f, 0.8f),
                SpringParameters(800.0f, 0.6f),
                SpringParameters(200.0f, 0.8f),
                MotionBuilderContext.StableThresholdSpatial,
            )

        val Effects =
            MaterialSprings(
                SpringParameters(1600.0f, 1.0f),
                SpringParameters(3800.0f, 1.0f),
                SpringParameters(800.0f, 1.0f),
                MotionBuilderContext.StableThresholdEffects,
            )
    }
}

internal class ViewMotionBuilderContext(
    override val spatial: MaterialSprings,
    override val effects: MaterialSprings,
    density: Density,
) : MotionBuilderContext, Density by density
+77 −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.
 */

@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)

package com.android.mechanics.view

import android.content.Context
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MotionScheme
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.rememberMotionBuilderContext
import com.google.common.truth.Truth
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ViewMotionBuilderContextTest {

    @get:Rule(order = 0) val rule = createComposeRule()

    @Test
    fun materialSprings_standardScheme_matchesComposeDefinition() {
        lateinit var viewContext: Context
        lateinit var composeReference: MotionBuilderContext

        rule.setContent {
            viewContext = LocalContext.current
            MaterialTheme(motionScheme = MotionScheme.standard()) {
                composeReference = rememberMotionBuilderContext()
            }
        }

        val underTest = standardViewMotionBuilderContext(viewContext)

        Truth.assertThat(underTest.density).isEqualTo(composeReference.density)
        Truth.assertThat(underTest.spatial).isEqualTo(composeReference.spatial)
        Truth.assertThat(underTest.effects).isEqualTo(composeReference.effects)
    }

    @Test
    fun materialSprings_expressiveScheme_matchesComposeDefinition() {
        lateinit var viewContext: Context
        lateinit var composeReference: MotionBuilderContext

        rule.setContent {
            viewContext = LocalContext.current
            MaterialTheme(motionScheme = MotionScheme.expressive()) {
                composeReference = rememberMotionBuilderContext()
            }
        }

        val underTest = expressiveViewMotionBuilderContext(viewContext)

        Truth.assertThat(underTest.density).isEqualTo(composeReference.density)
        Truth.assertThat(underTest.spatial).isEqualTo(composeReference.spatial)
        Truth.assertThat(underTest.effects).isEqualTo(composeReference.effects)
    }
}