Loading mechanics/TEST_MAPPING +6 −0 Original line number Diff line number Diff line Loading @@ -13,6 +13,12 @@ {"exclude-annotation": "org.junit.Ignore"}, {"exclude-annotation": "androidx.test.filters.FlakyTest"} ] }, { "name": "PlatformComposeSceneTransitionLayoutTests" }, { "name": "PlatformComposeCoreTests" } ], "presubmit-large": [ Loading mechanics/src/com/android/mechanics/effects/MagneticDetach.kt +141 −34 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import com.android.mechanics.spec.SegmentKey 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.EffectPlacemenType import com.android.mechanics.spec.builder.EffectPlacement import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spec.with Loading @@ -48,6 +49,7 @@ import com.android.mechanics.spring.SpringParameters */ class MagneticDetach( private val semanticState: SemanticKey<State> = Defaults.AttachDetachState, private val semanticAttachedValue: SemanticKey<Float?> = Defaults.AttachedValue, private val detachPosition: Dp = Defaults.DetachPosition, private val attachPosition: Dp = Defaults.AttachPosition, private val detachScale: Float = Defaults.AttachDetachScale, Loading Loading @@ -76,40 +78,135 @@ class MagneticDetach( maxLimitKey: BreakpointKey, placement: EffectPlacement, ) { if (placement.type == EffectPlacemenType.Before) { createPlacedBeforeSpec(minLimit, minLimitKey, maxLimit, maxLimitKey) } else { assert(placement.type == EffectPlacemenType.After) createPlacedAfterSpec(minLimit, minLimitKey, maxLimit, maxLimitKey) } } object Defaults { val AttachDetachState = SemanticKey<State>() val AttachedValue = SemanticKey<Float?>() val AttachDetachScale = .3f val DetachPosition = 80.dp val AttachPosition = 40.dp val Spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) } /* Effect is attached at minLimit, and detaches at maxLimit. */ private fun EffectApplyScope.createPlacedAfterSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, ) { val attachedValue = baseValue(minLimit) val detachedValue = baseValue(maxLimit) val reattachPos = minLimit + attachPosition.toPx() val startValue = baseValue(minLimit) val detachValue = baseValue(maxLimit) val reattachValue = baseValue(reattachPos) val scaledDetachValue = startValue + (detachValue - startValue) * detachScale val scaledReattachValue = startValue + (reattachValue - startValue) * attachScale val attachedSemantics = listOf(semanticState with State.Attached, semanticAttachedValue with attachedValue) val detachedSemantics = listOf(semanticState with State.Detached, semanticAttachedValue with null) val attachKey = BreakpointKey("attach") val scaledDetachValue = attachedValue + (detachedValue - attachedValue) * detachScale val scaledReattachValue = attachedValue + (reattachValue - attachedValue) * attachScale val attachKey = BreakpointKey("attach") forward( initialMapping = Mapping.Linear(minLimit, startValue, maxLimit, scaledDetachValue), semantics = listOf(semanticState with State.Attached), initialMapping = Mapping.Linear(minLimit, attachedValue, maxLimit, scaledDetachValue), semantics = attachedSemantics, ) { after(spring = detachSpring, semantics = listOf(semanticState with State.Detached)) after(spring = detachSpring, semantics = detachedSemantics) before(semantics = listOf(semanticAttachedValue with null)) } backward( initialMapping = Mapping.Linear(minLimit, startValue, reattachPos, scaledReattachValue), semantics = listOf(semanticState with State.Attached), initialMapping = Mapping.Linear(minLimit, attachedValue, reattachPos, scaledReattachValue), semantics = attachedSemantics, ) { mapping( breakpoint = reattachPos, key = attachKey, spring = attachSpring, semantics = listOf(semanticState with State.Detached), semantics = detachedSemantics, mapping = baseMapping, ) before(semantics = listOf(semanticAttachedValue with null)) after(semantics = listOf(semanticAttachedValue with null)) } val beforeDetachSegment = SegmentKey(minLimitKey, maxLimitKey, InputDirection.Max) val beforeAttachSegment = SegmentKey(attachKey, maxLimitKey, InputDirection.Min) val afterAttachSegment = SegmentKey(minLimitKey, attachKey, InputDirection.Min) addSegmentHandlers( beforeDetachSegment = SegmentKey(minLimitKey, maxLimitKey, InputDirection.Max), beforeAttachSegment = SegmentKey(attachKey, maxLimitKey, InputDirection.Min), afterAttachSegment = SegmentKey(minLimitKey, attachKey, InputDirection.Min), minLimit = minLimit, maxLimit = maxLimit, ) } /* Effect is attached at maxLimit, and detaches at minLimit. */ private fun EffectApplyScope.createPlacedBeforeSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, ) { val attachedValue = baseValue(maxLimit) val detachedValue = baseValue(minLimit) val reattachPos = maxLimit - attachPosition.toPx() val reattachValue = baseValue(reattachPos) val attachedSemantics = listOf(semanticState with State.Attached, semanticAttachedValue with attachedValue) val detachedSemantics = listOf(semanticState with State.Detached, semanticAttachedValue with null) val scaledDetachValue = attachedValue + (detachedValue - attachedValue) * detachScale val scaledReattachValue = attachedValue + (reattachValue - attachedValue) * attachScale val attachKey = BreakpointKey("attach") backward( initialMapping = Mapping.Linear(minLimit, scaledDetachValue, maxLimit, attachedValue), semantics = attachedSemantics, ) { before(spring = detachSpring, semantics = detachedSemantics) after(semantics = listOf(semanticAttachedValue with null)) } forward(initialMapping = baseMapping, semantics = detachedSemantics) { target( breakpoint = reattachPos, key = attachKey, from = scaledReattachValue, to = attachedValue, spring = attachSpring, semantics = attachedSemantics, ) after(semantics = listOf(semanticAttachedValue with null)) } addSegmentHandlers( beforeDetachSegment = SegmentKey(minLimitKey, maxLimitKey, InputDirection.Min), beforeAttachSegment = SegmentKey(minLimitKey, attachKey, InputDirection.Max), afterAttachSegment = SegmentKey(attachKey, maxLimitKey, InputDirection.Max), minLimit = minLimit, maxLimit = maxLimit, ) } private fun EffectApplyScope.addSegmentHandlers( beforeDetachSegment: SegmentKey, beforeAttachSegment: SegmentKey, afterAttachSegment: SegmentKey, minLimit: Float, maxLimit: Float, ) { // Suppress direction change during detach. This prevents snapping to the origin when // changing the direction while detaching. addSegmentHandler(beforeDetachSegment, PreventDirectionChangeWithinCurrentSegment) Loading @@ -122,31 +219,41 @@ class MagneticDetach( addSegmentHandler(afterAttachSegment) { currentSegment, newInput, newDirection -> val nextSegment = segmentAtInput(newInput, newDirection) if (nextSegment.key == beforeDetachSegment) { val pivotPos = newInput val pivotValue = currentSegment.mapping.map(pivotPos) val tweakedMapping = Mapping { input -> if (input <= pivotPos) { val t = (input - minLimit) / (pivotPos - minLimit) lerp(startValue, pivotValue, t) } else { val t = (input - pivotPos) / (maxLimit - pivotPos) lerp(pivotValue, scaledDetachValue, t) } } nextSegment.copy(mapping = tweakedMapping) nextSegment.copy( mapping = switchMappingWithSamePivotValue( currentSegment.mapping, nextSegment.mapping, minLimit, newInput, maxLimit, ) ) } else { nextSegment } } } object Defaults { val AttachDetachState = SemanticKey<State>() val AttachDetachScale = .3f val DetachPosition = 80.dp val AttachPosition = 40.dp val Spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) private fun switchMappingWithSamePivotValue( source: Mapping, target: Mapping, minLimit: Float, pivot: Float, maxLimit: Float, ): Mapping { val minValue = target.map(minLimit) val pivotValue = source.map(pivot) val maxValue = target.map(maxLimit) return Mapping { input -> if (input <= pivot) { val t = (input - minLimit) / (pivot - minLimit) lerp(minValue, pivotValue, t) } else { val t = (input - pivot) / (maxLimit - pivot) lerp(pivotValue, maxValue, t) } } } } 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/MotionSpec.kt +1 −1 Original line number Diff line number Diff line Loading @@ -177,7 +177,7 @@ data class DirectionalMotionSpec( semantics.forEach { require(it.values.size == mappings.size) { "Semantics ${it.key} does not include correct number of segments" "Semantics ${it.key} contains ${it.values.size} values vs ${mappings.size} expected" } } } Loading mechanics/src/com/android/mechanics/spec/SemanticValue.kt +2 −0 Original line number Diff line number Diff line Loading @@ -69,4 +69,6 @@ class SegmentSemanticValues<T>(val key: SemanticKey<T>, val values: List<T>) { operator fun get(segmentIndex: Int): SemanticValue<T> { return SemanticValue(key, values[segmentIndex]) } override fun toString() = "Semantics($key): [$values]" } Loading
mechanics/TEST_MAPPING +6 −0 Original line number Diff line number Diff line Loading @@ -13,6 +13,12 @@ {"exclude-annotation": "org.junit.Ignore"}, {"exclude-annotation": "androidx.test.filters.FlakyTest"} ] }, { "name": "PlatformComposeSceneTransitionLayoutTests" }, { "name": "PlatformComposeCoreTests" } ], "presubmit-large": [ Loading
mechanics/src/com/android/mechanics/effects/MagneticDetach.kt +141 −34 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import com.android.mechanics.spec.SegmentKey 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.EffectPlacemenType import com.android.mechanics.spec.builder.EffectPlacement import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spec.with Loading @@ -48,6 +49,7 @@ import com.android.mechanics.spring.SpringParameters */ class MagneticDetach( private val semanticState: SemanticKey<State> = Defaults.AttachDetachState, private val semanticAttachedValue: SemanticKey<Float?> = Defaults.AttachedValue, private val detachPosition: Dp = Defaults.DetachPosition, private val attachPosition: Dp = Defaults.AttachPosition, private val detachScale: Float = Defaults.AttachDetachScale, Loading Loading @@ -76,40 +78,135 @@ class MagneticDetach( maxLimitKey: BreakpointKey, placement: EffectPlacement, ) { if (placement.type == EffectPlacemenType.Before) { createPlacedBeforeSpec(minLimit, minLimitKey, maxLimit, maxLimitKey) } else { assert(placement.type == EffectPlacemenType.After) createPlacedAfterSpec(minLimit, minLimitKey, maxLimit, maxLimitKey) } } object Defaults { val AttachDetachState = SemanticKey<State>() val AttachedValue = SemanticKey<Float?>() val AttachDetachScale = .3f val DetachPosition = 80.dp val AttachPosition = 40.dp val Spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) } /* Effect is attached at minLimit, and detaches at maxLimit. */ private fun EffectApplyScope.createPlacedAfterSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, ) { val attachedValue = baseValue(minLimit) val detachedValue = baseValue(maxLimit) val reattachPos = minLimit + attachPosition.toPx() val startValue = baseValue(minLimit) val detachValue = baseValue(maxLimit) val reattachValue = baseValue(reattachPos) val scaledDetachValue = startValue + (detachValue - startValue) * detachScale val scaledReattachValue = startValue + (reattachValue - startValue) * attachScale val attachedSemantics = listOf(semanticState with State.Attached, semanticAttachedValue with attachedValue) val detachedSemantics = listOf(semanticState with State.Detached, semanticAttachedValue with null) val attachKey = BreakpointKey("attach") val scaledDetachValue = attachedValue + (detachedValue - attachedValue) * detachScale val scaledReattachValue = attachedValue + (reattachValue - attachedValue) * attachScale val attachKey = BreakpointKey("attach") forward( initialMapping = Mapping.Linear(minLimit, startValue, maxLimit, scaledDetachValue), semantics = listOf(semanticState with State.Attached), initialMapping = Mapping.Linear(minLimit, attachedValue, maxLimit, scaledDetachValue), semantics = attachedSemantics, ) { after(spring = detachSpring, semantics = listOf(semanticState with State.Detached)) after(spring = detachSpring, semantics = detachedSemantics) before(semantics = listOf(semanticAttachedValue with null)) } backward( initialMapping = Mapping.Linear(minLimit, startValue, reattachPos, scaledReattachValue), semantics = listOf(semanticState with State.Attached), initialMapping = Mapping.Linear(minLimit, attachedValue, reattachPos, scaledReattachValue), semantics = attachedSemantics, ) { mapping( breakpoint = reattachPos, key = attachKey, spring = attachSpring, semantics = listOf(semanticState with State.Detached), semantics = detachedSemantics, mapping = baseMapping, ) before(semantics = listOf(semanticAttachedValue with null)) after(semantics = listOf(semanticAttachedValue with null)) } val beforeDetachSegment = SegmentKey(minLimitKey, maxLimitKey, InputDirection.Max) val beforeAttachSegment = SegmentKey(attachKey, maxLimitKey, InputDirection.Min) val afterAttachSegment = SegmentKey(minLimitKey, attachKey, InputDirection.Min) addSegmentHandlers( beforeDetachSegment = SegmentKey(minLimitKey, maxLimitKey, InputDirection.Max), beforeAttachSegment = SegmentKey(attachKey, maxLimitKey, InputDirection.Min), afterAttachSegment = SegmentKey(minLimitKey, attachKey, InputDirection.Min), minLimit = minLimit, maxLimit = maxLimit, ) } /* Effect is attached at maxLimit, and detaches at minLimit. */ private fun EffectApplyScope.createPlacedBeforeSpec( minLimit: Float, minLimitKey: BreakpointKey, maxLimit: Float, maxLimitKey: BreakpointKey, ) { val attachedValue = baseValue(maxLimit) val detachedValue = baseValue(minLimit) val reattachPos = maxLimit - attachPosition.toPx() val reattachValue = baseValue(reattachPos) val attachedSemantics = listOf(semanticState with State.Attached, semanticAttachedValue with attachedValue) val detachedSemantics = listOf(semanticState with State.Detached, semanticAttachedValue with null) val scaledDetachValue = attachedValue + (detachedValue - attachedValue) * detachScale val scaledReattachValue = attachedValue + (reattachValue - attachedValue) * attachScale val attachKey = BreakpointKey("attach") backward( initialMapping = Mapping.Linear(minLimit, scaledDetachValue, maxLimit, attachedValue), semantics = attachedSemantics, ) { before(spring = detachSpring, semantics = detachedSemantics) after(semantics = listOf(semanticAttachedValue with null)) } forward(initialMapping = baseMapping, semantics = detachedSemantics) { target( breakpoint = reattachPos, key = attachKey, from = scaledReattachValue, to = attachedValue, spring = attachSpring, semantics = attachedSemantics, ) after(semantics = listOf(semanticAttachedValue with null)) } addSegmentHandlers( beforeDetachSegment = SegmentKey(minLimitKey, maxLimitKey, InputDirection.Min), beforeAttachSegment = SegmentKey(minLimitKey, attachKey, InputDirection.Max), afterAttachSegment = SegmentKey(attachKey, maxLimitKey, InputDirection.Max), minLimit = minLimit, maxLimit = maxLimit, ) } private fun EffectApplyScope.addSegmentHandlers( beforeDetachSegment: SegmentKey, beforeAttachSegment: SegmentKey, afterAttachSegment: SegmentKey, minLimit: Float, maxLimit: Float, ) { // Suppress direction change during detach. This prevents snapping to the origin when // changing the direction while detaching. addSegmentHandler(beforeDetachSegment, PreventDirectionChangeWithinCurrentSegment) Loading @@ -122,31 +219,41 @@ class MagneticDetach( addSegmentHandler(afterAttachSegment) { currentSegment, newInput, newDirection -> val nextSegment = segmentAtInput(newInput, newDirection) if (nextSegment.key == beforeDetachSegment) { val pivotPos = newInput val pivotValue = currentSegment.mapping.map(pivotPos) val tweakedMapping = Mapping { input -> if (input <= pivotPos) { val t = (input - minLimit) / (pivotPos - minLimit) lerp(startValue, pivotValue, t) } else { val t = (input - pivotPos) / (maxLimit - pivotPos) lerp(pivotValue, scaledDetachValue, t) } } nextSegment.copy(mapping = tweakedMapping) nextSegment.copy( mapping = switchMappingWithSamePivotValue( currentSegment.mapping, nextSegment.mapping, minLimit, newInput, maxLimit, ) ) } else { nextSegment } } } object Defaults { val AttachDetachState = SemanticKey<State>() val AttachDetachScale = .3f val DetachPosition = 80.dp val AttachPosition = 40.dp val Spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) private fun switchMappingWithSamePivotValue( source: Mapping, target: Mapping, minLimit: Float, pivot: Float, maxLimit: Float, ): Mapping { val minValue = target.map(minLimit) val pivotValue = source.map(pivot) val maxValue = target.map(maxLimit) return Mapping { input -> if (input <= pivot) { val t = (input - minLimit) / (pivot - minLimit) lerp(minValue, pivotValue, t) } else { val t = (input - pivot) / (maxLimit - pivot) lerp(pivotValue, maxValue, t) } } } }
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/MotionSpec.kt +1 −1 Original line number Diff line number Diff line Loading @@ -177,7 +177,7 @@ data class DirectionalMotionSpec( semantics.forEach { require(it.values.size == mappings.size) { "Semantics ${it.key} does not include correct number of segments" "Semantics ${it.key} contains ${it.values.size} values vs ${mappings.size} expected" } } } Loading
mechanics/src/com/android/mechanics/spec/SemanticValue.kt +2 −0 Original line number Diff line number Diff line Loading @@ -69,4 +69,6 @@ class SegmentSemanticValues<T>(val key: SemanticKey<T>, val values: List<T>) { operator fun get(segmentIndex: Int): SemanticValue<T> { return SemanticValue(key, values[segmentIndex]) } override fun toString() = "Semantics($key): [$values]" }