1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.animation 18 19 import android.animation.AnimatorRuleRecordingSpec 20 import android.animation.AnimatorTestRuleToolkit 21 import android.animation.MotionControl 22 import android.animation.recordMotion 23 import android.graphics.Color 24 import android.graphics.PointF 25 import android.graphics.drawable.GradientDrawable 26 import android.platform.test.annotations.MotionTest 27 import android.view.ViewGroup 28 import android.widget.FrameLayout 29 import androidx.test.ext.junit.rules.ActivityScenarioRule 30 import androidx.test.filters.SmallTest 31 import com.android.systemui.SysuiTestCase 32 import com.android.systemui.activity.EmptyTestActivity 33 import com.android.systemui.concurrency.fakeExecutor 34 import com.android.systemui.kosmos.Kosmos 35 import com.android.systemui.kosmos.testScope 36 import com.android.systemui.runOnMainThreadAndWaitForIdleSync 37 import kotlin.test.assertTrue 38 import org.junit.Rule 39 import org.junit.Test 40 import org.junit.runner.RunWith 41 import platform.test.motion.MotionTestRule 42 import platform.test.motion.RecordedMotion 43 import platform.test.motion.view.DrawableFeatureCaptures 44 import platform.test.runner.parameterized.ParameterizedAndroidJunit4 45 import platform.test.runner.parameterized.Parameters 46 import platform.test.screenshot.GoldenPathManager 47 import platform.test.screenshot.PathConfig 48 49 @SmallTest 50 @MotionTest 51 @RunWith(ParameterizedAndroidJunit4::class) 52 class TransitionAnimatorTest( 53 private val fadeWindowBackgroundLayer: Boolean, 54 private val isLaunching: Boolean, 55 private val useSpring: Boolean, 56 ) : SysuiTestCase() { 57 companion object { 58 private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens" 59 60 @get:Parameters(name = "fadeBackground={0}, isLaunching={1}, useSpring={2}") 61 @JvmStatic 62 val parameterValues = buildList { 63 booleanArrayOf(true, false).forEach { fadeBackground -> 64 booleanArrayOf(true, false).forEach { isLaunching -> 65 booleanArrayOf(true, false).forEach { useSpring -> 66 add(arrayOf(fadeBackground, isLaunching, useSpring)) 67 } 68 } 69 } 70 } 71 } 72 73 private val kosmos = Kosmos() 74 private val pathManager = GoldenPathManager(context, GOLDENS_PATH, pathConfig = PathConfig()) 75 private val transitionAnimator = 76 TransitionAnimator( 77 kosmos.fakeExecutor, 78 ActivityTransitionAnimator.TIMINGS, 79 ActivityTransitionAnimator.INTERPOLATORS, 80 ActivityTransitionAnimator.SPRING_TIMINGS, 81 ActivityTransitionAnimator.SPRING_INTERPOLATORS, 82 ) 83 private val fade = 84 if (fadeWindowBackgroundLayer) { 85 "withFade" 86 } else { 87 "withoutFade" 88 } 89 private val direction = 90 if (isLaunching) { 91 "whenLaunching" 92 } else { 93 "whenReturning" 94 } 95 private val mode = 96 if (useSpring) { 97 "withSpring" 98 } else { 99 "withAnimator" 100 } 101 102 @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java) 103 @get:Rule(order = 2) val animatorTestRule = android.animation.AnimatorTestRule(this) 104 @get:Rule(order = 3) 105 val motionRule = 106 MotionTestRule( 107 AnimatorTestRuleToolkit(animatorTestRule, kosmos.testScope) { activityRule.scenario }, 108 pathManager, 109 ) 110 111 @Test 112 fun backgroundAnimationTimeSeries() { 113 val transitionContainer = createScene() 114 val backgroundLayer = createBackgroundLayer() 115 val animation = createAnimation(transitionContainer, backgroundLayer) 116 117 val recordedMotion = record(backgroundLayer, animation) 118 119 motionRule 120 .assertThat(recordedMotion) 121 .timeSeriesMatchesGolden("backgroundAnimationTimeSeries_${fade}_${direction}_$mode") 122 } 123 124 private fun createScene(): ViewGroup { 125 lateinit var transitionContainer: ViewGroup 126 activityRule.scenario.onActivity { activity -> 127 transitionContainer = FrameLayout(activity) 128 activity.setContentView(transitionContainer) 129 } 130 waitForIdleSync() 131 return transitionContainer 132 } 133 134 private fun createBackgroundLayer() = 135 GradientDrawable().apply { 136 setColor(Color.BLACK) 137 alpha = 0 138 } 139 140 private fun createAnimation( 141 transitionContainer: ViewGroup, 142 backgroundLayer: GradientDrawable, 143 ): TransitionAnimator.Animation { 144 val controller = TestController(transitionContainer, isLaunching) 145 146 val containerLocation = IntArray(2) 147 transitionContainer.getLocationOnScreen(containerLocation) 148 val endState = 149 TransitionAnimator.State( 150 left = containerLocation[0], 151 top = containerLocation[1], 152 right = containerLocation[0] + 320, 153 bottom = containerLocation[1] + 690, 154 topCornerRadius = 0f, 155 bottomCornerRadius = 0f, 156 ) 157 158 val startVelocity = 159 if (useSpring) { 160 PointF(2500f, 30000f) 161 } else { 162 null 163 } 164 165 return transitionAnimator 166 .createAnimation( 167 controller, 168 controller.createAnimatorState(), 169 endState, 170 backgroundLayer, 171 fadeWindowBackgroundLayer, 172 startVelocity = startVelocity, 173 ) 174 .apply { runOnMainThreadAndWaitForIdleSync { start() } } 175 } 176 177 private fun record( 178 backgroundLayer: GradientDrawable, 179 animation: TransitionAnimator.Animation, 180 ): RecordedMotion { 181 val motionControl: MotionControl 182 val sampleIntervalMs: Long 183 if (useSpring) { 184 assertTrue { animation is TransitionAnimator.MultiSpringAnimation } 185 motionControl = MotionControl { 186 awaitCondition { (animation as TransitionAnimator.MultiSpringAnimation).isDone } 187 } 188 sampleIntervalMs = 16L 189 } else { 190 assertTrue { animation is TransitionAnimator.InterpolatedAnimation } 191 motionControl = MotionControl { awaitFrames(count = 26) } 192 sampleIntervalMs = 20L 193 } 194 195 return motionRule.recordMotion( 196 AnimatorRuleRecordingSpec(backgroundLayer, motionControl, sampleIntervalMs) { 197 feature(DrawableFeatureCaptures.bounds, "bounds") 198 feature(DrawableFeatureCaptures.cornerRadii, "corner_radii") 199 feature(DrawableFeatureCaptures.alpha, "alpha") 200 } 201 ) 202 } 203 } 204 205 /** 206 * A simple implementation of [TransitionAnimator.Controller] which throws if it is called outside 207 * of the main thread. 208 */ 209 private class TestController( 210 override var transitionContainer: ViewGroup, 211 override val isLaunching: Boolean, 212 ) : TransitionAnimator.Controller { createAnimatorStatenull213 override fun createAnimatorState(): TransitionAnimator.State { 214 val containerLocation = IntArray(2) 215 transitionContainer.getLocationOnScreen(containerLocation) 216 return TransitionAnimator.State( 217 left = containerLocation[0] + 100, 218 top = containerLocation[1] + 300, 219 right = containerLocation[0] + 200, 220 bottom = containerLocation[1] + 400, 221 topCornerRadius = 10f, 222 bottomCornerRadius = 20f, 223 ) 224 } 225 } 226