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.layout 18 19 import androidx.activity.compose.setContent 20 import androidx.compose.foundation.background 21 import androidx.compose.foundation.layout.Box 22 import androidx.compose.foundation.layout.IntrinsicSize 23 import androidx.compose.foundation.layout.Row 24 import androidx.compose.foundation.layout.padding 25 import androidx.compose.foundation.layout.size 26 import androidx.compose.foundation.layout.width 27 import androidx.compose.runtime.Composable 28 import androidx.compose.runtime.CompositionLocalProvider 29 import androidx.compose.runtime.mutableStateOf 30 import androidx.compose.ui.FixedSize 31 import androidx.compose.ui.Modifier 32 import androidx.compose.ui.geometry.Offset 33 import androidx.compose.ui.graphics.Color 34 import androidx.compose.ui.node.Ref 35 import androidx.compose.ui.platform.LocalDensity 36 import androidx.compose.ui.platform.LocalLayoutDirection 37 import androidx.compose.ui.runOnUiThreadIR 38 import androidx.compose.ui.test.TestActivity 39 import androidx.compose.ui.unit.Constraints 40 import androidx.compose.ui.unit.Density 41 import androidx.compose.ui.unit.DpSize 42 import androidx.compose.ui.unit.LayoutDirection 43 import androidx.compose.ui.unit.dp 44 import androidx.test.ext.junit.runners.AndroidJUnit4 45 import androidx.test.filters.SmallTest 46 import java.util.concurrent.CountDownLatch 47 import java.util.concurrent.TimeUnit 48 import kotlin.math.abs 49 import kotlin.math.roundToInt 50 import org.junit.Assert 51 import org.junit.Assert.assertEquals 52 import org.junit.Assert.assertTrue 53 import org.junit.Before 54 import org.junit.Rule 55 import org.junit.Test 56 import org.junit.runner.RunWith 57 58 @SmallTest 59 @RunWith(AndroidJUnit4::class) 60 class RtlLayoutTest { 61 @Suppress("DEPRECATION") 62 @get:Rule 63 val activityTestRule = 64 androidx.test.rule.ActivityTestRule<TestActivity>(TestActivity::class.java) 65 private lateinit var activity: TestActivity 66 internal lateinit var density: Density 67 internal lateinit var countDownLatch: CountDownLatch 68 internal lateinit var position: Array<Ref<Offset>> 69 private val size = 100 70 71 @Before 72 fun setup() { 73 activity = activityTestRule.activity 74 density = Density(activity) 75 activity.hasFocusLatch.await(5, TimeUnit.SECONDS) 76 position = Array(3) { Ref<Offset>() } 77 countDownLatch = CountDownLatch(3) 78 } 79 80 @Test 81 fun customLayout_absolutePositioning() = 82 with(density) { 83 activityTestRule.runOnUiThreadIR { 84 activity.setContent { CustomLayout(true, LayoutDirection.Ltr) } 85 } 86 87 countDownLatch.await(1, TimeUnit.SECONDS) 88 assertEquals(Offset(0f, 0f), position[0].value) 89 assertEquals(Offset(size.toFloat(), size.toFloat()), position[1].value) 90 assertEquals(Offset((size * 2).toFloat(), (size * 2).toFloat()), position[2].value) 91 } 92 93 @Test 94 fun customLayout_absolutePositioning_rtl() = 95 with(density) { 96 activityTestRule.runOnUiThreadIR { 97 activity.setContent { CustomLayout(true, LayoutDirection.Rtl) } 98 } 99 100 countDownLatch.await(1, TimeUnit.SECONDS) 101 assertEquals(Offset(0f, 0f), position[0].value) 102 assertEquals(Offset(size.toFloat(), size.toFloat()), position[1].value) 103 assertEquals(Offset((size * 2).toFloat(), (size * 2).toFloat()), position[2].value) 104 } 105 106 @Test 107 fun customLayout_positioning() = 108 with(density) { 109 activityTestRule.runOnUiThreadIR { 110 activity.setContent { CustomLayout(false, LayoutDirection.Ltr) } 111 } 112 113 countDownLatch.await(1, TimeUnit.SECONDS) 114 assertEquals(Offset(0f, 0f), position[0].value) 115 assertEquals(Offset(size.toFloat(), size.toFloat()), position[1].value) 116 assertEquals(Offset((size * 2).toFloat(), (size * 2).toFloat()), position[2].value) 117 } 118 119 @Test 120 fun customLayout_positioning_rtl() = 121 with(density) { 122 activityTestRule.runOnUiThreadIR { 123 activity.setContent { CustomLayout(false, LayoutDirection.Rtl) } 124 } 125 126 countDownLatch.await(1, TimeUnit.SECONDS) 127 128 countDownLatch.await(1, TimeUnit.SECONDS) 129 assertEquals(Offset((size * 2).toFloat(), 0f), position[0].value) 130 assertEquals(Offset(size.toFloat(), size.toFloat()), position[1].value) 131 assertEquals(Offset(0f, (size * 2).toFloat()), position[2].value) 132 } 133 134 @Test 135 fun customLayout_updatingDirectionCausesRemeasure() { 136 val direction = mutableStateOf(LayoutDirection.Rtl) 137 var latch = CountDownLatch(1) 138 var actualDirection: LayoutDirection? = null 139 140 activityTestRule.runOnUiThread { 141 activity.setContent { 142 val children = 143 @Composable { 144 Layout({}) { _, _ -> 145 actualDirection = layoutDirection 146 latch.countDown() 147 layout(100, 100) {} 148 } 149 } 150 CompositionLocalProvider(LocalLayoutDirection provides direction.value) { 151 Layout(children) { measurables, constraints -> 152 layout(100, 100) { 153 measurables.first().measure(constraints).placeRelative(0, 0) 154 } 155 } 156 } 157 } 158 } 159 assertTrue(latch.await(1, TimeUnit.SECONDS)) 160 assertEquals(LayoutDirection.Rtl, actualDirection) 161 162 latch = CountDownLatch(1) 163 activityTestRule.runOnUiThread { direction.value = LayoutDirection.Ltr } 164 165 assertTrue(latch.await(1, TimeUnit.SECONDS)) 166 assertEquals(LayoutDirection.Ltr, actualDirection) 167 } 168 169 @Test 170 fun testModifiedLayoutDirection_inMeasureScope() { 171 val latch = CountDownLatch(1) 172 val resultLayoutDirection = Ref<LayoutDirection>() 173 174 activityTestRule.runOnUiThread { 175 activity.setContent { 176 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 177 Layout(content = {}) { _, _ -> 178 resultLayoutDirection.value = layoutDirection 179 latch.countDown() 180 layout(0, 0) {} 181 } 182 } 183 } 184 } 185 186 assertTrue(latch.await(1, TimeUnit.SECONDS)) 187 assertTrue(LayoutDirection.Rtl == resultLayoutDirection.value) 188 } 189 190 @Test 191 fun testModifiedLayoutDirection_inIntrinsicsMeasure() { 192 val latch = CountDownLatch(1) 193 var resultLayoutDirection: LayoutDirection? = null 194 195 activityTestRule.runOnUiThread { 196 activity.setContent { 197 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 198 val measurePolicy = 199 object : MeasurePolicy { 200 override fun MeasureScope.measure( 201 measurables: List<Measurable>, 202 constraints: Constraints 203 ) = layout(0, 0) {} 204 205 override fun IntrinsicMeasureScope.minIntrinsicWidth( 206 measurables: List<IntrinsicMeasurable>, 207 height: Int 208 ) = 0 209 210 override fun IntrinsicMeasureScope.minIntrinsicHeight( 211 measurables: List<IntrinsicMeasurable>, 212 width: Int 213 ) = 0 214 215 override fun IntrinsicMeasureScope.maxIntrinsicWidth( 216 measurables: List<IntrinsicMeasurable>, 217 height: Int 218 ): Int { 219 resultLayoutDirection = this.layoutDirection 220 latch.countDown() 221 return 0 222 } 223 224 override fun IntrinsicMeasureScope.maxIntrinsicHeight( 225 measurables: List<IntrinsicMeasurable>, 226 width: Int 227 ) = 0 228 } 229 Layout( 230 content = {}, 231 modifier = Modifier.width(IntrinsicSize.Max), 232 measurePolicy = measurePolicy 233 ) 234 } 235 } 236 } 237 238 assertTrue(latch.await(1, TimeUnit.SECONDS)) 239 Assert.assertNotNull(resultLayoutDirection) 240 assertTrue(LayoutDirection.Rtl == resultLayoutDirection) 241 } 242 243 @Test 244 fun testRestoreLocaleLayoutDirection() { 245 val latch = CountDownLatch(1) 246 val resultLayoutDirection = Ref<LayoutDirection>() 247 248 activityTestRule.runOnUiThread { 249 activity.setContent { 250 val initialLayoutDirection = LocalLayoutDirection.current 251 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 252 Box { 253 CompositionLocalProvider( 254 LocalLayoutDirection provides initialLayoutDirection 255 ) { 256 Layout({}) { _, _ -> 257 resultLayoutDirection.value = layoutDirection 258 latch.countDown() 259 layout(0, 0) {} 260 } 261 } 262 } 263 } 264 } 265 } 266 267 assertTrue(latch.await(1, TimeUnit.SECONDS)) 268 assertEquals(LayoutDirection.Ltr, resultLayoutDirection.value) 269 } 270 271 @Test 272 fun testChildGetsPlacedWithinContainerWithPaddingAndMinimumTouchTarget() { 273 // copy-pasted from TouchTarget.kt (internal in material module) 274 class MinimumTouchTargetModifier(val size: DpSize = DpSize(48.dp, 48.dp)) : LayoutModifier { 275 override fun MeasureScope.measure( 276 measurable: Measurable, 277 constraints: Constraints 278 ): MeasureResult { 279 val placeable = measurable.measure(constraints) 280 val width = maxOf(placeable.width, size.width.roundToPx()) 281 val height = maxOf(placeable.height, size.height.roundToPx()) 282 return layout(width, height) { 283 val centerX = ((width - placeable.width) / 2f).roundToInt() 284 val centerY = ((height - placeable.height) / 2f).roundToInt() 285 placeable.place(centerX, centerY) 286 } 287 } 288 289 override fun equals(other: Any?): Boolean { 290 val otherModifier = other as? MinimumTouchTargetModifier ?: return false 291 return size == otherModifier.size 292 } 293 294 override fun hashCode(): Int = size.hashCode() 295 } 296 297 val latch = CountDownLatch(2) 298 var outerLC: LayoutCoordinates? = null 299 var innerLC: LayoutCoordinates? = null 300 var density: Density? = null 301 302 val rowWidth = 200.dp 303 val outerBoxWidth = 56.dp 304 val padding = 16.dp 305 306 activityTestRule.runOnUiThread { 307 activity.setContent { 308 density = LocalDensity.current 309 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 310 Row(modifier = Modifier.width(rowWidth)) { 311 Box( 312 modifier = 313 Modifier.onGloballyPositioned { 314 outerLC = it 315 latch.countDown() 316 } 317 .size(outerBoxWidth) 318 .background(color = Color.Red) 319 .padding(horizontal = padding) 320 .then(MinimumTouchTargetModifier()) 321 ) { 322 Box( 323 modifier = 324 Modifier.onGloballyPositioned { 325 innerLC = it 326 latch.countDown() 327 } 328 .size(30.dp) 329 .background(color = Color.Gray) 330 ) 331 } 332 } 333 } 334 } 335 } 336 337 assertTrue(latch.await(1, TimeUnit.SECONDS)) 338 val (innerOffset, innerWidth) = with(innerLC!!) { localToWindow(Offset.Zero) to size.width } 339 val (outerOffset, outerWidth) = with(outerLC!!) { localToWindow(Offset.Zero) to size.width } 340 assertTrue(innerWidth < outerWidth) 341 assertTrue(innerOffset.x > outerOffset.x) 342 assertTrue(innerWidth + innerOffset.x < outerWidth + outerOffset.x) 343 344 with(density!!) { 345 assertEquals(outerOffset.x.roundToInt(), rowWidth.roundToPx() - outerWidth) 346 val paddingPx = padding.roundToPx() 347 // OuterBoxLeftEdge_padding-16dp_InnerBoxLeftEdge 348 assertTrue(abs(outerOffset.x + paddingPx - innerOffset.x) <= 1.0) 349 // InnerBoxRightEdge_padding-16dp_OuterRightEdge 350 val outerRightEdge = outerOffset.x + outerWidth 351 assertTrue(abs(outerRightEdge - paddingPx - (innerOffset.x + innerWidth)) <= 1) 352 } 353 } 354 355 @Composable 356 private fun CustomLayout(absolutePositioning: Boolean, testLayoutDirection: LayoutDirection) { 357 CompositionLocalProvider(LocalLayoutDirection provides testLayoutDirection) { 358 Layout( 359 content = { 360 FixedSize(size, modifier = Modifier.saveLayoutInfo(position[0], countDownLatch)) 361 FixedSize(size, modifier = Modifier.saveLayoutInfo(position[1], countDownLatch)) 362 FixedSize(size, modifier = Modifier.saveLayoutInfo(position[2], countDownLatch)) 363 } 364 ) { measurables, constraints -> 365 val placeables = measurables.map { it.measure(constraints) } 366 val width = placeables.fold(0) { sum, p -> sum + p.width } 367 val height = placeables.fold(0) { sum, p -> sum + p.height } 368 layout(width, height) { 369 var x = 0 370 var y = 0 371 for (placeable in placeables) { 372 if (absolutePositioning) { 373 placeable.place(x, y) 374 } else { 375 placeable.placeRelative(x, y) 376 } 377 x += placeable.width 378 y += placeable.height 379 } 380 } 381 } 382 } 383 } 384 385 private fun Modifier.saveLayoutInfo( 386 position: Ref<Offset>, 387 countDownLatch: CountDownLatch 388 ): Modifier = onGloballyPositioned { 389 position.value = it.localToRoot(Offset(0f, 0f)) 390 countDownLatch.countDown() 391 } 392 } 393