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