1 /*
<lambda>null2 * 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
17 package androidx.compose.ui.input.pointer
18
19 import android.os.Build
20 import androidx.compose.foundation.gestures.detectTapGestures
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.fillMaxSize
23 import androidx.compose.foundation.layout.offset
24 import androidx.compose.foundation.layout.size
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.mutableStateOf
27 import androidx.compose.runtime.remember
28 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.draw.drawBehind
30 import androidx.compose.ui.geometry.Offset
31 import androidx.compose.ui.graphics.Color
32 import androidx.compose.ui.graphics.TransformOrigin
33 import androidx.compose.ui.graphics.asAndroidBitmap
34 import androidx.compose.ui.graphics.graphicsLayer
35 import androidx.compose.ui.graphics.toArgb
36 import androidx.compose.ui.layout.Layout
37 import androidx.compose.ui.platform.LocalDensity
38 import androidx.compose.ui.platform.testTag
39 import androidx.compose.ui.test.captureToImage
40 import androidx.compose.ui.test.junit4.createComposeRule
41 import androidx.compose.ui.test.onNodeWithTag
42 import androidx.compose.ui.test.performTouchInput
43 import androidx.test.ext.junit.runners.AndroidJUnit4
44 import androidx.test.filters.LargeTest
45 import androidx.test.filters.SdkSuppress
46 import java.util.concurrent.CountDownLatch
47 import java.util.concurrent.TimeUnit
48 import kotlin.math.max
49 import org.junit.Assert
50 import org.junit.Assert.assertTrue
51 import org.junit.Rule
52 import org.junit.Test
53 import org.junit.runner.RunWith
54
55 @LargeTest
56 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
57 @RunWith(AndroidJUnit4::class)
58 class LayerTouchTransformTest {
59
60 @get:Rule val rule = createComposeRule()
61
62 @Test
63 fun testTransformTouchEventConsumed() {
64 val testTag = "transformedComposable"
65 var latch = CountDownLatch(1)
66 rule.setContent {
67 val pressed = remember { mutableStateOf(false) }
68 val onStart: (Offset) -> Unit = { pressed.value = true }
69
70 val onStop = { pressed.value = false }
71
72 val color =
73 if (pressed.value) {
74 Color.Red
75 } else {
76 Color.Blue
77 }
78
79 val background = Modifier.drawBehind { drawRect(Color.Gray) }
80
81 val latchDrawModifier = Modifier.drawBehind { latch.countDown() }
82
83 with(LocalDensity.current) {
84 val containerDp = 200f.toDp()
85 val boxDp = 50f.toDp()
86
87 val offsetX = 270f.toDp()
88 val offsetY = 120f.toDp()
89 Box(Modifier.testTag(testTag)) {
90 SimpleLayout(modifier = Modifier.fillMaxSize().offset(offsetX, offsetY)) {
91 SimpleLayout(modifier = background.then(Modifier.size(containerDp))) {
92 SimpleLayout(
93 modifier =
94 Modifier.graphicsLayer(
95 scaleX = 2.0f,
96 scaleY = 0.5f,
97 translationX = 50.0f,
98 translationY = 30.0f,
99 rotationZ = 45.0f,
100 transformOrigin = TransformOrigin(1.0f, 1.0f)
101 )
102 .drawBehind { drawRect(color) }
103 .then(latchDrawModifier)
104 .size(boxDp)
105 .pointerInput(Unit) {
106 detectTapGestures(
107 onPress = {
108 onStart.invoke(it)
109 val success = tryAwaitRelease()
110 if (success) onStop.invoke()
111 else onStop.invoke()
112 }
113 )
114 }
115 )
116 }
117 }
118 }
119 }
120 }
121
122 rule.waitForIdle()
123 assertTrue(latch.await(5, TimeUnit.SECONDS))
124
125 // Touch position outside the bounds of the target composable
126 // however, after transformations, this point will be within
127 // its bounds
128
129 latch = CountDownLatch(1)
130 val mappedPosition = Offset(342.0f, 168.0f)
131 val node = rule.onNodeWithTag(testTag).performTouchInput { down(mappedPosition) }
132
133 rule.waitForIdle()
134 assertTrue(latch.await(5, TimeUnit.SECONDS))
135
136 node.captureToImage().asAndroidBitmap().apply {
137 Assert.assertEquals(
138 Color.Red.toArgb(),
139 getPixel(mappedPosition.x.toInt(), mappedPosition.y.toInt())
140 )
141 }
142 }
143 }
144
145 @Composable
SimpleLayoutnull146 fun SimpleLayout(modifier: Modifier, content: @Composable () -> Unit = {}) {
measurablesnull147 Layout(content, modifier) { measurables, constraints ->
148 val childConstraints = constraints.copyMaxDimensions()
149 val placeables = measurables.map { it.measure(childConstraints) }
150 var containerWidth = constraints.minWidth
151 var containerHeight = constraints.minHeight
152 placeables.forEach {
153 containerWidth = max(containerWidth, it.width)
154 containerHeight = max(containerHeight, it.height)
155 }
156 layout(containerWidth, containerHeight) { placeables.forEach { it.placeRelative(0, 0) } }
157 }
158 }
159