Loading mechanics/src/com/android/mechanics/effects/Fixed.kt +13 −14 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.mechanics.effects import com.android.mechanics.spec.BreakpointKey import com.android.mechanics.spec.Mapping import com.android.mechanics.spec.builder.Effect import com.android.mechanics.spec.builder.EffectApplyScope Loading @@ -32,9 +33,18 @@ val MotionSpecBuilderScope.one: FixedValue get() = FixedValue.One /** Produces a fixed [value]. */ open class FixedValue(val value: Float) : Effect { override fun EffectApplyScope.createSpec() { class FixedValue(val value: Float) : Effect.PlaceableAfter, Effect.PlaceableBefore, Effect.PlaceableBetween { override fun MotionBuilderContext.intrinsicSize(): Float = Float.NaN override fun EffectApplyScope.createSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, placement: EffectPlacement, ) { return unidirectional(Mapping.Fixed(value)) } Loading @@ -43,14 +53,3 @@ open class FixedValue(val value: Float) : Effect { val One = FixedValue(1f) } } /** Produces a fixed [value], for a predefined [extent]. */ class FixedValueWithExtent(value: Float, private val extent: Float) : FixedValue(value) { init { require(extent > 0) } override fun MotionBuilderContext.measure(effectPlacement: EffectPlacement): Float { return extent * effectPlacement.directionSign } } mechanics/src/com/android/mechanics/effects/MagneticDetach.kt +24 −23 Original line number Diff line number Diff line Loading @@ -54,7 +54,7 @@ class MagneticDetach( private val attachScale: Float = Defaults.AttachDetachScale * (attachPosition / detachPosition), private val detachSpring: SpringParameters = Defaults.Spring, private val attachSpring: SpringParameters = Defaults.Spring, ) : Effect { ) : Effect.PlaceableAfter, Effect.PlaceableBefore { init { require(attachPosition <= detachPosition) Loading @@ -65,16 +65,20 @@ class MagneticDetach( Detached, } override fun MotionBuilderContext.measure(effectPlacement: EffectPlacement): Float { return detachPosition.toPx() * effectPlacement.directionSign override fun MotionBuilderContext.intrinsicSize(): Float { return detachPosition.toPx() } override fun EffectApplyScope.createSpec() { val startPos = minLimit val reattachPos = startPos + attachPosition.toPx() val detachPos = maxLimit val startValue = baseValue(startPos) val detachValue = baseValue(detachPos) override fun EffectApplyScope.createSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, placement: EffectPlacement, ) { val reattachPos = minLimit + attachPosition.toPx() val startValue = baseValue(minLimit) val detachValue = baseValue(maxLimit) val reattachValue = baseValue(reattachPos) val scaledDetachValue = startValue + (detachValue - startValue) * detachScale Loading @@ -83,15 +87,14 @@ class MagneticDetach( val attachKey = BreakpointKey("attach") forward( initialMapping = Mapping.Linear(startPos, startValue, detachPos, scaledDetachValue), initialMapping = Mapping.Linear(minLimit, startValue, maxLimit, scaledDetachValue), semantics = listOf(semanticState with State.Attached), ) { maxLimitSpring = detachSpring maxLimitSemantics = listOf(semanticState with State.Detached) after(spring = detachSpring, semantics = listOf(semanticState with State.Detached)) } backward( initialMapping = Mapping.Linear(startPos, startValue, reattachPos, scaledReattachValue), initialMapping = Mapping.Linear(minLimit, startValue, reattachPos, scaledReattachValue), semantics = listOf(semanticState with State.Attached), ) { mapping( Loading Loading @@ -125,10 +128,10 @@ class MagneticDetach( val tweakedMapping = Mapping { input -> if (input <= pivotPos) { val t = (input - startPos) / (pivotPos - startPos) val t = (input - minLimit) / (pivotPos - minLimit) lerp(startValue, pivotValue, t) } else { val t = (input - pivotPos) / (detachPos - pivotPos) val t = (input - pivotPos) / (maxLimit - pivotPos) lerp(pivotValue, scaledDetachValue, t) } } Loading @@ -139,7 +142,6 @@ class MagneticDetach( } } companion object { object Defaults { val AttachDetachState = SemanticKey<State>() val AttachDetachScale = .3f Loading @@ -148,4 +150,3 @@ class MagneticDetach( val Spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) } } } mechanics/src/com/android/mechanics/effects/Overdrag.kt 0 → 100644 +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 } } mechanics/src/com/android/mechanics/impl/Computations.kt +12 −12 Original line number Diff line number Diff line Loading @@ -481,13 +481,6 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static guaranteeState.updatedSpringParameters(lastBreakpoint) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime val mappingBefore = mappings[segmentIndex] val beforeBreakpoint = mappingBefore.map(nextBreakpoint.position) val mappingAfter = mappings[segmentIndex + directionOffset] Loading @@ -505,7 +498,18 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static " after: $afterBreakpoint (mapping: $mappingAfter)", ) } hasJumped = hasJumped || delta != 0f if (!hasJumped && delta != 0f) { hasJumped = true springState = springState.nudge(velocityDelta = directMappedVelocity) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime if (deltaIsFinite) { springState = springState.nudge(displacementDelta = -delta) Loading @@ -530,10 +534,6 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static } } if (hasJumped) { springState = springState.nudge(velocityDelta = directMappedVelocity) } val tightened = guarantee.updatedSpringParameters(segment.entryBreakpoint) DiscontinuityAnimation(springState, tightened, lastAnimationTime) Loading mechanics/src/com/android/mechanics/spec/Segment.kt +0 −13 Original line number Diff line number Diff line Loading @@ -136,19 +136,6 @@ fun interface Mapping { } } data class Tanh(val scaling: Float, val tilt: Float, val offset: Float = 0f) : Mapping { init { require(scaling.isFinite()) require(tilt.isFinite()) require(offset.isFinite()) } override fun map(input: Float): Float { return scaling * kotlin.math.tanh((input + offset) / (scaling * tilt)) } } companion object { val Zero = Fixed(0f) val One = Fixed(1f) Loading Loading
mechanics/src/com/android/mechanics/effects/Fixed.kt +13 −14 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.mechanics.effects import com.android.mechanics.spec.BreakpointKey import com.android.mechanics.spec.Mapping import com.android.mechanics.spec.builder.Effect import com.android.mechanics.spec.builder.EffectApplyScope Loading @@ -32,9 +33,18 @@ val MotionSpecBuilderScope.one: FixedValue get() = FixedValue.One /** Produces a fixed [value]. */ open class FixedValue(val value: Float) : Effect { override fun EffectApplyScope.createSpec() { class FixedValue(val value: Float) : Effect.PlaceableAfter, Effect.PlaceableBefore, Effect.PlaceableBetween { override fun MotionBuilderContext.intrinsicSize(): Float = Float.NaN override fun EffectApplyScope.createSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, placement: EffectPlacement, ) { return unidirectional(Mapping.Fixed(value)) } Loading @@ -43,14 +53,3 @@ open class FixedValue(val value: Float) : Effect { val One = FixedValue(1f) } } /** Produces a fixed [value], for a predefined [extent]. */ class FixedValueWithExtent(value: Float, private val extent: Float) : FixedValue(value) { init { require(extent > 0) } override fun MotionBuilderContext.measure(effectPlacement: EffectPlacement): Float { return extent * effectPlacement.directionSign } }
mechanics/src/com/android/mechanics/effects/MagneticDetach.kt +24 −23 Original line number Diff line number Diff line Loading @@ -54,7 +54,7 @@ class MagneticDetach( private val attachScale: Float = Defaults.AttachDetachScale * (attachPosition / detachPosition), private val detachSpring: SpringParameters = Defaults.Spring, private val attachSpring: SpringParameters = Defaults.Spring, ) : Effect { ) : Effect.PlaceableAfter, Effect.PlaceableBefore { init { require(attachPosition <= detachPosition) Loading @@ -65,16 +65,20 @@ class MagneticDetach( Detached, } override fun MotionBuilderContext.measure(effectPlacement: EffectPlacement): Float { return detachPosition.toPx() * effectPlacement.directionSign override fun MotionBuilderContext.intrinsicSize(): Float { return detachPosition.toPx() } override fun EffectApplyScope.createSpec() { val startPos = minLimit val reattachPos = startPos + attachPosition.toPx() val detachPos = maxLimit val startValue = baseValue(startPos) val detachValue = baseValue(detachPos) override fun EffectApplyScope.createSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, placement: EffectPlacement, ) { val reattachPos = minLimit + attachPosition.toPx() val startValue = baseValue(minLimit) val detachValue = baseValue(maxLimit) val reattachValue = baseValue(reattachPos) val scaledDetachValue = startValue + (detachValue - startValue) * detachScale Loading @@ -83,15 +87,14 @@ class MagneticDetach( val attachKey = BreakpointKey("attach") forward( initialMapping = Mapping.Linear(startPos, startValue, detachPos, scaledDetachValue), initialMapping = Mapping.Linear(minLimit, startValue, maxLimit, scaledDetachValue), semantics = listOf(semanticState with State.Attached), ) { maxLimitSpring = detachSpring maxLimitSemantics = listOf(semanticState with State.Detached) after(spring = detachSpring, semantics = listOf(semanticState with State.Detached)) } backward( initialMapping = Mapping.Linear(startPos, startValue, reattachPos, scaledReattachValue), initialMapping = Mapping.Linear(minLimit, startValue, reattachPos, scaledReattachValue), semantics = listOf(semanticState with State.Attached), ) { mapping( Loading Loading @@ -125,10 +128,10 @@ class MagneticDetach( val tweakedMapping = Mapping { input -> if (input <= pivotPos) { val t = (input - startPos) / (pivotPos - startPos) val t = (input - minLimit) / (pivotPos - minLimit) lerp(startValue, pivotValue, t) } else { val t = (input - pivotPos) / (detachPos - pivotPos) val t = (input - pivotPos) / (maxLimit - pivotPos) lerp(pivotValue, scaledDetachValue, t) } } Loading @@ -139,7 +142,6 @@ class MagneticDetach( } } companion object { object Defaults { val AttachDetachState = SemanticKey<State>() val AttachDetachScale = .3f Loading @@ -148,4 +150,3 @@ class MagneticDetach( val Spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) } } }
mechanics/src/com/android/mechanics/effects/Overdrag.kt 0 → 100644 +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 } }
mechanics/src/com/android/mechanics/impl/Computations.kt +12 −12 Original line number Diff line number Diff line Loading @@ -481,13 +481,6 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static guaranteeState.updatedSpringParameters(lastBreakpoint) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime val mappingBefore = mappings[segmentIndex] val beforeBreakpoint = mappingBefore.map(nextBreakpoint.position) val mappingAfter = mappings[segmentIndex + directionOffset] Loading @@ -505,7 +498,18 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static " after: $afterBreakpoint (mapping: $mappingAfter)", ) } hasJumped = hasJumped || delta != 0f if (!hasJumped && delta != 0f) { hasJumped = true springState = springState.nudge(velocityDelta = directMappedVelocity) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime if (deltaIsFinite) { springState = springState.nudge(displacementDelta = -delta) Loading @@ -530,10 +534,6 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static } } if (hasJumped) { springState = springState.nudge(velocityDelta = directMappedVelocity) } val tightened = guarantee.updatedSpringParameters(segment.entryBreakpoint) DiscontinuityAnimation(springState, tightened, lastAnimationTime) Loading
mechanics/src/com/android/mechanics/spec/Segment.kt +0 −13 Original line number Diff line number Diff line Loading @@ -136,19 +136,6 @@ fun interface Mapping { } } data class Tanh(val scaling: Float, val tilt: Float, val offset: Float = 0f) : Mapping { init { require(scaling.isFinite()) require(tilt.isFinite()) require(offset.isFinite()) } override fun map(input: Float): Float { return scaling * kotlin.math.tanh((input + offset) / (scaling * tilt)) } } companion object { val Zero = Fixed(0f) val One = Fixed(1f) Loading