Add benchmark test for MotionScene
MotionScene is an object that represents the animations of a
MotionLayout.
This object can be failry complex and building it may be more costly
than it should.
As such, this change adds a couple of benchmarking test for MotionScene.
With the purpose to track its performance as we work to improve on it.
Bug: 291331242
Test: n/a
Change-Id: I8ee4dd0a91504b12a6491bd4914930a65ba17a7d
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
new file mode 100644
index 0000000..57aa1a8
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+ id("androidx.benchmark")
+}
+
+dependencies {
+ androidTestImplementation project(":constraintlayout:constraintlayout-compose")
+ androidTestImplementation project(":constraintlayout:constraintlayout-core")
+ androidTestImplementation project(":benchmark:benchmark-junit4")
+ androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.kotlinTestCommon)
+ androidTestImplementation(libs.truth)
+}
+
+android {
+ namespace "androidx.constraintlayout.compose.benchmark"
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/src/androidTest/java/androidx/constraintlayout/compose/benchmark/MotionSceneBenchmark.kt b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/src/androidTest/java/androidx/constraintlayout/compose/benchmark/MotionSceneBenchmark.kt
new file mode 100644
index 0000000..f3acb7c
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/src/androidTest/java/androidx/constraintlayout/compose/benchmark/MotionSceneBenchmark.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright 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 androidx.constraintlayout.compose.benchmark
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.Dimension
+import androidx.constraintlayout.compose.Easing
+import androidx.constraintlayout.compose.MotionScene
+import androidx.constraintlayout.compose.OnSwipe
+import androidx.constraintlayout.compose.SwipeDirection
+import androidx.constraintlayout.compose.SwipeMode
+import androidx.constraintlayout.compose.SwipeSide
+import androidx.constraintlayout.compose.SwipeTouchUp
+import androidx.constraintlayout.compose.Visibility
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class MotionSceneBenchmark {
+ @get:Rule
+ val benchmarkRule = BenchmarkRule()
+
+ /**
+ * One of the most basics MotionScenes.
+ *
+ * Just a box moving from one corner to the other. Fairly minimal example.
+ */
+ @Test
+ fun motionScene_simple() {
+ benchmarkRule.measureRepeated {
+ MotionScene {
+ val boxRef = createRefFor("box")
+ defaultTransition(
+ from = constraintSet {
+ constrain(boxRef) {
+ width = 50.dp.asDimension()
+ height = 50.dp.asDimension()
+
+ top.linkTo(parent.top, 8.dp)
+ start.linkTo(parent.start, 8.dp)
+ }
+ },
+ to = constraintSet {
+ constrain(boxRef) {
+ width = 50.dp.asDimension()
+ height = 50.dp.asDimension()
+
+ bottom.linkTo(parent.bottom, 8.dp)
+ end.linkTo(parent.end, 8.dp)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ /**
+ * The MotionScene was mostly a copy of `messageMotionScene()` from NewMessage.kt in the
+ * macrobenchmark-target module.
+ *
+ * It's been modified to represent a more complex scenario. Does not necessarily have to make
+ * sense since it's for benchmarking.
+ */
+ @Test
+ fun motionScene_complex() {
+ val primary = Color(0xFFF44336)
+ val primaryVariant = Color(0xFFE91E63)
+ val onPrimary = Color(0xFF673AB7)
+ val surface = Color(0xFF3F51B5)
+ val onSurface = Color(0xFF2196F3)
+
+ benchmarkRule.measureRepeated {
+ MotionScene {
+ val (box, minIcon, editClose, title, content) =
+ createRefsFor("box", "minIcon", "editClose", "title", "content")
+
+ val fab = constraintSet(NewMessageLayout.Fab.name) {
+ constrain(box) {
+ width = Dimension.value(50.dp)
+ height = Dimension.value(50.dp)
+ end.linkTo(parent.end, 12.dp)
+ bottom.linkTo(parent.bottom, 12.dp)
+
+ customColor("background", primary)
+
+ staggeredWeight = 1f
+ }
+ constrain(minIcon) {
+ width = Dimension.value(40.dp)
+ height = Dimension.value(40.dp)
+
+ end.linkTo(editClose.start, 8.dp)
+ top.linkTo(editClose.top)
+ customColor("content", onPrimary)
+ }
+ constrain(editClose) {
+ width = Dimension.value(40.dp)
+ height = Dimension.value(40.dp)
+
+ centerTo(box)
+
+ customColor("content", onPrimary)
+ }
+ constrain(title) {
+ width = Dimension.fillToConstraints
+ top.linkTo(box.top)
+ bottom.linkTo(editClose.bottom)
+ start.linkTo(box.start, 8.dp)
+ end.linkTo(minIcon.start, 8.dp)
+ customColor("content", onPrimary)
+
+ visibility = Visibility.Gone
+ }
+ constrain(content) {
+ width = Dimension.fillToConstraints
+ height = Dimension.fillToConstraints
+ start.linkTo(box.start, 8.dp)
+ end.linkTo(box.end, 8.dp)
+
+ top.linkTo(editClose.bottom, 8.dp)
+ bottom.linkTo(box.bottom, 8.dp)
+
+ visibility = Visibility.Gone
+ }
+ }
+ val full = constraintSet(NewMessageLayout.Full.name) {
+ constrain(box) {
+ width = Dimension.fillToConstraints
+ height = Dimension.fillToConstraints
+ start.linkTo(parent.start, 12.dp)
+ end.linkTo(parent.end, 12.dp)
+ bottom.linkTo(parent.bottom, 12.dp)
+ top.linkTo(parent.top, 40.dp)
+ customColor("background", surface)
+ }
+ constrain(minIcon) {
+ width = Dimension.value(40.dp)
+ height = Dimension.value(40.dp)
+
+ end.linkTo(editClose.start, 8.dp)
+ top.linkTo(editClose.top)
+ customColor("content", onSurface)
+ }
+ constrain(editClose) {
+ width = Dimension.value(40.dp)
+ height = Dimension.value(40.dp)
+
+ end.linkTo(box.end, 4.dp)
+ top.linkTo(box.top, 4.dp)
+ customColor("content", onSurface)
+ }
+ constrain(title) {
+ width = Dimension.fillToConstraints
+ top.linkTo(box.top)
+ bottom.linkTo(editClose.bottom)
+ start.linkTo(box.start, 8.dp)
+ end.linkTo(minIcon.start, 8.dp)
+ customColor("content", onSurface)
+ }
+ constrain(content) {
+ width = Dimension.fillToConstraints
+ height = Dimension.fillToConstraints
+ start.linkTo(box.start, 8.dp)
+ end.linkTo(box.end, 8.dp)
+ top.linkTo(editClose.bottom, 8.dp)
+ bottom.linkTo(box.bottom, 8.dp)
+ }
+ }
+ val mini = constraintSet(NewMessageLayout.Mini.name) {
+ constrain(box) {
+ width = Dimension.value(220.dp)
+ height = Dimension.value(50.dp)
+
+ end.linkTo(parent.end, 12.dp)
+ bottom.linkTo(parent.bottom, 12.dp)
+
+ customColor("background", primaryVariant)
+ }
+ constrain(minIcon) {
+ width = Dimension.value(40.dp)
+ height = Dimension.value(40.dp)
+
+ end.linkTo(editClose.start, 8.dp)
+ top.linkTo(editClose.top)
+
+ rotationZ = 180f
+
+ customColor("content", onPrimary)
+ }
+ constrain(editClose) {
+ width = Dimension.value(40.dp)
+ height = Dimension.value(40.dp)
+
+ end.linkTo(box.end, 4.dp)
+ top.linkTo(box.top, 4.dp)
+ customColor("content", onPrimary)
+ }
+ constrain(title) {
+ width = Dimension.fillToConstraints
+ top.linkTo(box.top)
+ bottom.linkTo(editClose.bottom)
+ start.linkTo(box.start, 8.dp)
+ end.linkTo(minIcon.start, 8.dp)
+ customColor("content", onPrimary)
+ }
+ constrain(content) {
+ width = Dimension.fillToConstraints
+ start.linkTo(box.start, 8.dp)
+ end.linkTo(box.end, 8.dp)
+
+ top.linkTo(editClose.bottom, 8.dp)
+ bottom.linkTo(box.bottom, 8.dp)
+
+ visibility = Visibility.Gone
+ }
+ }
+
+ fun constraintSetFor(layoutState: NewMessageLayout) =
+ when (layoutState) {
+ NewMessageLayout.Full -> full
+ NewMessageLayout.Mini -> mini
+ NewMessageLayout.Fab -> fab
+ }
+ defaultTransition(
+ from = constraintSetFor(NewMessageLayout.Fab),
+ to = constraintSetFor(NewMessageLayout.Full)
+ ) {
+ maxStaggerDelay = 0.6f
+
+ keyAttributes(title, content) {
+ frame(30) {
+ alpha = 0.5f
+ }
+ frame(60) {
+ alpha = 0.9f
+ }
+ }
+ }
+
+ transition(
+ from = constraintSetFor(NewMessageLayout.Full),
+ to = constraintSetFor(NewMessageLayout.Mini)
+ ) {
+ onSwipe = OnSwipe(
+ anchor = editClose,
+ side = SwipeSide.Middle,
+ direction = SwipeDirection.Down,
+ onTouchUp = SwipeTouchUp.AutoComplete,
+ mode = SwipeMode.spring(threshold = 0.001f)
+ )
+
+ keyCycles(minIcon) {
+ easing = Easing.cubic(x1 = 0.3f, y1 = 0.2f, x2 = 0.8f, y2 = 0.7f)
+ frame(50) {
+ rotationZ = 90f
+ period = 4f
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private enum class NewMessageLayout {
+ Full,
+ Mini,
+ Fab
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 7f88274..6d5d849 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -667,6 +667,7 @@
includeProject(":constraintlayout:constraintlayout-compose:integration-tests:demos", [BuildType.COMPOSE])
includeProject(":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark", [BuildType.COMPOSE])
includeProject(":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
+includeProject(":constraintlayout:constraintlayout-compose:integration-tests:compose-benchmark", [BuildType.COMPOSE])
includeProject(":constraintlayout:constraintlayout", [BuildType.MAIN])
includeProject(":constraintlayout:constraintlayout-core", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":contentpager:contentpager", [BuildType.MAIN])