[go: nahoru, domu]

blob: 0dfbea341d9a44d6115faf61d22c12028473d20e [file] [log] [blame]
Matvei Malkov59fa63f2020-03-24 17:15:50 +00001/*
2 * Copyright 2020 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
Louis Pullen-Freilichddda7be2020-07-17 18:28:12 +010017package androidx.compose.foundation
Matvei Malkov59fa63f2020-03-24 17:15:50 +000018
19import android.os.Handler
20import android.os.Looper
Louis Pullen-Freilich5bb0c4712020-07-20 22:01:15 +010021import androidx.compose.animation.core.ExponentialDecay
22import androidx.compose.animation.core.ManualAnimationClock
Matvei Malkov59fa63f2020-03-24 17:15:50 +000023import androidx.compose.Composable
Adam Powell5f07ae82020-03-30 14:36:42 -070024import androidx.compose.getValue
Matvei Malkov59fa63f2020-03-24 17:15:50 +000025import androidx.compose.mutableStateOf
Adam Powell5f07ae82020-03-30 14:36:42 -070026import androidx.compose.setValue
Matvei Malkov59fa63f2020-03-24 17:15:50 +000027import androidx.test.filters.SmallTest
28import androidx.ui.core.Modifier
Matvei Malkov710fb4f2020-07-03 15:53:10 +010029import androidx.ui.core.gesture.scrollorientationlocking.Orientation
Filip Pavlis6de64f52020-05-27 18:11:55 +010030import androidx.ui.core.testTag
Louis Pullen-Freilichddda7be2020-07-17 18:28:12 +010031import androidx.compose.foundation.animation.FlingConfig
32import androidx.compose.foundation.gestures.ScrollableController
33import androidx.compose.foundation.gestures.scrollable
Louis Pullen-Freilich623e4052020-07-19 20:24:03 +010034import androidx.compose.foundation.layout.Stack
35import androidx.compose.foundation.layout.preferredSize
Matvei Malkov59fa63f2020-03-24 17:15:50 +000036import androidx.ui.test.center
37import androidx.ui.test.createComposeRule
Filip Pavlis659ea722020-07-13 14:14:32 +010038import androidx.ui.test.performGesture
39import androidx.ui.test.onNodeWithTag
Filip Pavlisa4d73a52020-07-14 11:30:00 +010040import androidx.ui.test.runOnIdle
Filip Pavlis6fdb2502020-04-02 14:41:32 +010041import androidx.ui.test.runOnUiThread
Filip Pavlis659ea722020-07-13 14:14:32 +010042import androidx.ui.test.swipe
43import androidx.ui.test.swipeWithVelocity
Nader Jawad6df06122020-06-03 15:27:08 -070044import androidx.ui.geometry.Offset
Matvei Malkov59fa63f2020-03-24 17:15:50 +000045import androidx.ui.unit.dp
46import androidx.ui.unit.milliseconds
Matvei Malkov59fa63f2020-03-24 17:15:50 +000047import com.google.common.truth.Truth
48import com.google.common.truth.Truth.assertThat
49import org.junit.Rule
50import org.junit.Test
51import org.junit.runner.RunWith
52import org.junit.runners.JUnit4
53import java.util.concurrent.CountDownLatch
54import java.util.concurrent.TimeUnit
55
56@SmallTest
57@RunWith(JUnit4::class)
58class ScrollableTest {
59
60 @get:Rule
61 val composeTestRule = createComposeRule()
62
63 private val scrollableBoxTag = "scrollableBox"
64
65 @Test
66 fun scrollable_horizontalScroll() {
67 val clocks = ManualAnimationClock(0L)
68 var total = 0f
Matvei Malkov710fb4f2020-07-03 15:53:10 +010069 val controller = ScrollableController(
70 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +000071 total += it
72 it
73 },
74 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
75 animationClock = clocks
76 )
77 setScrollableContent {
Matvei Malkov4a945c32020-03-27 16:27:23 +000078 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +010079 controller = controller,
80 orientation = Orientation.Horizontal
Matvei Malkov59fa63f2020-03-24 17:15:50 +000081 )
82 }
Filip Pavlis659ea722020-07-13 14:14:32 +010083 onNodeWithTag(scrollableBoxTag).performGesture {
84 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +000085 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -070086 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +000087 duration = 100.milliseconds
88 )
89 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +010090 advanceClockAndAwaitAnimation(controller, clocks)
Matvei Malkov59fa63f2020-03-24 17:15:50 +000091
Filip Pavlisa4d73a52020-07-14 11:30:00 +010092 val lastTotal = runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +000093 assertThat(total).isGreaterThan(0)
94 total
95 }
Filip Pavlis659ea722020-07-13 14:14:32 +010096 onNodeWithTag(scrollableBoxTag).performGesture {
97 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +000098 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -070099 end = Offset(this.center.x, this.center.y + 100f),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000100 duration = 100.milliseconds
101 )
102 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100103 advanceClockAndAwaitAnimation(controller, clocks)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000104
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100105 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000106 assertThat(total).isEqualTo(lastTotal)
107 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100108 onNodeWithTag(scrollableBoxTag).performGesture {
109 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000110 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700111 end = Offset(this.center.x - 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000112 duration = 100.milliseconds
113 )
114 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100115 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100116 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000117 assertThat(total).isLessThan(0.01f)
118 }
119 }
120
121 @Test
122 fun scrollable_verticalScroll() {
123 val clocks = ManualAnimationClock(0L)
124 var total = 0f
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100125 val controller = ScrollableController(
126 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000127 total += it
128 it
129 },
130 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
131 animationClock = clocks
132 )
133 setScrollableContent {
Matvei Malkov4a945c32020-03-27 16:27:23 +0000134 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100135 controller = controller,
136 orientation = Orientation.Vertical
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000137 )
138 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100139 onNodeWithTag(scrollableBoxTag).performGesture {
140 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000141 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700142 end = Offset(this.center.x, this.center.y + 100f),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000143 duration = 100.milliseconds
144 )
145 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100146 advanceClockAndAwaitAnimation(controller, clocks)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000147
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100148 val lastTotal = runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000149 assertThat(total).isGreaterThan(0)
150 total
151 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100152 onNodeWithTag(scrollableBoxTag).performGesture {
153 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000154 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700155 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000156 duration = 100.milliseconds
157 )
158 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100159 advanceClockAndAwaitAnimation(controller, clocks)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000160
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100161 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000162 assertThat(total).isEqualTo(lastTotal)
163 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100164 onNodeWithTag(scrollableBoxTag).performGesture {
165 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000166 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700167 end = Offset(this.center.x, this.center.y - 100f),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000168 duration = 100.milliseconds
169 )
170 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100171 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100172 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000173 assertThat(total).isLessThan(0.01f)
174 }
175 }
176
177 @Test
178 fun scrollable_startStop_notify() {
179 var startTrigger = 0f
180 var stopTrigger = 0f
181 val clocks = ManualAnimationClock(0L)
182 var total = 0f
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100183 val controller = ScrollableController(
184 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000185 total += it
186 it
187 },
188 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
189 animationClock = clocks
190 )
191 setScrollableContent {
Matvei Malkov4a945c32020-03-27 16:27:23 +0000192 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100193 controller = controller,
194 orientation = Orientation.Horizontal,
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000195 onScrollStarted = { startTrigger++ },
196 onScrollStopped = { stopTrigger++ }
197 )
198 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100199 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000200 assertThat(startTrigger).isEqualTo(0)
201 assertThat(stopTrigger).isEqualTo(0)
202 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100203 onNodeWithTag(scrollableBoxTag).performGesture {
204 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000205 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700206 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000207 duration = 100.milliseconds
208 )
209 }
210 // don't wait for animation so stop is 0, as we flinging still
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100211 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000212 assertThat(startTrigger).isEqualTo(1)
213 assertThat(stopTrigger).isEqualTo(0)
214 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100215 advanceClockAndAwaitAnimation(controller, clocks)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000216 // after wait we expect stop to trigger
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100217 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000218 assertThat(startTrigger).isEqualTo(1)
219 assertThat(stopTrigger).isEqualTo(1)
220 }
221 }
222
223 @Test
224 fun scrollable_disabledWontCallLambda() {
225 var enabled = mutableStateOf(true)
226 val clocks = ManualAnimationClock(0L)
227 var total = 0f
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100228 val controller = ScrollableController(
229 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000230 total += it
231 it
232 },
233 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
234 animationClock = clocks
235 )
236 setScrollableContent {
Matvei Malkov4a945c32020-03-27 16:27:23 +0000237 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100238 controller = controller,
239 orientation = Orientation.Horizontal,
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000240 enabled = enabled.value
241 )
242 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100243 onNodeWithTag(scrollableBoxTag).performGesture {
244 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000245 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700246 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000247 duration = 100.milliseconds
248 )
249 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100250 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100251 val prevTotal = runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000252 assertThat(total).isGreaterThan(0f)
253 enabled.value = false
254 total
255 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100256 onNodeWithTag(scrollableBoxTag).performGesture {
257 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000258 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700259 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000260 duration = 100.milliseconds
261 )
262 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100263 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100264 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000265 assertThat(total).isEqualTo(prevTotal)
266 }
267 }
268
269 @Test
270 fun scrollable_velocityProxy() {
271 var velocityTriggered = 0f
272 val clocks = ManualAnimationClock(0L)
273 var total = 0f
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100274 val controller = ScrollableController(
275 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000276 total += it
277 it
278 },
279 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
280 animationClock = clocks
281 )
282 setScrollableContent {
Matvei Malkov4a945c32020-03-27 16:27:23 +0000283 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100284 controller = controller,
285 orientation = Orientation.Horizontal,
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000286 onScrollStopped = { velocity ->
287 velocityTriggered = velocity
288 }
289 )
290 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100291 onNodeWithTag(scrollableBoxTag).performGesture {
292 this.swipeWithVelocity(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000293 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700294 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000295 endVelocity = 112f,
296 duration = 100.milliseconds
297
298 )
299 }
300 // don't advance clocks, so animation won't trigger yet
301 // and interrupt
Filip Pavlis659ea722020-07-13 14:14:32 +0100302 onNodeWithTag(scrollableBoxTag).performGesture {
303 this.swipeWithVelocity(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000304 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700305 end = Offset(this.center.x - 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000306 endVelocity = 312f,
307 duration = 100.milliseconds
308
309 )
310 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100311 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000312 // should be first velocity, as fling was disrupted
313 assertThat(velocityTriggered - 112f).isLessThan(0.1f)
314 }
315 }
316
317 @Test
318 fun scrollable_startWithoutSlop_ifFlinging() {
319 val clocks = ManualAnimationClock(0L)
320 var total = 0f
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100321 val controller = ScrollableController(
322 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000323 total += it
324 it
325 },
326 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
327 animationClock = clocks
328 )
329 setScrollableContent {
Matvei Malkov4a945c32020-03-27 16:27:23 +0000330 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100331 controller = controller,
332 orientation = Orientation.Horizontal
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000333 )
334 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100335 onNodeWithTag(scrollableBoxTag).performGesture {
336 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000337 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700338 end = Offset(this.center.x + 100f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000339 duration = 100.milliseconds
340 )
341 }
342 // don't advance clocks
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100343 val prevTotal = runOnUiThread {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000344 Truth.assertThat(total).isGreaterThan(0f)
345 total
346 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100347 onNodeWithTag(scrollableBoxTag).performGesture {
348 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000349 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700350 end = Offset(this.center.x + 114f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000351 duration = 100.milliseconds
352 )
353 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100354 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000355 // last swipe should add exactly 114 as we don't advance clocks and already flinging
356 val expected = prevTotal + 114
357 assertThat(total - expected).isLessThan(0.1f)
358 }
359 }
360
361 @Test
362 fun scrollable_cancel_callsDragStop() {
363 var total by mutableStateOf(0f)
364 var dragStopped = 0f
365 val clocks = ManualAnimationClock(0L)
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100366 val controller = ScrollableController(
367 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000368 total += it
369 it
370 },
371 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
372 animationClock = clocks
373 )
374 setScrollableContent {
375 if (total < 20) {
Matvei Malkov4a945c32020-03-27 16:27:23 +0000376 Modifier.scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100377 controller = controller,
378 orientation = Orientation.Horizontal,
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000379 onScrollStopped = {
380 dragStopped++
381 }
382 )
383 } else {
Adam Powellb6d8db22020-04-02 12:40:03 -0700384 Modifier
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000385 }
386 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100387 onNodeWithTag(scrollableBoxTag).performGesture {
388 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000389 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700390 end = Offset(this.center.x + 100, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000391 duration = 100.milliseconds
392 )
393 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100394 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000395 assertThat(total).isGreaterThan(0f)
396 assertThat(dragStopped).isEqualTo(1f)
397 }
398 }
399
400 @Test
401 fun scrollable_snappingScrolling() {
402 var total = 0f
403 val clocks = ManualAnimationClock(0L)
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100404 val controller = ScrollableController(
405 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000406 total += it
407 it
408 },
409 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
410 animationClock = clocks
411 )
412 setScrollableContent {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100413 Modifier.scrollable(orientation = Orientation.Vertical, controller = controller)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000414 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100415 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000416 assertThat(total).isEqualTo(0f)
417 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100418 runOnIdle {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100419 controller.smoothScrollBy(1000f)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000420 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100421 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100422 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000423 assertThat(total).isEqualTo(1000f)
424 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100425 runOnIdle {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100426 controller.smoothScrollBy(-200f)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000427 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100428 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100429 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000430 assertThat(total).isEqualTo(800f)
431 }
432 }
433
434 @Test
435 fun scrollable_immediateDisposal() {
436 val disposed = mutableStateOf(false)
437 var total = 0f
438 val clocks = ManualAnimationClock(0L)
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100439 val controller = ScrollableController(
440 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000441 Truth.assertWithMessage("Animating after dispose!").that(disposed.value).isFalse()
442 total += it
443 it
444 },
445 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
446 animationClock = clocks
447 )
448 setScrollableContent {
449 if (!disposed.value) {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100450 Modifier.scrollable(orientation = Orientation.Vertical, controller = controller)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000451 } else {
Adam Powellb6d8db22020-04-02 12:40:03 -0700452 Modifier
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000453 }
454 }
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100455 runOnUiThread {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100456 controller.smoothScrollBy(1000f)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000457 disposed.value = true
458 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100459 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100460 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000461 assertThat(total).isEqualTo(0f)
462 }
463 }
464
465 @Test
466 fun scrollable_explicitDisposal() {
467 val disposed = mutableStateOf(false)
468 var total = 0f
469 val clocks = ManualAnimationClock(0L)
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100470 val controller = ScrollableController(
471 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000472 Truth.assertWithMessage("Animating after dispose!").that(disposed.value).isFalse()
473 total += it
474 it
475 },
476 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
477 animationClock = clocks
478 )
479 setScrollableContent {
480 if (!disposed.value) {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100481 Modifier.scrollable(orientation = Orientation.Vertical, controller = controller)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000482 } else {
Adam Powellb6d8db22020-04-02 12:40:03 -0700483 Modifier
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000484 }
485 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100486 runOnIdle {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100487 controller.smoothScrollBy(300f)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000488 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100489 advanceClockAndAwaitAnimation(controller, clocks)
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100490 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000491 assertThat(total).isEqualTo(300f)
492 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100493 runOnIdle {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100494 controller.smoothScrollBy(200f)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000495 }
496 // don't advance clocks yet, toggle disposed value
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100497 runOnUiThread {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000498 disposed.value = true
499 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100500 advanceClockAndAwaitAnimation(controller, clocks)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000501 // still 300 and didn't fail in onScrollConsumptionRequested.. lambda
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100502 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000503 assertThat(total).isEqualTo(300f)
504 }
505 }
506
507 @Test
508 fun scrollable_nestedDrag() {
509 var innerDrag = 0f
510 var outerDrag = 0f
511 val clocks = ManualAnimationClock(0L)
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100512 val outerState = ScrollableController(
513 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000514 outerDrag += it
515 it
516 },
517 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
518 animationClock = clocks
519 )
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100520 val innerState = ScrollableController(
521 consumeScrollDelta = {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000522 innerDrag += it / 2
523 it / 2
524 },
525 flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
526 animationClock = clocks
527 )
528
529 composeTestRule.setContent {
530 Stack {
Filip Pavlis6de64f52020-05-27 18:11:55 +0100531 Box(
532 gravity = ContentGravity.Center,
533 modifier = Modifier
Filip Pavlis6de64f52020-05-27 18:11:55 +0100534 .testTag(scrollableBoxTag)
535 .preferredSize(300.dp)
536 .scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100537 controller = outerState,
538 orientation = Orientation.Horizontal
Filip Pavlis6de64f52020-05-27 18:11:55 +0100539 )
540 ) {
541 Box(
542 modifier = Modifier.preferredSize(300.dp).scrollable(
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100543 controller = innerState,
544 orientation = Orientation.Horizontal
Filip Pavlis6de64f52020-05-27 18:11:55 +0100545 )
546 )
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000547 }
548 }
549 }
Filip Pavlis659ea722020-07-13 14:14:32 +0100550 onNodeWithTag(scrollableBoxTag).performGesture {
551 this.swipe(
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000552 start = this.center,
Nader Jawad6df06122020-06-03 15:27:08 -0700553 end = Offset(this.center.x + 200f, this.center.y),
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000554 duration = 300.milliseconds
555 )
556 }
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100557 val lastEqualDrag = runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000558 assertThat(innerDrag).isGreaterThan(0f)
559 assertThat(outerDrag).isGreaterThan(0f)
560 // we consumed half delta in child, so exactly half should go to the parent
561 assertThat(outerDrag).isEqualTo(innerDrag)
562 innerDrag
563 }
564 advanceClockAndAwaitAnimation(innerState, clocks)
565 advanceClockAndAwaitAnimation(outerState, clocks)
566 // and nothing should change as we don't do nested fling
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100567 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000568 assertThat(outerDrag).isEqualTo(lastEqualDrag)
569 }
570 }
571
Louis Pullen-Freilich3a54b942020-05-07 13:23:03 +0100572 private fun setScrollableContent(scrollableModifierFactory: @Composable () -> Modifier) {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000573 composeTestRule.setContent {
574 Stack {
575 val scrollable = scrollableModifierFactory()
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100576 Box(
577 modifier = Modifier
578 .testTag(scrollableBoxTag)
579 .preferredSize(100.dp) +
580 scrollable
Filip Pavlis6de64f52020-05-27 18:11:55 +0100581 )
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000582 }
583 }
584 }
585
586 // TODO(b/147291885): This should not be needed in the future.
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100587 private fun awaitScrollAnimation(controller: ScrollableController) {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000588 val latch = CountDownLatch(1)
589 val handler = Handler(Looper.getMainLooper())
590 handler.post(object : Runnable {
591 override fun run() {
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100592 if (controller.isAnimationRunning) {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000593 handler.post(this)
594 } else {
595 latch.countDown()
596 }
597 }
598 })
599 Truth.assertWithMessage("Scroll didn't finish after 20 seconds")
600 .that(latch.await(20, TimeUnit.SECONDS)).isTrue()
601 }
602
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100603 private fun advanceClockAndAwaitAnimation(
604 controller: ScrollableController,
605 clock: ManualAnimationClock
606 ) {
Filip Pavlisa4d73a52020-07-14 11:30:00 +0100607 runOnIdle {
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000608 clock.clockTimeMillis += 5000
609 }
Matvei Malkov710fb4f2020-07-03 15:53:10 +0100610 awaitScrollAnimation(controller)
Matvei Malkov59fa63f2020-03-24 17:15:50 +0000611 }
612}