1 /*
2  * Copyright (C) 2022 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 distributed under the
11  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
12  * KIND, either express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 package com.android.systemui.shared.animation
16 
17 import android.testing.AndroidTestingRunner
18 import android.view.View
19 import android.view.ViewGroup
20 import androidx.test.filters.SmallTest
21 import com.android.systemui.SysuiTestCase
22 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction
23 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate
24 import com.android.systemui.unfold.TestUnfoldTransitionProvider
25 import org.junit.Assert.assertEquals
26 import org.junit.Before
27 import org.junit.Test
28 import org.junit.runner.RunWith
29 import org.mockito.Mock
30 import org.mockito.Mockito.`when` as whenever
31 import org.mockito.MockitoAnnotations
32 
33 @SmallTest
34 @RunWith(AndroidTestingRunner::class)
35 class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() {
36 
37     private val progressProvider = TestUnfoldTransitionProvider()
38 
39     @Mock
40     private lateinit var parent: ViewGroup
41 
42     @Mock
43     private lateinit var shouldBeAnimated: () -> Boolean
44 
45     private lateinit var animator: UnfoldConstantTranslateAnimator
46 
47     private val viewsIdToRegister
48         get() =
49             setOf(
50                     ViewIdToTranslate(START_VIEW_ID, Direction.START, shouldBeAnimated),
51                     ViewIdToTranslate(END_VIEW_ID, Direction.END, shouldBeAnimated)
52             )
53 
54     @Before
55     fun setup() {
56         MockitoAnnotations.initMocks(this)
57         whenever(shouldBeAnimated.invoke()).thenReturn(true)
58         animator = UnfoldConstantTranslateAnimator(viewsIdToRegister, progressProvider)
59 
60         animator.init(parent, MAX_TRANSLATION)
61     }
62 
63     @Test
64     fun onTransition_noMatchingIds() {
65         // GIVEN no views matching any ids
66         // WHEN the transition starts
67         progressProvider.onTransitionStarted()
68         progressProvider.onTransitionProgress(.1f)
69 
70         // THEN nothing... no exceptions
71     }
72 
73     @Test
74     fun onTransition_oneMovesStartWithLTR() {
75         // GIVEN one view with a matching id
76         val view = View(context)
77         whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(view)
78 
79         moveAndValidate(listOf(view to START), View.LAYOUT_DIRECTION_LTR)
80     }
81 
82     @Test
83     fun onTransition_oneMovesStartWithRTL() {
84         // GIVEN one view with a matching id
85         val view = View(context)
86         whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(view)
87 
88         whenever(parent.getLayoutDirection()).thenReturn(View.LAYOUT_DIRECTION_RTL)
89         moveAndValidate(listOf(view to START), View.LAYOUT_DIRECTION_RTL)
90     }
91 
92     @Test
93     fun onTransition_oneMovesStartAndOneMovesEndMultipleTimes() {
94         // GIVEN two views with a matching id
95         val leftView = View(context)
96         val rightView = View(context)
97         whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(leftView)
98         whenever(parent.findViewById<View>(END_VIEW_ID)).thenReturn(rightView)
99 
100         moveAndValidate(listOf(leftView to START, rightView to END), View.LAYOUT_DIRECTION_LTR)
101         moveAndValidate(listOf(leftView to START, rightView to END), View.LAYOUT_DIRECTION_LTR)
102     }
103 
104     @Test
105     fun onTransition_completeStartedTranslation() {
106         // GIVEN
107         val leftView = View(context)
108         whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(leftView)
109         // To start animation, shouldBeAnimated should return true.
110         // There is a possibility for shouldBeAnimated to return false during the animation.
111         whenever(shouldBeAnimated.invoke()).thenReturn(true).thenReturn(false)
112 
113         // shouldBeAnimated state may change during the animation.
114         // However, started animation should be completed.
115         moveAndValidate(listOf(leftView to START), View.LAYOUT_DIRECTION_LTR)
116     }
117 
118     private fun moveAndValidate(list: List<Pair<View, Int>>, layoutDirection: Int) {
119         // Compare values as ints because -0f != 0f
120 
121         // WHEN the transition starts
122         progressProvider.onTransitionStarted()
123         progressProvider.onTransitionProgress(0f)
124 
125         val rtlMultiplier = if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
126             1
127         } else {
128             -1
129         }
130         list.forEach { (view, direction) ->
131             assertEquals(
132                 (-MAX_TRANSLATION * direction * rtlMultiplier).toInt(),
133                 view.translationX.toInt()
134             )
135         }
136 
137         // WHEN the transition progresses, translation is updated
138         progressProvider.onTransitionProgress(.5f)
139         list.forEach { (view, direction) ->
140             assertEquals(
141                 (-MAX_TRANSLATION / 2f * direction * rtlMultiplier).toInt(),
142                 view.translationX.toInt()
143             )
144         }
145 
146         // WHEN the transition ends, translation is completed
147         progressProvider.onTransitionProgress(1f)
148         progressProvider.onTransitionFinished()
149         list.forEach { (view, _) -> assertEquals(0, view.translationX.toInt()) }
150     }
151 
152     companion object {
153         private val START = Direction.START.multiplier.toInt()
154         private val END = Direction.END.multiplier.toInt()
155 
156         private const val MAX_TRANSLATION = 42f
157 
158         private const val START_VIEW_ID = 1
159         private const val END_VIEW_ID = 2
160     }
161 }
162