Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +4 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset Loading Loading @@ -140,6 +142,7 @@ interface BaseTransitionBuilder : PropertyTransformationBuilder { fun fractionRange( start: Float? = null, end: Float? = null, easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) } Loading Loading @@ -182,6 +185,7 @@ interface TransitionBuilder : BaseTransitionBuilder { fun timestampRange( startMillis: Int? = null, endMillis: Int? = null, easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +5 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.DurationBasedAnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.VectorConverter Loading Loading @@ -163,9 +164,10 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { override fun fractionRange( start: Float?, end: Float?, easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { range = TransformationRange(start, end) range = TransformationRange(start, end, easing) builder() range = null } Loading Loading @@ -251,6 +253,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu override fun timestampRange( startMillis: Int?, endMillis: Int?, easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) { Loading @@ -263,7 +266,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu val start = startMillis?.let { it.toFloat() / durationMillis } val end = endMillis?.let { it.toFloat() / durationMillis } fractionRange(start, end, builder) fractionRange(start, end, easing, builder) } } Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +17 −10 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.compose.animation.scene.transformation import androidx.compose.animation.core.Easing import androidx.compose.animation.core.LinearEasing import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastCoerceIn Loading Loading @@ -90,11 +92,13 @@ internal class RangedPropertyTransformation<T>( data class TransformationRange( val start: Float, val end: Float, val easing: Easing, ) { constructor( start: Float? = null, end: Float? = null ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified) end: Float? = null, easing: Easing = LinearEasing, ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified, easing) init { require(!start.isSpecified() || (start in 0f..1f)) Loading @@ -103,17 +107,20 @@ data class TransformationRange( } /** Reverse this range. */ fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start)) fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing) /** Get the progress of this range given the global [transitionProgress]. */ fun progress(transitionProgress: Float): Float { return when { val progress = when { start.isSpecified() && end.isSpecified() -> ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f) !start.isSpecified() && !end.isSpecified() -> transitionProgress end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f) else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f) } return easing.transform(progress) } private fun Float.isSpecified() = this != BoundUnspecified Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +25 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.spring Loading Loading @@ -107,6 +108,13 @@ class TransitionDslTest { fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } fractionRange(start = 0.2f) { fade(TestElements.Foo) } fractionRange(end = 0.9f) { fade(TestElements.Foo) } fractionRange( start = 0.1f, end = 0.8f, easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ) { fade(TestElements.Foo) } } } Loading @@ -118,6 +126,11 @@ class TransitionDslTest { TransformationRange(start = 0.1f, end = 0.8f), TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f), TransformationRange( start = 0.1f, end = 0.8f, CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ), ) } Loading @@ -130,6 +143,13 @@ class TransitionDslTest { timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } timestampRange(startMillis = 200) { fade(TestElements.Foo) } timestampRange(endMillis = 400) { fade(TestElements.Foo) } timestampRange( startMillis = 100, endMillis = 300, easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ) { fade(TestElements.Foo) } } } Loading @@ -141,6 +161,11 @@ class TransitionDslTest { TransformationRange(start = 100 / 500f, end = 300 / 500f), TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f), TransformationRange( start = 100 / 500f, end = 300 / 500f, easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ), ) } Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt 0 → 100644 +126 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.compose.animation.scene.transformation import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.testTransition import com.android.compose.test.assertSizeIsEqualTo import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EasingTest { @get:Rule val rule = createComposeRule() @Test fun testFractionRangeEasing() { val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) rule.testTransition( fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, transition = { // Scale during 4 frames. spec = tween(16 * 4, easing = LinearEasing) fractionRange(easing = easing) { scaleSize(TestElements.Foo, width = 0f, height = 0f) scaleSize(TestElements.Bar, width = 0f, height = 0f) } }, ) { // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the // transition so it starts at 200dp x 50dp. before { onElement(TestElements.Bar).assertDoesNotExist() } at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) } at(16) { // 25% linear progress is mapped to 68.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) } at(32) { // 50% linear progress is mapped to 89.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) } at(48) { // 75% linear progress is mapped to 97.8% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) } after { onElement(TestElements.Foo).assertDoesNotExist() onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) } } } @Test fun testTimestampRangeEasing() { val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) rule.testTransition( fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, transition = { // Scale during 4 frames. spec = tween(16 * 4, easing = LinearEasing) timestampRange(easing = easing) { scaleSize(TestElements.Foo, width = 0f, height = 0f) scaleSize(TestElements.Bar, width = 0f, height = 0f) } }, ) { // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the // transition so it starts at 200dp x 50dp. before { onElement(TestElements.Bar).assertDoesNotExist() } at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) } at(16) { // 25% linear progress is mapped to 68.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) } at(32) { // 50% linear progress is mapped to 89.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) } at(48) { // 75% linear progress is mapped to 97.8% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) } after { onElement(TestElements.Foo).assertDoesNotExist() onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) } } } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +4 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset Loading Loading @@ -140,6 +142,7 @@ interface BaseTransitionBuilder : PropertyTransformationBuilder { fun fractionRange( start: Float? = null, end: Float? = null, easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) } Loading Loading @@ -182,6 +185,7 @@ interface TransitionBuilder : BaseTransitionBuilder { fun timestampRange( startMillis: Int? = null, endMillis: Int? = null, easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +5 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.DurationBasedAnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.VectorConverter Loading Loading @@ -163,9 +164,10 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { override fun fractionRange( start: Float?, end: Float?, easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { range = TransformationRange(start, end) range = TransformationRange(start, end, easing) builder() range = null } Loading Loading @@ -251,6 +253,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu override fun timestampRange( startMillis: Int?, endMillis: Int?, easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) { Loading @@ -263,7 +266,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu val start = startMillis?.let { it.toFloat() / durationMillis } val end = endMillis?.let { it.toFloat() / durationMillis } fractionRange(start, end, builder) fractionRange(start, end, easing, builder) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +17 −10 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.compose.animation.scene.transformation import androidx.compose.animation.core.Easing import androidx.compose.animation.core.LinearEasing import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastCoerceIn Loading Loading @@ -90,11 +92,13 @@ internal class RangedPropertyTransformation<T>( data class TransformationRange( val start: Float, val end: Float, val easing: Easing, ) { constructor( start: Float? = null, end: Float? = null ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified) end: Float? = null, easing: Easing = LinearEasing, ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified, easing) init { require(!start.isSpecified() || (start in 0f..1f)) Loading @@ -103,17 +107,20 @@ data class TransformationRange( } /** Reverse this range. */ fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start)) fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing) /** Get the progress of this range given the global [transitionProgress]. */ fun progress(transitionProgress: Float): Float { return when { val progress = when { start.isSpecified() && end.isSpecified() -> ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f) !start.isSpecified() && !end.isSpecified() -> transitionProgress end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f) else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f) } return easing.transform(progress) } private fun Float.isSpecified() = this != BoundUnspecified Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +25 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.spring Loading Loading @@ -107,6 +108,13 @@ class TransitionDslTest { fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } fractionRange(start = 0.2f) { fade(TestElements.Foo) } fractionRange(end = 0.9f) { fade(TestElements.Foo) } fractionRange( start = 0.1f, end = 0.8f, easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ) { fade(TestElements.Foo) } } } Loading @@ -118,6 +126,11 @@ class TransitionDslTest { TransformationRange(start = 0.1f, end = 0.8f), TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f), TransformationRange( start = 0.1f, end = 0.8f, CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ), ) } Loading @@ -130,6 +143,13 @@ class TransitionDslTest { timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } timestampRange(startMillis = 200) { fade(TestElements.Foo) } timestampRange(endMillis = 400) { fade(TestElements.Foo) } timestampRange( startMillis = 100, endMillis = 300, easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ) { fade(TestElements.Foo) } } } Loading @@ -141,6 +161,11 @@ class TransitionDslTest { TransformationRange(start = 100 / 500f, end = 300 / 500f), TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f), TransformationRange( start = 100 / 500f, end = 300 / 500f, easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) ), ) } Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt 0 → 100644 +126 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.compose.animation.scene.transformation import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.testTransition import com.android.compose.test.assertSizeIsEqualTo import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EasingTest { @get:Rule val rule = createComposeRule() @Test fun testFractionRangeEasing() { val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) rule.testTransition( fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, transition = { // Scale during 4 frames. spec = tween(16 * 4, easing = LinearEasing) fractionRange(easing = easing) { scaleSize(TestElements.Foo, width = 0f, height = 0f) scaleSize(TestElements.Bar, width = 0f, height = 0f) } }, ) { // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the // transition so it starts at 200dp x 50dp. before { onElement(TestElements.Bar).assertDoesNotExist() } at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) } at(16) { // 25% linear progress is mapped to 68.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) } at(32) { // 50% linear progress is mapped to 89.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) } at(48) { // 75% linear progress is mapped to 97.8% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) } after { onElement(TestElements.Foo).assertDoesNotExist() onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) } } } @Test fun testTimestampRangeEasing() { val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) rule.testTransition( fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, transition = { // Scale during 4 frames. spec = tween(16 * 4, easing = LinearEasing) timestampRange(easing = easing) { scaleSize(TestElements.Foo, width = 0f, height = 0f) scaleSize(TestElements.Bar, width = 0f, height = 0f) } }, ) { // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the // transition so it starts at 200dp x 50dp. before { onElement(TestElements.Bar).assertDoesNotExist() } at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) } at(16) { // 25% linear progress is mapped to 68.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) } at(32) { // 50% linear progress is mapped to 89.5% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) } at(48) { // 75% linear progress is mapped to 97.8% eased progress onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) } after { onElement(TestElements.Foo).assertDoesNotExist() onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) } } } }