[go: nahoru, domu]

blob: 4b3f0827b51a9ff1eebaa3f7ccad5d3c5c40da5d [file] [log] [blame]
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +01001/*
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
17package androidx.ui.material.ripple
18
19import android.os.Build
20import androidx.compose.Composable
21import androidx.compose.Providers
22import androidx.compose.emptyContent
23import androidx.compose.getValue
24import androidx.compose.mutableStateOf
25import androidx.compose.setValue
26import androidx.test.filters.LargeTest
27import androidx.test.filters.SdkSuppress
28import androidx.test.screenshot.AndroidXScreenshotTestRule
29import androidx.test.screenshot.assertAgainstGolden
30import androidx.ui.core.Modifier
31import androidx.ui.core.semantics.semantics
32import androidx.ui.core.testTag
Louis Pullen-Freilichddda7be2020-07-17 18:28:12 +010033import androidx.compose.foundation.Box
34import androidx.compose.foundation.ContentGravity
35import androidx.compose.foundation.Interaction
36import androidx.compose.foundation.InteractionState
37import androidx.compose.foundation.indication
38import androidx.compose.foundation.shape.corner.RoundedCornerShape
Louis Pullen-Freilichf434a132020-07-22 14:19:24 +010039import androidx.compose.ui.geometry.Offset
Louis Pullen-Freilich4dc4dac2020-07-22 14:39:14 +010040import androidx.compose.ui.graphics.Color
41import androidx.compose.ui.graphics.compositeOver
Louis Pullen-Freilich623e4052020-07-19 20:24:03 +010042import androidx.compose.foundation.layout.fillMaxSize
43import androidx.compose.foundation.layout.padding
44import androidx.compose.foundation.layout.preferredHeight
45import androidx.compose.foundation.layout.preferredWidth
Matvei Malkovebe058f2020-07-21 13:53:47 +010046import androidx.ui.material.ExperimentalMaterialApi
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +010047import androidx.ui.material.GOLDEN_MATERIAL
48import androidx.ui.material.MaterialTheme
49import androidx.ui.material.Surface
50import androidx.ui.material.darkColorPalette
51import androidx.ui.material.lightColorPalette
52import androidx.ui.test.ComposeTestRule
53import androidx.ui.test.captureToBitmap
54import androidx.ui.test.createComposeRule
Filip Pavlis659ea722020-07-13 14:14:32 +010055import androidx.ui.test.onNodeWithTag
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +010056import androidx.ui.test.runOnUiThread
57import androidx.ui.test.waitForIdle
Louis Pullen-Freilicha7eeb102020-07-22 17:54:24 +010058import androidx.compose.ui.unit.dp
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +010059import com.google.common.truth.Truth
60import org.junit.Rule
61import org.junit.Test
62import org.junit.runner.RunWith
63import org.junit.runners.JUnit4
64
65@LargeTest
66@RunWith(JUnit4::class)
67@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
Matvei Malkovebe058f2020-07-21 13:53:47 +010068@OptIn(ExperimentalMaterialApi::class)
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +010069class RippleIndicationTest {
70
71 @get:Rule
72 val composeTestRule = createComposeRule()
73
74 @get:Rule
75 val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
76
77 @Test
78 fun bounded_lightTheme_highLuminance_pressed() {
79 val interactionState = InteractionState()
80
81 val contentColor = Color.White
82
83 composeTestRule.setRippleContent(
84 interactionState = interactionState,
85 bounded = true,
86 lightTheme = true,
87 contentColor = contentColor
88 )
89
90 assertRippleMatches(
91 interactionState,
92 Interaction.Pressed,
93 "rippleindication_bounded_light_highluminance_pressed",
94 calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
95 )
96 }
97
98 @Test
99 fun bounded_lightTheme_highLuminance_dragged() {
100 val interactionState = InteractionState()
101
102 val contentColor = Color.White
103
104 composeTestRule.setRippleContent(
105 interactionState = interactionState,
106 bounded = true,
107 lightTheme = true,
108 contentColor = contentColor
109 )
110
111 assertRippleMatches(
112 interactionState,
113 Interaction.Dragged,
114 "rippleindication_bounded_light_highluminance_dragged",
115 calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
116 )
117 }
118
119 @Test
120 fun bounded_lightTheme_lowLuminance_pressed() {
121 val interactionState = InteractionState()
122
123 val contentColor = Color.Black
124
125 composeTestRule.setRippleContent(
126 interactionState = interactionState,
127 bounded = true,
128 lightTheme = true,
129 contentColor = contentColor
130 )
131
132 assertRippleMatches(
133 interactionState,
134 Interaction.Pressed,
135 "rippleindication_bounded_light_lowluminance_pressed",
136 calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
137 )
138 }
139
140 @Test
141 fun bounded_lightTheme_lowLuminance_dragged() {
142 val interactionState = InteractionState()
143
144 val contentColor = Color.Black
145
146 composeTestRule.setRippleContent(
147 interactionState = interactionState,
148 bounded = true,
149 lightTheme = true,
150 contentColor = contentColor
151 )
152
153 assertRippleMatches(
154 interactionState,
155 Interaction.Dragged,
156 "rippleindication_bounded_light_lowluminance_dragged",
157 calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
158 )
159 }
160
161 @Test
162 fun bounded_darkTheme_highLuminance_pressed() {
163 val interactionState = InteractionState()
164
165 val contentColor = Color.White
166
167 composeTestRule.setRippleContent(
168 interactionState = interactionState,
169 bounded = true,
170 lightTheme = false,
171 contentColor = contentColor
172 )
173
174 assertRippleMatches(
175 interactionState,
176 Interaction.Pressed,
177 "rippleindication_bounded_dark_highluminance_pressed",
178 calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
179 )
180 }
181
182 @Test
183 fun bounded_darkTheme_highLuminance_dragged() {
184 val interactionState = InteractionState()
185
186 val contentColor = Color.White
187
188 composeTestRule.setRippleContent(
189 interactionState = interactionState,
190 bounded = true,
191 lightTheme = false,
192 contentColor = contentColor
193 )
194
195 assertRippleMatches(
196 interactionState,
197 Interaction.Dragged,
198 "rippleindication_bounded_dark_highluminance_dragged",
199 calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
200 )
201 }
202
203 @Test
204 fun bounded_darkTheme_lowLuminance_pressed() {
205 val interactionState = InteractionState()
206
207 val contentColor = Color.Black
208
209 composeTestRule.setRippleContent(
210 interactionState = interactionState,
211 bounded = true,
212 lightTheme = false,
213 contentColor = contentColor
214 )
215
216 assertRippleMatches(
217 interactionState,
218 Interaction.Pressed,
219 "rippleindication_bounded_dark_lowluminance_pressed",
220 // Low luminance content in dark theme should use a white ripple by default
221 calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
222 )
223 }
224
225 @Test
226 fun bounded_darkTheme_lowLuminance_dragged() {
227 val interactionState = InteractionState()
228
229 val contentColor = Color.Black
230
231 composeTestRule.setRippleContent(
232 interactionState = interactionState,
233 bounded = true,
234 lightTheme = false,
235 contentColor = contentColor
236 )
237
238 assertRippleMatches(
239 interactionState,
240 Interaction.Dragged,
241 "rippleindication_bounded_dark_lowluminance_dragged",
242 // Low luminance content in dark theme should use a white ripple by default
243 calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
244 )
245 }
246
247 @Test
248 fun unbounded_lightTheme_highLuminance_pressed() {
249 val interactionState = InteractionState()
250
251 val contentColor = Color.White
252
253 composeTestRule.setRippleContent(
254 interactionState = interactionState,
255 bounded = false,
256 lightTheme = true,
257 contentColor = contentColor
258 )
259
260 assertRippleMatches(
261 interactionState,
262 Interaction.Pressed,
263 "rippleindication_unbounded_light_highluminance_pressed",
264 calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
265 )
266 }
267
268 @Test
269 fun unbounded_lightTheme_highLuminance_dragged() {
270 val interactionState = InteractionState()
271
272 val contentColor = Color.White
273
274 composeTestRule.setRippleContent(
275 interactionState = interactionState,
276 bounded = false,
277 lightTheme = true,
278 contentColor = contentColor
279 )
280
281 assertRippleMatches(
282 interactionState,
283 Interaction.Dragged,
284 "rippleindication_unbounded_light_highluminance_dragged",
285 calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
286 )
287 }
288
289 @Test
290 fun unbounded_lightTheme_lowLuminance_pressed() {
291 val interactionState = InteractionState()
292
293 val contentColor = Color.Black
294
295 composeTestRule.setRippleContent(
296 interactionState = interactionState,
297 bounded = false,
298 lightTheme = true,
299 contentColor = contentColor
300 )
301
302 assertRippleMatches(
303 interactionState,
304 Interaction.Pressed,
305 "rippleindication_unbounded_light_lowluminance_pressed",
306 calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
307 )
308 }
309
310 @Test
311 fun unbounded_lightTheme_lowLuminance_dragged() {
312 val interactionState = InteractionState()
313
314 val contentColor = Color.Black
315
316 composeTestRule.setRippleContent(
317 interactionState = interactionState,
318 bounded = false,
319 lightTheme = true,
320 contentColor = contentColor
321 )
322
323 assertRippleMatches(
324 interactionState,
325 Interaction.Dragged,
326 "rippleindication_unbounded_light_lowluminance_dragged",
327 calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
328 )
329 }
330
331 @Test
332 fun unbounded_darkTheme_highLuminance_pressed() {
333 val interactionState = InteractionState()
334
335 val contentColor = Color.White
336
337 composeTestRule.setRippleContent(
338 interactionState = interactionState,
339 bounded = false,
340 lightTheme = false,
341 contentColor = contentColor
342 )
343
344 assertRippleMatches(
345 interactionState,
346 Interaction.Pressed,
347 "rippleindication_unbounded_dark_highluminance_pressed",
348 calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
349 )
350 }
351
352 @Test
353 fun unbounded_darkTheme_highLuminance_dragged() {
354 val interactionState = InteractionState()
355
356 val contentColor = Color.White
357
358 composeTestRule.setRippleContent(
359 interactionState = interactionState,
360 bounded = false,
361 lightTheme = false,
362 contentColor = contentColor
363 )
364
365 assertRippleMatches(
366 interactionState,
367 Interaction.Dragged,
368 "rippleindication_unbounded_dark_highluminance_dragged",
369 calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
370 )
371 }
372
373 @Test
374 fun unbounded_darkTheme_lowLuminance_pressed() {
375 val interactionState = InteractionState()
376
377 val contentColor = Color.Black
378
379 composeTestRule.setRippleContent(
380 interactionState = interactionState,
381 bounded = false,
382 lightTheme = false,
383 contentColor = contentColor
384 )
385
386 assertRippleMatches(
387 interactionState,
388 Interaction.Pressed,
389 "rippleindication_unbounded_dark_lowluminance_pressed",
390 // Low luminance content in dark theme should use a white ripple by default
391 calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
392 )
393 }
394
395 @Test
396 fun unbounded_darkTheme_lowLuminance_dragged() {
397 val interactionState = InteractionState()
398
399 val contentColor = Color.Black
400
401 composeTestRule.setRippleContent(
402 interactionState = interactionState,
403 bounded = false,
404 lightTheme = false,
405 contentColor = contentColor
406 )
407
408 assertRippleMatches(
409 interactionState,
410 Interaction.Dragged,
411 "rippleindication_unbounded_dark_lowluminance_dragged",
412 // Low luminance content in dark theme should use a white ripple by default
413 calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
414 )
415 }
416
417 @Test
418 fun customRippleTheme_pressed() {
419 val interactionState = InteractionState()
420
421 val contentColor = Color.Black
422
423 val rippleColor = Color.Red
424 val rippleAlpha = 0.5f
425
426 val rippleTheme = object : RippleTheme {
427 @Composable
428 override fun defaultColor() = rippleColor
429
430 @Composable
431 override fun rippleOpacity() = object : RippleOpacity {
432 override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
433 }
434 }
435
436 composeTestRule.setContent {
437 Providers(RippleThemeAmbient provides rippleTheme) {
438 MaterialTheme {
439 Surface(contentColor = contentColor) {
440 Box(Modifier.fillMaxSize(), gravity = ContentGravity.Center) {
441 RippleBox(interactionState, RippleIndication())
442 }
443 }
444 }
445 }
446 }
447
448 val expectedColor = calculateResultingRippleColor(rippleColor, rippleOpacity = rippleAlpha)
449
450 assertRippleMatches(
451 interactionState,
452 Interaction.Pressed,
453 "rippleindication_customtheme_pressed",
454 expectedColor
455 )
456 }
457
458 @Test
459 fun customRippleTheme_dragged() {
460 val interactionState = InteractionState()
461
462 val contentColor = Color.Black
463
464 val rippleColor = Color.Red
465 val rippleAlpha = 0.5f
466
467 val rippleTheme = object : RippleTheme {
468 @Composable
469 override fun defaultColor() = rippleColor
470
471 @Composable
472 override fun rippleOpacity() = object : RippleOpacity {
473 override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
474 }
475 }
476
477 composeTestRule.setContent {
478 Providers(RippleThemeAmbient provides rippleTheme) {
479 MaterialTheme {
480 Surface(contentColor = contentColor) {
481 Box(Modifier.fillMaxSize(), gravity = ContentGravity.Center) {
482 RippleBox(interactionState, RippleIndication())
483 }
484 }
485 }
486 }
487 }
488
489 val expectedColor = calculateResultingRippleColor(rippleColor, rippleOpacity = rippleAlpha)
490
491 assertRippleMatches(
492 interactionState,
493 Interaction.Dragged,
494 "rippleindication_customtheme_dragged",
495 expectedColor
496 )
497 }
498
499 @Test
500 fun themeChangeDuringRipple() {
501 val interactionState = InteractionState()
502
503 fun createRippleTheme(color: Color, alpha: Float) = object : RippleTheme {
504 @Composable
505 override fun defaultColor() = color
506
507 @Composable
508 override fun rippleOpacity() = object : RippleOpacity {
509 override fun opacityForInteraction(interaction: Interaction) = alpha
510 }
511 }
512
513 val initialColor = Color.Red
514 val initialAlpha = 0.5f
515
516 var rippleTheme by mutableStateOf(createRippleTheme(initialColor, initialAlpha))
517
518 composeTestRule.setContent {
519 Providers(RippleThemeAmbient provides rippleTheme) {
520 MaterialTheme {
521 Surface(contentColor = Color.Black) {
522 Box(Modifier.fillMaxSize(), gravity = ContentGravity.Center) {
523 RippleBox(interactionState, RippleIndication())
524 }
525 }
526 }
527 }
528 }
529
530 runOnUiThread {
Nader Jawad6df06122020-06-03 15:27:08 -0700531 interactionState.addInteraction(Interaction.Pressed, Offset(10f, 10f))
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100532 }
533
Filip Pavlis659ea722020-07-13 14:14:32 +0100534 with(onNodeWithTag(Tag)) {
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100535 val centerPixel = captureToBitmap()
536 .run {
537 getPixel(width / 2, height / 2)
538 }
539
540 val expectedColor =
541 calculateResultingRippleColor(initialColor, rippleOpacity = initialAlpha)
542
543 Truth.assertThat(Color(centerPixel)).isEqualTo(expectedColor)
544 }
545
546 val newColor = Color.Green
547 val newAlpha = 0.2f
548
549 runOnUiThread {
550 rippleTheme = createRippleTheme(newColor, newAlpha)
551 }
552
Filip Pavlis659ea722020-07-13 14:14:32 +0100553 with(onNodeWithTag(Tag)) {
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100554 val centerPixel = captureToBitmap()
555 .run {
556 getPixel(width / 2, height / 2)
557 }
558
559 val expectedColor =
560 calculateResultingRippleColor(newColor, rippleOpacity = newAlpha)
561
562 Truth.assertThat(Color(centerPixel)).isEqualTo(expectedColor)
563 }
564 }
565
566 /**
567 * Asserts that the ripple matches the screenshot with identifier [goldenIdentifier], and
568 * that the resultant color of the ripple on screen matches [expectedCenterPixelColor].
569 *
570 * @param interactionState the [InteractionState] driving the ripple
571 * @param interaction the [Interaction] to assert for
572 * @param goldenIdentifier the identifier for the corresponding screenshot
573 * @param expectedCenterPixelColor the expected color for the pixel at the center of the
574 * [RippleBox]
575 */
576 private fun assertRippleMatches(
577 interactionState: InteractionState,
578 interaction: Interaction,
579 goldenIdentifier: String,
580 expectedCenterPixelColor: Color
581 ) {
582 composeTestRule.clockTestRule.pauseClock()
583
584 // Start ripple
585 runOnUiThread {
586 if (interaction is Interaction.Pressed) {
Nader Jawad6df06122020-06-03 15:27:08 -0700587 interactionState.addInteraction(interaction, Offset(10f, 10f))
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100588 } else {
589 interactionState.addInteraction(interaction)
590 }
591 }
592
593 // Advance to somewhere in the middle of the animation for a ripple, or at the end of a
594 // state layer transition
595 waitForIdle()
596 composeTestRule.clockTestRule.advanceClock(50)
597
598 // Capture and compare screenshots
Filip Pavlis659ea722020-07-13 14:14:32 +0100599 onNodeWithTag(Tag)
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100600 .captureToBitmap()
601 .assertAgainstGolden(screenshotRule, goldenIdentifier)
602
603 // Advance until after the end of the ripple animation, so we have a stable final opacity
604 waitForIdle()
605 composeTestRule.clockTestRule.advanceClock(50)
606 waitForIdle()
607
608 // Compare expected and actual pixel color
Filip Pavlis659ea722020-07-13 14:14:32 +0100609 val centerPixel = onNodeWithTag(Tag)
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100610 .captureToBitmap()
611 .run {
612 getPixel(width / 2, height / 2)
613 }
614
615 Truth.assertThat(Color(centerPixel)).isEqualTo(expectedCenterPixelColor)
616 }
617}
618
619/**
620 * Generic Button like component that allows injecting a [RippleIndication] and also includes
621 * padding around the rippled surface, so screenshots will include some dead space for clarity.
622 *
623 * @param interactionState the [InteractionState] that is used to drive the ripple state
624 * @param rippleIndication [RippleIndication] placed inside the surface
625 */
626@Composable
627private fun RippleBox(interactionState: InteractionState, rippleIndication: RippleIndication) {
Alexandre Eliasc60f33e2020-07-10 16:23:09 -0700628 Box(Modifier.semantics(mergeAllDescendants = true) {}.testTag(Tag)) {
Louis Pullen-Freilich32d14712020-06-02 21:40:35 +0100629 Surface(
630 Modifier.padding(25.dp),
631 color = RippleBoxBackgroundColor, shape = RoundedCornerShape(20)
632 ) {
633 Box(
634 Modifier.preferredWidth(80.dp).preferredHeight(50.dp).indication(
635 interactionState = interactionState,
636 indication = rippleIndication
637 ),
638 children = emptyContent()
639 )
640 }
641 }
642}
643
644/**
645 * Sets the content to a [RippleBox] with a [MaterialTheme] and surrounding [Surface]
646 *
647 * @param interactionState [InteractionState] used to drive the ripple inside the [RippleBox]
648 * @param bounded whether the ripple inside the [RippleBox] is bounded
649 * @param lightTheme whether the theme is light or dark
650 * @param contentColor the contentColor that will be used for the ripple color
651 */
652private fun ComposeTestRule.setRippleContent(
653 interactionState: InteractionState,
654 bounded: Boolean,
655 lightTheme: Boolean,
656 contentColor: Color
657) {
658 setContent {
659 val colorPalette = if (lightTheme) lightColorPalette() else darkColorPalette()
660
661 MaterialTheme(colorPalette) {
662 Surface(contentColor = contentColor) {
663 Box(Modifier.fillMaxSize(), gravity = ContentGravity.Center) {
664 RippleBox(interactionState, RippleIndication(bounded))
665 }
666 }
667 }
668 }
669}
670
671/**
672 * Blends ([contentColor] with [rippleOpacity]) on top of [RippleBoxBackgroundColor] to provide
673 * the resulting RGB color that can be used for pixel comparison.
674 */
675private fun calculateResultingRippleColor(
676 contentColor: Color,
677 rippleOpacity: Float
678) = contentColor.copy(alpha = rippleOpacity).compositeOver(RippleBoxBackgroundColor)
679
680private val RippleBoxBackgroundColor = Color.Blue
681
682private const val Tag = "Ripple"