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.compose.animation
18 
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.Column
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.fillMaxHeight
24 import androidx.compose.foundation.layout.fillMaxWidth
25 import androidx.compose.foundation.layout.size
26 import androidx.compose.runtime.getValue
27 import androidx.compose.runtime.mutableStateOf
28 import androidx.compose.runtime.setValue
29 import androidx.compose.ui.Modifier
30 import androidx.compose.ui.platform.testTag
31 import androidx.compose.ui.test.assertHeightIsEqualTo
32 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
33 import androidx.compose.ui.test.assertWidthIsEqualTo
34 import androidx.compose.ui.test.junit4.createComposeRule
35 import androidx.compose.ui.test.onNodeWithTag
36 import androidx.compose.ui.unit.Dp
37 import androidx.compose.ui.unit.dp
38 import androidx.compose.ui.unit.times
39 import androidx.test.ext.junit.runners.AndroidJUnit4
40 import org.junit.Rule
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 
44 @RunWith(AndroidJUnit4::class)
45 class BounceableTest {
46     @get:Rule val rule = createComposeRule()
47 
48     @Test
49     fun bounceable_horizontal() {
50         var bounceables by mutableStateOf(List(4) { bounceable(0.dp) })
51 
52         rule.setContent {
53             Row(Modifier.size(100.dp, 50.dp)) {
54                 repeat(bounceables.size) { i ->
55                     Box(
56                         Modifier.weight(1f)
57                             .fillMaxHeight()
58                             .bounceable(bounceables, i, orientation = Orientation.Horizontal)
59                     )
60                 }
61             }
62         }
63 
64         // All bounceables have a width of (100dp / bounceables.size) = 25dp and height of 50dp.
65         repeat(bounceables.size) { i ->
66             rule
67                 .onNodeWithTag(bounceableTag(i))
68                 .assertWidthIsEqualTo(25.dp)
69                 .assertHeightIsEqualTo(50.dp)
70                 .assertPositionInRootIsEqualTo(i * 25.dp, 0.dp)
71         }
72 
73         // If all bounceables have the same bounce, it's the same as if they didn't have any.
74         bounceables = List(4) { bounceable(10.dp) }
75         repeat(bounceables.size) { i ->
76             rule
77                 .onNodeWithTag(bounceableTag(i))
78                 .assertWidthIsEqualTo(25.dp)
79                 .assertHeightIsEqualTo(50.dp)
80                 .assertPositionInRootIsEqualTo(i * 25.dp, 0.dp)
81         }
82 
83         // Bounce the first and third one.
84         bounceables =
85             listOf(
86                 bounceable(bounce = 5.dp),
87                 bounceable(bounce = 0.dp),
88                 bounceable(bounce = 10.dp),
89                 bounceable(bounce = 0.dp),
90             )
91 
92         // First one has a width of 25dp + 5dp, located in (0, 0).
93         rule
94             .onNodeWithTag(bounceableTag(0))
95             .assertWidthIsEqualTo(30.dp)
96             .assertHeightIsEqualTo(50.dp)
97             .assertPositionInRootIsEqualTo(0.dp, 0.dp)
98 
99         // Second one has a width of 25dp - 5dp - 10dp, located in (30, 0).
100         rule
101             .onNodeWithTag(bounceableTag(1))
102             .assertWidthIsEqualTo(10.dp)
103             .assertHeightIsEqualTo(50.dp)
104             .assertPositionInRootIsEqualTo(30.dp, 0.dp)
105 
106         // Third one has a width of 25 + 2 * 10dp, located in (40, 0).
107         rule
108             .onNodeWithTag(bounceableTag(2))
109             .assertWidthIsEqualTo(45.dp)
110             .assertHeightIsEqualTo(50.dp)
111             .assertPositionInRootIsEqualTo(40.dp, 0.dp)
112 
113         // First one has a width of 25dp - 10dp, located in (85, 0).
114         rule
115             .onNodeWithTag(bounceableTag(3))
116             .assertWidthIsEqualTo(15.dp)
117             .assertHeightIsEqualTo(50.dp)
118             .assertPositionInRootIsEqualTo(85.dp, 0.dp)
119     }
120 
121     @Test
122     fun bounceable_vertical() {
123         var bounceables by mutableStateOf(List(4) { bounceable(0.dp) })
124 
125         rule.setContent {
126             Column(Modifier.size(50.dp, 100.dp)) {
127                 repeat(bounceables.size) { i ->
128                     Box(
129                         Modifier.weight(1f)
130                             .fillMaxWidth()
131                             .bounceable(bounceables, i, Orientation.Vertical)
132                     )
133                 }
134             }
135         }
136 
137         // All bounceables have a height of (100dp / bounceables.size) = 25dp and width of 50dp.
138         repeat(bounceables.size) { i ->
139             rule
140                 .onNodeWithTag(bounceableTag(i))
141                 .assertWidthIsEqualTo(50.dp)
142                 .assertHeightIsEqualTo(25.dp)
143                 .assertPositionInRootIsEqualTo(0.dp, i * 25.dp)
144         }
145 
146         // If all bounceables have the same bounce, it's the same as if they didn't have any.
147         bounceables = List(4) { bounceable(10.dp) }
148         repeat(bounceables.size) { i ->
149             rule
150                 .onNodeWithTag(bounceableTag(i))
151                 .assertWidthIsEqualTo(50.dp)
152                 .assertHeightIsEqualTo(25.dp)
153                 .assertPositionInRootIsEqualTo(0.dp, i * 25.dp)
154         }
155 
156         // Bounce the first and third one.
157         bounceables =
158             listOf(
159                 bounceable(bounce = 5.dp),
160                 bounceable(bounce = 0.dp),
161                 bounceable(bounce = 10.dp),
162                 bounceable(bounce = 0.dp),
163             )
164 
165         // First one has a height of 25dp + 5dp, located in (0, 0).
166         rule
167             .onNodeWithTag(bounceableTag(0))
168             .assertWidthIsEqualTo(50.dp)
169             .assertHeightIsEqualTo(30.dp)
170             .assertPositionInRootIsEqualTo(0.dp, 0.dp)
171 
172         // Second one has a height of 25dp - 5dp - 10dp, located in (0, 30).
173         rule
174             .onNodeWithTag(bounceableTag(1))
175             .assertWidthIsEqualTo(50.dp)
176             .assertHeightIsEqualTo(10.dp)
177             .assertPositionInRootIsEqualTo(0.dp, 30.dp)
178 
179         // Third one has a height of 25 + 2 * 10dp, located in (0, 40).
180         rule
181             .onNodeWithTag(bounceableTag(2))
182             .assertWidthIsEqualTo(50.dp)
183             .assertHeightIsEqualTo(45.dp)
184             .assertPositionInRootIsEqualTo(0.dp, 40.dp)
185 
186         // First one has a height of 25dp - 10dp, located in (0, 85).
187         rule
188             .onNodeWithTag(bounceableTag(3))
189             .assertWidthIsEqualTo(50.dp)
190             .assertHeightIsEqualTo(15.dp)
191             .assertPositionInRootIsEqualTo(0.dp, 85.dp)
192     }
193 
194     private fun bounceable(bounce: Dp): Bounceable {
195         return object : Bounceable {
196             override val bounce: Dp = bounce
197         }
198     }
199 
200     private fun Modifier.bounceable(
201         bounceables: List<Bounceable>,
202         i: Int,
203         orientation: Orientation,
204     ): Modifier {
205         val previous = if (i > 0) bounceables[i - 1] else null
206         val next = if (i < bounceables.lastIndex) bounceables[i + 1] else null
207         return this.bounceable(bounceables[i], previous, next, orientation)
208             .testTag(bounceableTag(i))
209     }
210 
211     private fun bounceableTag(i: Int) = "bounceable$i"
212 }
213