blob: ad0035991293f1f7557bc19f84f1e08df311eba7 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("Deprecation")
package androidx.compose.ui
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.transition.TransitionManager
import android.view.PixelCopy
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.HorizontalAlignmentLine
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.VerticalAlignmentLine
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.RenderNodeApi23
import androidx.compose.ui.platform.RenderNodeApi29
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.ViewLayer
import androidx.compose.ui.platform.ViewLayerContainer
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.unit.toOffset
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Corresponds to ContainingViewTest, but tests single composition measure, layout and draw.
* It also tests that layouts with both Layout and MeasureBox work.
*/
@MediumTest
@RunWith(AndroidJUnit4::class)
class AndroidLayoutDrawTest {
@Suppress("DEPRECATION")
@get:Rule
val activityTestRule = androidx.test.rule.ActivityTestRule<TestActivity>(
TestActivity::class.java
)
@get:Rule
val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
private lateinit var activity: TestActivity
private lateinit var drawLatch: CountDownLatch
private lateinit var density: Density
@Before
fun setup() {
activity = activityTestRule.activity
activity.hasFocusLatch.await(5, TimeUnit.SECONDS)
drawLatch = CountDownLatch(1)
density = Density(activity)
}
// Tests that simple drawing works with layered squares
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun simpleDrawTest() {
val yellow = Color(0xFFFFFF00)
val red = Color(0xFF800000)
val model = SquareModel(outerColor = yellow, innerColor = red, size = 10)
composeSquares(model)
validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
}
// Tests that the fail-over for M RenderNode support works. Note that this would work with M
// and above except that our snapshots only work with O and above.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O, maxSdkVersion = Build.VERSION_CODES.O)
@Test
fun simpleDrawTestLegacyFallback() {
try {
RenderNodeApi23.testFailCreateRenderNode = true
val yellow = Color(0xFFFFFF00)
val red = Color(0xFF800000)
val model = SquareModel(outerColor = yellow, innerColor = red, size = 10)
composeSquares(model)
validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
} finally {
RenderNodeApi23.testFailCreateRenderNode = false
}
}
@Test
fun testCompositingStrategyAuto() {
drawLatch = CountDownLatch(1)
var compositingApplied = false
activity.runOnUiThread {
compositingApplied = when (Build.VERSION.SDK_INT) {
// Use public RenderNode API
in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
verifyRenderNode29CompositingStrategy(
CompositingStrategy.Auto,
expectedCompositing = false,
expectedOverlappingRendering = true
)
// Cannot access private APIs on P
Build.VERSION_CODES.P ->
verifyViewLayerCompositingStrategy(
CompositingStrategy.Auto,
View.LAYER_TYPE_NONE,
true
)
// Use stub access to framework RenderNode API
in Build.VERSION_CODES.M..Int.MAX_VALUE ->
verifyRenderNode23CompositingStrategy(
CompositingStrategy.Auto,
expectedLayerType = View.LAYER_TYPE_NONE,
expectedOverlappingRendering = true
)
// No RenderNodes, use Views instead
else ->
verifyViewLayerCompositingStrategy(
CompositingStrategy.Auto,
View.LAYER_TYPE_NONE,
true
)
}
drawLatch.countDown()
}
drawLatch.await(1, TimeUnit.SECONDS)
assertTrue(compositingApplied)
}
@Test
fun testCompositingStrategyModulateAlpha() {
drawLatch = CountDownLatch(1)
var compositingApplied = false
activity.runOnUiThread {
compositingApplied = when (Build.VERSION.SDK_INT) {
// Use public RenderNode API
in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
verifyRenderNode29CompositingStrategy(
CompositingStrategy.ModulateAlpha,
expectedCompositing = false,
expectedOverlappingRendering = false
)
// Cannot access private APIs on P
Build.VERSION_CODES.P ->
verifyViewLayerCompositingStrategy(
CompositingStrategy.ModulateAlpha,
View.LAYER_TYPE_NONE,
false
)
// Use stub access to framework RenderNode API
in Build.VERSION_CODES.M..Int.MAX_VALUE ->
verifyRenderNode23CompositingStrategy(
CompositingStrategy.ModulateAlpha,
expectedLayerType = View.LAYER_TYPE_NONE,
expectedOverlappingRendering = false
)
// No RenderNodes, use Views instead
else ->
verifyViewLayerCompositingStrategy(
CompositingStrategy.ModulateAlpha,
View.LAYER_TYPE_NONE,
false
)
}
drawLatch.countDown()
}
drawLatch.await(1, TimeUnit.SECONDS)
assertTrue(compositingApplied)
}
@Test
fun testCompositingStrategyAlways() {
drawLatch = CountDownLatch(1)
var compositingApplied = false
activity.runOnUiThread {
compositingApplied = when (Build.VERSION.SDK_INT) {
// Use public RenderNode API
in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
verifyRenderNode29CompositingStrategy(
CompositingStrategy.Offscreen,
expectedCompositing = true,
expectedOverlappingRendering = true
)
// Cannot access private APIs on P
Build.VERSION_CODES.P ->
verifyViewLayerCompositingStrategy(
CompositingStrategy.Offscreen,
View.LAYER_TYPE_HARDWARE,
true
)
// Use stub access to framework RenderNode API
in Build.VERSION_CODES.M..Int.MAX_VALUE ->
verifyRenderNode23CompositingStrategy(
CompositingStrategy.Offscreen,
expectedLayerType = View.LAYER_TYPE_HARDWARE,
expectedOverlappingRendering = true
)
// No RenderNodes, use Views instead
else ->
verifyViewLayerCompositingStrategy(
CompositingStrategy.Offscreen,
View.LAYER_TYPE_HARDWARE,
true
)
}
drawLatch.countDown()
}
drawLatch.await(1, TimeUnit.SECONDS)
assertTrue(compositingApplied)
}
@Test
fun testLayerCameraDistance() {
val targetCameraDistance = 15f
drawLatch = CountDownLatch(1)
var cameraDistanceApplied = false
activity.runOnUiThread {
// Verify that the camera distance parameters are consumed properly across API levels.
// camera distance on the View API assumes Dp however, the compose API consumes pixels
// Additionally RenderNode consumed the negative value of the camera distance.
// Ensure that each implementation of camera distance consumes positive pixel values
// properly. Layer implementations backed by View should be compatible on all
// API versions
cameraDistanceApplied = when (Build.VERSION.SDK_INT) {
// Use public RenderNode API
in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
verifyRenderNode29CameraDistance(targetCameraDistance) &&
verifyViewLayerCameraDistance(targetCameraDistance)
// Cannot access private APIs on P
Build.VERSION_CODES.P -> verifyViewLayerCameraDistance(targetCameraDistance)
// Use stub access to framework RenderNode API
in Build.VERSION_CODES.M..Int.MAX_VALUE ->
verifyRenderNode23CameraDistance(targetCameraDistance) &&
verifyViewLayerCameraDistance(targetCameraDistance)
// No RenderNodes, use Views instead
else -> verifyViewLayerCameraDistance(targetCameraDistance)
}
drawLatch.countDown()
}
drawLatch.await(1, TimeUnit.SECONDS)
assertTrue(cameraDistanceApplied)
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun verifyRenderNode29CompositingStrategy(
compositingStrategy: CompositingStrategy,
expectedCompositing: Boolean,
expectedOverlappingRendering: Boolean
): Boolean {
val node = RenderNodeApi29(AndroidComposeView(
activity,
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
)).apply {
this.compositingStrategy = compositingStrategy
}
return expectedCompositing == node.isUsingCompositingLayer() &&
expectedOverlappingRendering == node.hasOverlappingRendering()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun verifyRenderNode23CompositingStrategy(
compositingStrategy: CompositingStrategy,
expectedLayerType: Int,
expectedOverlappingRendering: Boolean
): Boolean {
val node = RenderNodeApi23(AndroidComposeView(
activity,
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
)).apply {
this.compositingStrategy = compositingStrategy
}
return expectedLayerType == node.getLayerType() &&
expectedOverlappingRendering == node.hasOverlappingRendering()
}
private fun verifyViewLayerCompositingStrategy(
compositingStrategy: CompositingStrategy,
expectedLayerType: Int,
expectedOverlappingRendering: Boolean
): Boolean {
val view = ViewLayer(
AndroidComposeView(
activity,
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
),
ViewLayerContainer(activity),
{ _, _ -> },
{}).apply {
val scope = ReusableGraphicsLayerScope()
scope.cameraDistance = cameraDistance
scope.compositingStrategy = compositingStrategy
scope.layoutDirection = LayoutDirection.Ltr
scope.graphicsDensity = Density(1f)
updateLayerProperties(scope)
}
return expectedLayerType == view.layerType &&
expectedOverlappingRendering == view.hasOverlappingRendering()
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun verifyRenderNode29CameraDistance(cameraDistance: Float): Boolean =
// Verify that the internal render node has the camera distance property
// given to the wrapper
RenderNodeApi29(AndroidComposeView(
activity,
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
)).apply {
this.cameraDistance = cameraDistance
}.dumpRenderNodeData().cameraDistance == cameraDistance
@RequiresApi(Build.VERSION_CODES.M)
private fun verifyRenderNode23CameraDistance(cameraDistance: Float): Boolean =
// Verify that the internal render node has the camera distance property
// given to the wrapper
RenderNodeApi23(AndroidComposeView(
activity,
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
)).apply {
this.cameraDistance = cameraDistance
}.dumpRenderNodeData().cameraDistance == -cameraDistance // Camera distance is negative
private fun verifyViewLayerCameraDistance(cameraDistance: Float): Boolean {
val layer = ViewLayer(
AndroidComposeView(
activity,
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
),
ViewLayerContainer(activity),
{ _, _ -> },
{}
).apply {
val scope = ReusableGraphicsLayerScope()
scope.cameraDistance = cameraDistance
scope.layoutDirection = LayoutDirection.Ltr
scope.graphicsDensity = Density(1f)
updateLayerProperties(scope)
}
// Verify that the camera distance is applied properly even after accounting for
// the internal dp conversion within View
return layer.cameraDistance == cameraDistance * layer.resources.displayMetrics.densityDpi
}
// Tests that simple drawing works with draw with nested children
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun nestedDrawTest() {
val yellow = Color(0xFFFFFF00)
val red = Color(0xFF800000)
val model = SquareModel(outerColor = yellow, innerColor = red, size = 10)
composeNestedSquares(model)
validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
}
// Tests that recomposition works with models used within Draw components
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun recomposeDrawTest() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
composeSquares(model)
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
drawLatch = CountDownLatch(1)
val red = Color(0xFF800000)
val yellow = Color(0xFFFFFF00)
activityTestRule.runOnUiThreadIR {
model.outerColor = red
model.innerColor = yellow
}
validateSquareColors(outerColor = red, innerColor = yellow, size = 10)
}
// Tests that recomposition of nested repaint boundaries work
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun recomposeNestedRepaintBoundariesColorChange() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
composeSquaresWithNestedRepaintBoundaries(model)
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
drawLatch = CountDownLatch(1)
val yellow = Color(0xFFFFFF00)
activityTestRule.runOnUiThreadIR {
model.innerColor = yellow
}
validateSquareColors(outerColor = blue, innerColor = yellow, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun recomposeNestedRepaintBoundariesSizeChange() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
composeSquaresWithNestedRepaintBoundaries(model)
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.size = 20
}
validateSquareColors(outerColor = blue, innerColor = white, size = 20)
}
// When there is a repaint boundary around a moving child, the child move
// should be reflected in the repainted bitmap
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun recomposeRepaintBoundariesMove() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
val offset = mutableStateOf(10)
composeMovingSquaresWithRepaintBoundary(model, offset)
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
positionLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
offset.value = 20
}
assertTrue(positionLatch!!.await(1, TimeUnit.SECONDS))
validateSquareColors(outerColor = blue, innerColor = white, offset = 10, size = 10)
}
// When there is no repaint boundary around a moving child, the child move
// should be reflected in the repainted bitmap
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun recomposeMove() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
val offset = mutableStateOf(10)
composeMovingSquares(model, offset)
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
// there isn't going to be a normal draw because we are just moving the repaint
// boundary, but we should have a draw cycle
activityTestRule.findAndroidComposeView().viewTreeObserver.addOnDrawListener {
drawLatch.countDown()
}
offset.value = 20
}
validateSquareColors(outerColor = blue, innerColor = white, offset = 10, size = 10)
}
// Tests that recomposition works with models used within Layout components
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun recomposeSizeTest() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
composeSquares(model)
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { model.size = 20 }
validateSquareColors(outerColor = blue, innerColor = white, size = 20)
}
// The size and color are both changed in a simpler single-color square.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun simpleSquareColorAndSizeTest() {
val green = Color(0xFF00FF00)
val model = SquareModel(size = 20, outerColor = green, innerColor = green)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Padding(
size = (model.size * 3),
modifier = Modifier.fillColor(model, isInner = false)
) {
}
}
}
validateSquareColors(outerColor = green, innerColor = green, size = 20)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.size = 30
}
validateSquareColors(outerColor = green, innerColor = green, size = 30)
drawLatch = CountDownLatch(1)
val blue = Color(0xFF0000FF)
activityTestRule.runOnUiThreadIR {
model.innerColor = blue
model.outerColor = blue
}
validateSquareColors(outerColor = blue, innerColor = blue, size = 30)
}
// Components that aren't placed shouldn't be drawn.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun noPlaceNoDraw() {
val green = Color(0xFF00FF00)
val white = Color(0xFFFFFFFF)
val model = SquareModel(size = 20, outerColor = green, innerColor = white)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
Padding(
size = (model.size * 3),
modifier = Modifier.fillColor(model, isInner = false)
) { }
Padding(
size = model.size,
modifier = Modifier.fillColor(model, isInner = true)
) { }
},
measurePolicy = { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
layout(placeables[0].width, placeables[0].height) {
placeables[0].place(0, 0)
}
}
)
}
}
validateSquareColors(outerColor = green, innerColor = green, size = 20)
}
// Make sure that draws intersperse properly with sub-layouts
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawOrderWithChildren() {
val green = Color(0xFF00FF00)
val white = Color(0xFFFFFFFF)
val model = SquareModel(size = 20, outerColor = green, innerColor = white)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val contentDrawing = object : DrawModifier {
override fun ContentDrawScope.draw() {
// Fill the space with the outerColor
drawRect(model.outerColor)
val offset = size.width / 3
// clip drawing to the inner rectangle
clipRect(offset, offset, offset * 2, offset * 2) {
this@draw.drawContent()
// Fill bottom half with innerColor -- should be clipped
drawRect(
model.innerColor,
topLeft = Offset(0f, size.height / 2f),
size = Size(size.width, size.height / 2f)
)
}
}
}
val paddingContent = Modifier.drawBehind {
// Fill top half with innerColor -- should be clipped
drawLatch.countDown()
drawRect(
model.innerColor,
size = Size(size.width, size.height / 2f)
)
}
Padding(size = (model.size * 3), modifier = contentDrawing.then(paddingContent)) {
}
}
}
validateSquareColors(outerColor = green, innerColor = white, size = 20)
}
@Test
fun multiChildLayoutTest() {
val childrenCount = 3
val childConstraints = arrayOf(
Constraints(),
Constraints.fixedWidth(50),
Constraints.fixedHeight(50)
)
val headerChildrenCount = 1
val footerChildrenCount = 2
activityTestRule.runOnUiThreadIR {
activity.setContent {
val header = @Composable {
Layout(
measurePolicy = { _, constraints ->
assertEquals(childConstraints[0], constraints)
layout(0, 0) {}
},
content = {}, modifier = Modifier.layoutId("header")
)
}
val footer = @Composable {
Layout(
measurePolicy = { _, constraints ->
assertEquals(childConstraints[1], constraints)
layout(0, 0) {}
},
content = {}, modifier = Modifier.layoutId("footer")
)
Layout(
measurePolicy = { _, constraints ->
assertEquals(childConstraints[2], constraints)
layout(0, 0) {}
},
content = {}, modifier = Modifier.layoutId("footer")
)
}
Layout({ header(); footer() }) { measurables, _ ->
assertEquals(childrenCount, measurables.size)
measurables.forEachIndexed { index, measurable ->
measurable.measure(childConstraints[index])
}
val measurablesHeader = measurables.filter { it.layoutId == "header" }
val measurablesFooter = measurables.filter { it.layoutId == "footer" }
assertEquals(headerChildrenCount, measurablesHeader.size)
assertSame(measurables[0], measurablesHeader[0])
assertEquals(footerChildrenCount, measurablesFooter.size)
assertSame(measurables[1], measurablesFooter[0])
assertSame(measurables[2], measurablesFooter[1])
layout(0, 0) {}
}
}
}
}
// When a child's measure() is done within the layout, it should not affect the parent's
// size. The parent's layout shouldn't be called when the child's size changes
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun measureInLayoutDoesNotAffectParentSize() {
val white = Color(0xFFFFFFFF)
val blue = Color(0xFF000080)
val model = SquareModel(outerColor = blue, innerColor = white)
var measureCalls = 0
var layoutCalls = 0
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
modifier = remember {
Modifier.drawBehind {
drawRect(model.outerColor)
}
},
content = {
AtLeastSize(
size = model.size,
modifier = Modifier.drawBehind {
drawLatch.countDown()
drawRect(model.innerColor)
}
)
},
measurePolicy = remember {
MeasurePolicy { measurables, constraints ->
measureCalls++
layout(30, 30) {
layoutCalls++
layoutLatch.countDown()
val placeable = measurables[0].measure(constraints)
placeable.place(
(30 - placeable.width) / 2,
(30 - placeable.height) / 2
)
}
}
}
)
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
layoutCalls = 0
measureCalls = 0
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.size = 20
}
validateSquareColors(outerColor = blue, innerColor = white, size = 20, totalSize = 30)
assertEquals(0, measureCalls)
assertEquals(1, layoutCalls)
}
@Test
fun testLayout_whenMeasuringIsDoneDuringPlacing() {
@Composable
fun FixedSizeRow(
width: Int,
height: Int,
content: @Composable () -> Unit
) {
Layout(
content = content,
measurePolicy = { measurables, constraints ->
val resolvedWidth = constraints.constrainWidth(width)
val resolvedHeight = constraints.constrainHeight(height)
layout(resolvedWidth, resolvedHeight) {
val childConstraints = Constraints(
0,
Constraints.Infinity,
resolvedHeight,
resolvedHeight
)
var left = 0
for (measurable in measurables) {
val placeable = measurable.measure(childConstraints)
if (left + placeable.width > width) {
break
}
placeable.place(left, 0)
left += placeable.width
}
}
}
)
}
@Composable
fun FixedWidthBox(
width: Int,
measured: Ref<Boolean?>,
laidOut: Ref<Boolean?>,
drawn: Ref<Boolean?>,
latch: CountDownLatch
) {
Layout(
content = {},
modifier = Modifier.drawBehind {
drawn.value = true
latch.countDown()
},
measurePolicy = { _, constraints ->
measured.value = true
val resolvedWidth = constraints.constrainWidth(width)
val resolvedHeight = constraints.minHeight
layout(resolvedWidth, resolvedHeight) { laidOut.value = true }
}
)
}
val childrenCount = 5
val measured = Array(childrenCount) { Ref<Boolean?>() }
val laidOut = Array(childrenCount) { Ref<Boolean?>() }
val drawn = Array(childrenCount) { Ref<Boolean?>() }
val latch = CountDownLatch(3)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Align {
FixedSizeRow(width = 90, height = 40) {
for (i in 0 until childrenCount) {
FixedWidthBox(
width = 30,
measured = measured[i],
laidOut = laidOut[i],
drawn = drawn[i],
latch = latch
)
}
}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
for (i in 0 until childrenCount) {
assertEquals(i <= 3, measured[i].value ?: false)
assertEquals(i <= 2, laidOut[i].value ?: false)
assertEquals(i <= 2, drawn[i].value ?: false)
}
}
// When a new child is added, the parent must be remeasured because we don't know
// if it affects the size and the child's measure() must be called as well.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testRelayoutOnNewChild() {
val drawChild = mutableStateOf(false)
val outerColor = Color(0xFF000080)
val innerColor = Color(0xFFFFFFFF)
activityTestRule.runOnUiThreadIR {
activity.setContent {
AtLeastSize(size = 30, modifier = Modifier.fillColor(outerColor)) {
if (drawChild.value) {
Padding(size = 20) {
AtLeastSize(size = 20, modifier = Modifier.fillColor(innerColor)) {
}
}
}
}
}
}
// The padded area doesn't draw
validateSquareColors(outerColor = outerColor, innerColor = outerColor, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { drawChild.value = true }
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 20)
}
// When we change a position of one LayoutNode up the tree it automatically
// changes the position of all the children. RepaintBoundary with few intermediate
// LayoutNode parents should be drawn on a correct position
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun moveRootLayoutRedrawsLeafRepaintBoundary() {
val offset = mutableStateOf(0)
drawLatch = CountDownLatch(2)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
modifier = Modifier.fillColor(Color.Green),
content = {
AtLeastSize(size = 10) {
AtLeastSize(
size = 10,
modifier = Modifier.graphicsLayer().fillColor(Color.Cyan)
) {
}
}
}
) { measurables, constraints ->
layout(width = 20, height = 20) {
measurables.first().measure(constraints)
.place(offset.value, offset.value)
}
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
activityTestRule.waitAndScreenShot().apply {
assertRect(Color.Cyan, size = 10, centerX = 5, centerY = 5)
assertRect(Color.Green, size = 10, centerX = 15, centerY = 15)
}
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 10 }
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
activityTestRule.waitAndScreenShot().apply {
assertRect(Color.Green, size = 10, centerX = 5, centerY = 5)
assertRect(Color.Cyan, size = 10, centerX = 15, centerY = 15)
}
}
// When a child is removed, the parent must be remeasured and redrawn.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testRedrawOnRemovedChild() {
val drawChild = mutableStateOf(true)
val outerColor = Color(0xFF000080)
val innerColor = Color(0xFFFFFFFF)
activityTestRule.runOnUiThreadIR {
activity.setContent {
AtLeastSize(
size = 30,
modifier = Modifier.drawBehind {
drawLatch.countDown()
drawRect(outerColor)
}
) {
AtLeastSize(size = 30) {
if (drawChild.value) {
Padding(size = 10) {
AtLeastSize(
size = 10,
modifier = Modifier.drawBehind {
drawLatch.countDown()
drawRect(innerColor)
}
)
}
}
}
}
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { drawChild.value = false }
// The padded area doesn't draw
validateSquareColors(outerColor = outerColor, innerColor = outerColor, size = 10)
}
// When a child is removed, the parent must be remeasured.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testRelayoutOnRemovedChild() {
val drawChild = mutableStateOf(true)
val outerColor = Color(0xFF000080)
val innerColor = Color(0xFFFFFFFF)
activityTestRule.runOnUiThreadIR {
activity.setContent {
AtLeastSize(
size = 30,
modifier = Modifier.drawBehind {
drawLatch.countDown()
drawRect(outerColor)
}
) {
Padding(size = 20) {
if (drawChild.value) {
AtLeastSize(
size = 20,
modifier = Modifier.drawBehind {
drawLatch.countDown()
drawRect(innerColor)
}
)
}
}
}
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 20)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { drawChild.value = false }
// The padded area doesn't draw
validateSquareColors(outerColor = outerColor, innerColor = outerColor, size = 10)
}
@Test
fun testAlignmentLines() {
val TestVerticalLine = VerticalAlignmentLine(::min)
val TestHorizontalLine = HorizontalAlignmentLine(::max)
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child1 = @Composable {
Wrap {
Layout(content = {}) { _, _ ->
layout(
0,
0,
mapOf(
TestVerticalLine to 10,
TestHorizontalLine to 20
)
) { }
}
}
}
val child2 = @Composable {
Wrap {
Layout(content = {}) { _, _ ->
layout(
0,
0,
mapOf(
TestVerticalLine to 20,
TestHorizontalLine to 10
)
) { }
}
}
}
val inner = @Composable {
Layout({ child1(); child2() }) { measurables, constraints ->
val placeable1 = measurables[0].measure(constraints)
val placeable2 = measurables[1].measure(constraints)
assertEquals(10, placeable1[TestVerticalLine])
assertEquals(20, placeable1[TestHorizontalLine])
assertEquals(20, placeable2[TestVerticalLine])
assertEquals(10, placeable2[TestHorizontalLine])
layout(0, 0) {
placeable1.place(0, 0)
placeable2.place(0, 0)
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
assertEquals(10, placeable[TestVerticalLine])
assertEquals(20, placeable[TestHorizontalLine])
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun testAlignmentLines_areNotInheritedFromInvisibleChildren() {
val TestLine1 = VerticalAlignmentLine(::min)
val TestLine2 = VerticalAlignmentLine(::min)
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child1 = @Composable {
Layout(content = {}) { _, _ ->
layout(0, 0, mapOf(TestLine1 to 10)) {}
}
}
val child2 = @Composable {
Layout(content = {}) { _, _ ->
layout(0, 0, mapOf(TestLine2 to 20)) { }
}
}
val inner = @Composable {
Layout({ child1(); child2() }) { measurables, constraints ->
val placeable1 = measurables[0].measure(constraints)
measurables[1].measure(constraints)
layout(0, 0) {
// Only place the first child.
placeable1.place(0, 0)
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
assertEquals(10, placeable[TestLine1])
assertEquals(AlignmentLine.Unspecified, placeable[TestLine2])
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun testAlignmentLines_doNotCauseMultipleMeasuresOrLayouts() {
val TestLine1 = VerticalAlignmentLine(::min)
val TestLine2 = VerticalAlignmentLine(::min)
var child1Measures = 0
var child2Measures = 0
var child1Layouts = 0
var child2Layouts = 0
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child1 = @Composable {
Layout(content = {}) { _, _ ->
++child1Measures
layout(0, 0, mapOf(TestLine1 to 10)) {
++child1Layouts
}
}
}
val child2 = @Composable {
Layout(content = {}) { _, _ ->
++child2Measures
layout(0, 0, mapOf(TestLine2 to 20)) {
++child2Layouts
}
}
}
val inner = @Composable {
Layout({ child1(); child2() }) { measurables, constraints ->
val placeable1 = measurables[0].measure(constraints)
val placeable2 = measurables[1].measure(constraints)
layout(0, 0) {
placeable1.place(0, 0)
placeable2.place(0, 0)
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
assertEquals(10, placeable[TestLine1])
assertEquals(20, placeable[TestLine2])
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, child1Measures)
assertEquals(1, child2Measures)
assertEquals(1, child1Layouts)
assertEquals(1, child2Layouts)
}
@Test
fun testAlignmentLines_onlyLayoutEarlyWhenNeeded() {
val TestLine1 = VerticalAlignmentLine(::min)
val TestLine2 = VerticalAlignmentLine(::min)
var child1Measures = 0
var child2Measures = 0
var child1Layouts = 0
var child2Layouts = 0
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child1 = @Composable {
Layout(content = {}) { _, _ ->
++child1Measures
layout(0, 0, mapOf(TestLine1 to 10)) {
++child1Layouts
}
}
}
val child2 = @Composable {
Layout(content = {}) { _, _ ->
++child2Measures
layout(0, 0, mapOf(TestLine2 to 20)) {
++child2Layouts
}
}
}
val inner = @Composable {
Layout({ child1(); child2() }) { measurables, constraints ->
val placeable1 = measurables[0].measure(constraints)
assertEquals(10, placeable1[TestLine1])
val placeable2 = measurables[1].measure(constraints)
layout(0, 0) {
placeable1.place(0, 0)
placeable2.place(0, 0)
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(placeable.width, placeable.height) {
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, child1Measures)
assertEquals(1, child2Measures)
assertEquals(1, child1Layouts)
assertEquals(0, child2Layouts)
}
@Test
fun testAlignmentLines_canBeQueriedInThePositioningBlock() {
val TestLine = VerticalAlignmentLine(::min)
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child1 = @Composable {
Layout(content = { }) { _, _ ->
layout(0, 0, mapOf(TestLine to 10)) { }
}
}
val child2 = @Composable {
Layout(content = {}) { _, _ ->
layout(
0,
0,
mapOf(TestLine to 20)
) { }
}
}
val inner = @Composable {
Layout({ child1(); child2() }) { measurables, constraints ->
val placeable1 = measurables[0].measure(constraints)
layout(0, 0) {
assertEquals(10, placeable1[TestLine])
val placeable2 = measurables[1].measure(constraints)
assertEquals(20, placeable2[TestLine])
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(placeable.width, placeable.height) {
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun testAlignmentLines_doNotCauseExtraLayout_whenQueriedAfterPositioning() {
val TestLine = VerticalAlignmentLine(::min)
val layoutLatch = CountDownLatch(1)
var childLayouts = 0
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Layout(content = { }) { _, _ ->
layout(0, 0, mapOf(TestLine to 10)) {
++childLayouts
}
}
}
val inner = @Composable {
Layout({ child() }) { measurables, constraints ->
val placeable = measurables[0].measure(constraints)
layout(0, 0) {
assertEquals(10, placeable[TestLine])
placeable.place(0, 0)
assertEquals(10, placeable[TestLine])
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, childLayouts)
}
@Test
fun testAlignmentLines_recomposeCorrectly() {
val TestLine = VerticalAlignmentLine(::min)
var layoutLatch = CountDownLatch(1)
val offset = mutableStateOf(10)
var measure = 0
var layout = 0
var linePosition: Int? = null
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Layout(content = {}) { _, _ ->
layout(0, 0, mapOf(TestLine to offset.value)) {}
}
}
Layout(child) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
linePosition = placeable[TestLine]
++measure
layout(placeable.width, placeable.height) {
++layout
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, measure)
assertEquals(1, layout)
assertEquals(10, linePosition)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
offset.value = 20
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(2, measure)
assertEquals(2, layout)
assertEquals(20, linePosition)
}
@Test
fun testAlignmentLines_recomposeCorrectly_whenQueriedInLayout() {
val TestLine = VerticalAlignmentLine(::min)
var layoutLatch = CountDownLatch(1)
val offset = mutableStateOf(10)
var measure = 0
var layout = 0
var linePosition: Int? = null
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Layout(content = {}) { _, _ ->
layout(
0,
0,
mapOf(TestLine to offset.value)
) {}
}
}
Layout(child) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
++measure
layout(placeable.width, placeable.height) {
linePosition = placeable[TestLine]
++layout
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, measure)
assertEquals(1, layout)
assertEquals(10, linePosition)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 20 }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, measure)
assertEquals(2, layout)
assertEquals(20, linePosition)
}
@Test
fun testAlignmentLines_recomposeCorrectly_whenMeasuredAndQueriedInLayout() {
val TestLine = VerticalAlignmentLine(::min)
var layoutLatch = CountDownLatch(1)
val offset = mutableStateOf(10)
var measure = 0
var layout = 0
var linePosition: Int? = null
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Layout(content = {}) { _, _ ->
layout(0, 0, mapOf(TestLine to offset.value)) { }
}
}
Layout(child) { measurables, constraints ->
++measure
layout(1, 1) {
val placeable = measurables.first().measure(constraints)
linePosition = placeable[TestLine]
++layout
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, measure)
assertEquals(1, layout)
assertEquals(10, linePosition)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 20 }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, measure)
assertEquals(2, layout)
assertEquals(20, linePosition)
}
@Test
fun testAlignmentLines_onlyComputesAlignmentLinesWhenNeeded() {
var layoutLatch = CountDownLatch(1)
val offset = mutableStateOf(10)
var alignmentLinesCalculations = 0
val TestLine = VerticalAlignmentLine { _, _ ->
++alignmentLinesCalculations
0
}
var linePosition by mutableStateOf(10)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val innerChild = @Composable {
offset.value // Artificial remeasure.
Layout(content = {}) { _, _ ->
layout(0, 0, mapOf(TestLine to linePosition)) { }
}
}
val child = @Composable {
Layout({ innerChild(); innerChild() }) { measurables, constraints ->
offset.value // Artificial remeasure.
val placeable1 = measurables[0].measure(constraints)
val placeable2 = measurables[1].measure(constraints)
layout(0, 0) {
placeable1.place(0, 0)
placeable2.place(0, 0)
}
}
}
Layout(child) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
if (offset.value < 15) {
placeable[TestLine]
}
layout(0, 0) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, alignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 20; linePosition = 20 }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, alignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 10; linePosition = 30 }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(2, alignmentLinesCalculations)
}
@Test
fun testAlignmentLines_providedLinesOverrideInherited() {
val layoutLatch = CountDownLatch(1)
val TestLine = VerticalAlignmentLine(::min)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val innerChild = @Composable {
Layout(content = {}) { _, _ ->
layout(0, 0, mapOf(TestLine to 10)) { }
}
}
val child = @Composable {
Layout({ innerChild() }) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(0, 0, mapOf(TestLine to 20)) {
placeable.place(0, 0)
}
}
}
Layout(child) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
assertEquals(20, placeable[TestLine])
layout(0, 0) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun testAlignmentLines_areRecalculatedCorrectlyOnRelayout_withNoRemeasure() {
val TestLine = VerticalAlignmentLine(::min)
var layoutLatch = CountDownLatch(1)
var innerChildMeasures = 0
var innerChildLayouts = 0
var outerChildMeasures = 0
var outerChildLayouts = 0
val offset = mutableStateOf(0)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Layout(content = {}) { _, _ ->
++innerChildMeasures
layout(0, 0, mapOf(TestLine to 10)) { ++innerChildLayouts }
}
}
val inner = @Composable {
Layout({ Wrap { Wrap { child() } } }) { measurables, constraints ->
++outerChildMeasures
val placeable = measurables[0].measure(constraints)
layout(0, 0) {
++outerChildLayouts
placeable.place(offset.value, 0)
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
val width = placeable.width.coerceAtLeast(10)
val height = placeable.height.coerceAtLeast(10)
layout(width, height) {
assertEquals(offset.value + 10, placeable[TestLine])
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, innerChildMeasures)
assertEquals(1, innerChildLayouts)
assertEquals(1, outerChildMeasures)
assertEquals(1, outerChildLayouts)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
offset.value = 10
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, innerChildMeasures)
assertEquals(1, innerChildLayouts)
assertEquals(1, outerChildMeasures)
assertEquals(2, outerChildLayouts)
}
@Test
fun testAlignmentLines_whenQueriedAfterPlacing() {
val TestLine = VerticalAlignmentLine(::min)
val layoutLatch = CountDownLatch(1)
var childLayouts = 0
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Layout(content = {}) { _, constraints ->
layout(
constraints.minWidth,
constraints.minHeight,
mapOf(TestLine to 10)
) { ++childLayouts }
}
}
val inner = @Composable {
Layout({ Wrap { Wrap { child() } } }) { measurables, constraints ->
val placeable = measurables[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
assertEquals(10, placeable[TestLine])
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, childLayouts)
}
@Test
fun testAlignmentLines_whenQueriedAfterPlacing_haveCorrectNumberOfLayouts() {
var childLayouts = 0
var childAlignmentLinesCalculations = 0
val TestLine = VerticalAlignmentLine { v1, _ ->
++childAlignmentLinesCalculations
v1
}
val offset = mutableStateOf(10)
var linePositionState by mutableStateOf(10)
var linePosition = 10
fun changeLinePosition() {
linePosition = 30 - linePosition
linePositionState = 30 - linePositionState
}
var layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val childChild = @Composable {
Layout(content = {}) { _, constraints ->
layout(
constraints.minWidth,
constraints.minHeight,
mapOf(TestLine to linePositionState)
) {
offset.value // To ensure relayout.
}
}
}
val child = @Composable {
Layout(content = { childChild(); childChild() }) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
layout(constraints.minWidth, constraints.minHeight) {
offset.value // To ensure relayout.
placeables.forEach { it.place(0, 0) }
++childLayouts
}
}
}
val inner = @Composable {
Layout({
WrapForceRelayout(offset) { child() }
}) { measurables, constraints ->
val placeable = measurables[0].measure(constraints)
layout(placeable.width, placeable.height) {
if (offset.value > 15) assertEquals(linePosition, placeable[TestLine])
placeable.place(0, 0)
if (offset.value > 5) assertEquals(linePosition, placeable[TestLine])
}
}
}
Layout(inner) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
val width = placeable.width.coerceAtLeast(10)
val height = placeable.height.coerceAtLeast(10)
layout(width, height) {
offset.value // To ensure relayout.
placeable.place(0, 0)
layoutLatch.countDown()
}
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(2, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 1 }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(3, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 10; changeLinePosition() }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(5, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 12; changeLinePosition() }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(7, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 17; changeLinePosition() }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(9, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 12; changeLinePosition() }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(11, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 1; changeLinePosition() }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(13, childLayouts + childAlignmentLinesCalculations)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR { offset.value = 10; changeLinePosition() }
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(15, childLayouts + childAlignmentLinesCalculations)
}
@Test
fun testAlignmentLines_readFromModifier_duringMeasurement() = with(density) {
val testVerticalLine = VerticalAlignmentLine(::min)
val testHorizontalLine = HorizontalAlignmentLine(::max)
val assertLines: Modifier.(Int, Int) -> Modifier = { vertical, horizontal ->
this.then(object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
assertEquals(vertical, placeable[testVerticalLine])
assertEquals(horizontal, placeable[testHorizontalLine])
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
})
}
testAlignmentLinesReads(testVerticalLine, testHorizontalLine, assertLines)
}
@Test
fun testAlignmentLines_readFromModifier_duringPositioning_before() = with(density) {
val testVerticalLine = VerticalAlignmentLine(::min)
val testHorizontalLine = HorizontalAlignmentLine(::max)
val assertLines: Modifier.(Int, Int) -> Modifier = { vertical, horizontal ->
this.then(object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
assertEquals(vertical, placeable[testVerticalLine])
assertEquals(horizontal, placeable[testHorizontalLine])
placeable.place(0, 0)
}
}
})
}
testAlignmentLinesReads(testVerticalLine, testHorizontalLine, assertLines)
}
@Test
fun testAlignmentLines_readFromModifier_duringPositioning_after() = with(density) {
val testVerticalLine = VerticalAlignmentLine(::min)
val testHorizontalLine = HorizontalAlignmentLine(::max)
val assertLines: Modifier.(Int, Int) -> Modifier = { vertical, horizontal ->
this.then(object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
assertEquals(vertical, placeable[testVerticalLine])
assertEquals(horizontal, placeable[testHorizontalLine])
}
}
})
}
testAlignmentLinesReads(testVerticalLine, testHorizontalLine, assertLines)
}
private fun Density.testAlignmentLinesReads(
testVerticalLine: VerticalAlignmentLine,
testHorizontalLine: HorizontalAlignmentLine,
assertLines: Modifier.(Int, Int) -> Modifier
) {
val layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val layout = @Composable { modifier: Modifier ->
Layout(modifier = modifier, content = {}) { _, _ ->
layout(
0,
0,
mapOf(
testVerticalLine to 10,
testHorizontalLine to 20
)
) {
layoutLatch.countDown()
}
}
}
layout(Modifier.assertLines(10, 20))
layout(Modifier.assertLines(30, 30).offset(20.toDp(), 10.toDp()))
layout(
Modifier
.assertLines(30, 30)
.graphicsLayer()
.offset(20.toDp(), 10.toDp())
)
layout(
Modifier
.assertLines(30, 30)
.background(Color.Blue)
.graphicsLayer()
.offset(20.toDp(), 10.toDp())
.graphicsLayer()
.background(Color.Blue)
)
layout(
Modifier
.background(Color.Blue)
.assertLines(30, 30)
.background(Color.Blue)
.graphicsLayer()
.offset(20.toDp(), 10.toDp())
.graphicsLayer()
.background(Color.Blue)
)
Wrap(
Modifier
.background(Color.Blue)
.assertLines(30, 30)
.background(Color.Blue)
.graphicsLayer()
.offset(20.toDp(), 10.toDp())
.graphicsLayer()
.background(Color.Blue)
) {
layout(Modifier)
}
Wrap(
Modifier
.background(Color.Blue)
.assertLines(40, 50)
.background(Color.Blue)
.graphicsLayer()
.offset(20.toDp(), 10.toDp())
.graphicsLayer()
.background(Color.Blue)
) {
layout(Modifier.offset(10.toDp(), 20.toDp()))
}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun testLayoutBeforeDraw_forRecomposingNodesNotAffectingRootSize() {
val offset = mutableStateOf(0)
var latch = CountDownLatch(1)
var laidOut = false
activityTestRule.runOnUiThreadIR {
activity.setContent {
val container = @Composable { content: @Composable () -> Unit ->
// This simulates a Container optimisation, when the child does not
// affect parent size.
Layout(content) { measurables, constraints ->
layout(30, 30) {
measurables[0].measure(constraints).place(0, 0)
}
}
}
val recomposingChild = @Composable { content: @Composable (Int) -> Unit ->
// This simulates a child that recomposes, for example due to a transition.
content(offset.value)
}
val assumeLayoutBeforeDraw = @Composable { value: Int ->
// This assumes a layout was done before the draw pass.
Layout(
content = {},
modifier = Modifier.drawBehind {
assertEquals(offset.value, value)
assertTrue(laidOut)
latch.countDown()
}
) { _, _ ->
laidOut = true
layout(0, 0) {}
}
}
container {
recomposingChild {
assumeLayoutBeforeDraw(it)
}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
latch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
offset.value = 10
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun testDrawWithLayoutNotPlaced() {
val latch = CountDownLatch(1)
var drawn = false
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
AtLeastSize(30, modifier = Modifier.drawBehind { drawn = true })
},
modifier = Modifier.drawLatchModifier()
) { _, _ ->
// don't measure or place the AtLeastSize
latch.countDown()
layout(20, 20) {}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThreadIR {
assertFalse(drawn)
}
}
/**
* Because we use invalidate() to cause relayout when children
* are laid out, we want to ensure that when the View is 0-sized
* that it gets a relayout when it needs to change to non-0
*/
@Test
fun testZeroSizeCanRelayout() {
var latch = CountDownLatch(1)
val model = SquareModel(size = 0)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(content = { }) { _, _ ->
latch.countDown()
layout(model.size, model.size) {}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
latch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.size = 10
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun testZeroSizeCanRelayout_child() {
var latch = CountDownLatch(1)
val model = SquareModel(size = 0)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
Layout(content = {}) { _, _ ->
latch.countDown()
layout(model.size, model.size) {}
}
}
) { measurables, constraints ->
val placeable = measurables[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
latch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.size = 10
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun testZeroSizeCanRelayout_childRepaintBoundary() {
var latch = CountDownLatch(1)
val model = SquareModel(size = 0)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
Layout(
modifier = Modifier.graphicsLayer(),
content = {}
) { _, _ ->
latch.countDown()
layout(model.size, model.size) {}
}
}
) { measurables, constraints ->
val placeable = measurables[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
latch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.size = 10
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun parentSizeForDrawIsProvidedWithoutPadding() {
val latch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
val drawnContent = Modifier.drawBehind {
assertEquals(100.0f, size.width)
assertEquals(100.0f, size.height)
latch.countDown()
}
AtLeastSize(100, Modifier.padding(10).then(drawnContent)) {
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun parentSizeForDrawInsideRepaintBoundaryIsProvidedWithoutPadding() {
val latch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
AtLeastSize(
100,
Modifier.padding(10).graphicsLayer()
.drawBehind {
assertEquals(100.0f, size.width)
assertEquals(100.0f, size.height)
latch.countDown()
}
) {
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun alignmentLinesInheritedCorrectlyByParents_withModifiedPosition() {
val testLine = HorizontalAlignmentLine(::min)
val latch = CountDownLatch(1)
val alignmentLinePosition = 10
val padding = 20
activityTestRule.runOnUiThreadIR {
activity.setContent {
val child = @Composable {
Wrap {
Layout(content = {}, modifier = Modifier.padding(padding)) { _, _ ->
layout(0, 0, mapOf(testLine to alignmentLinePosition)) { }
}
}
}
Layout(child) { measurables, constraints ->
assertEquals(
padding + alignmentLinePosition,
measurables[0].measure(constraints)[testLine]
)
latch.countDown()
layout(0, 0) { }
}
}
}
}
@Test
fun modifiers_validateCorrectSizes() {
val layoutModifier = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
val parentDataModifier = object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?) = parentData
}
val size = 50
val latch = CountDownLatch(2)
val childSizes = arrayOfNulls<IntSize>(2)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
FixedSize(size, layoutModifier)
FixedSize(size, parentDataModifier)
},
measurePolicy = { measurables, constraints ->
for (i in 0 until measurables.size) {
val child = measurables[i]
val placeable = child.measure(constraints)
childSizes[i] = IntSize(placeable.width, placeable.height)
latch.countDown()
}
layout(0, 0) { }
}
)
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
assertEquals(IntSize(size, size), childSizes[0])
assertEquals(IntSize(size, size), childSizes[1])
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_drawPositioning() {
val outerColor = Color.Blue
val innerColor = Color.White
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(30, Modifier.background(outerColor)) {
FixedSize(
10,
Modifier.padding(10).background(innerColor).drawLatchModifier()
)
}
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 10)
}
@Test
fun drawModifier_testLayoutDirection() {
val drawLatch = CountDownLatch(1)
val layoutDirection = Ref<LayoutDirection>()
activityTestRule.runOnUiThreadIR {
activity.setContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
FixedSize(
size = 50,
modifier = Modifier.drawBehind {
layoutDirection.value = this.layoutDirection
drawLatch.countDown()
}
)
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
assertEquals(LayoutDirection.Rtl, layoutDirection.value)
}
@Test
fun layoutModifier_testLayoutDirection() {
val latch = CountDownLatch(1)
val layoutDirection = Ref<LayoutDirection>()
val layoutModifier = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
layoutDirection.value = this.layoutDirection
latch.countDown()
return layout(0, 0) {}
}
}
activityTestRule.runOnUiThreadIR {
activity.setContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
FixedSize(
size = 50,
modifier = layoutModifier
)
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
assertEquals(LayoutDirection.Rtl, layoutDirection.value)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_modelChangesOnRoot() {
val model = SquareModel(innerColor = Color.White, outerColor = Color.Green)
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(30, Modifier.background(model, false)) {
FixedSize(
10,
Modifier.padding(10).background(model, true).drawLatchModifier()
)
}
}
}
validateSquareColors(outerColor = Color.Green, innerColor = Color.White, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.innerColor = Color.Yellow
}
validateSquareColors(outerColor = Color.Green, innerColor = Color.Yellow, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_modelChangesOnRepaintBoundary() {
val model = SquareModel(innerColor = Color.White, outerColor = Color.Green)
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(30, Modifier.background(Color.Green)) {
FixedSize(
10,
Modifier.graphicsLayer()
.padding(10)
.background(model, true)
.drawLatchModifier()
)
}
}
}
validateSquareColors(outerColor = Color.Green, innerColor = Color.White, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
model.innerColor = Color.Yellow
}
validateSquareColors(outerColor = Color.Green, innerColor = Color.Yellow, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_oneModifier() {
val outerColor = Color.Blue
val innerColor = Color.White
activityTestRule.runOnUiThreadIR {
activity.setContent {
val colorModifier = Modifier.drawBehind {
drawRect(outerColor)
drawRect(
innerColor,
topLeft = Offset(10f, 10f),
size = Size(10f, 10f)
)
drawLatch.countDown()
}
FixedSize(30, colorModifier)
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_nestedModifiers() {
val outerColor = Color.Blue
val innerColor = Color.White
activityTestRule.runOnUiThreadIR {
activity.setContent {
val countDownModifier = Modifier.drawBehind {
drawLatch.countDown()
}
FixedSize(30, countDownModifier.background(color = outerColor)) {
Padding(10) {
FixedSize(10, Modifier.background(color = innerColor))
}
}
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_withLayoutModifier() {
val outerColor = Color.Blue
val innerColor = Color.White
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(30, Modifier.background(color = outerColor)) {
FixedSize(
size = 10,
modifier = Modifier.padding(10)
.background(color = innerColor)
.drawLatchModifier()
)
}
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawModifier_withLayout() {
val outerColor = Color.Blue
val innerColor = Color.White
activityTestRule.runOnUiThreadIR {
activity.setContent {
val drawAndOffset = Modifier.drawWithContent {
drawRect(outerColor)
translate(10f, 10f) {
this@drawWithContent.drawContent()
}
}
FixedSize(30, drawAndOffset) {
FixedSize(
size = 10,
modifier = AlignTopLeft.background(innerColor).drawLatchModifier()
)
}
}
}
validateSquareColors(outerColor = outerColor, innerColor = innerColor, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun layoutModifier_redrawsCorrectlyWhenOnlyNonModifiedSizeChanges() {
val blue = Color(0xFF000080)
val green = Color(0xFF00FF00)
val offset = mutableStateOf(10)
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(
30,
modifier = Modifier.drawBehind {
drawRect(green)
}
) {
FixedSize(
offset.value,
modifier = AlignTopLeft.graphicsLayer()
.drawBehind {
drawLatch.countDown()
drawRect(blue)
}
) {
}
}
}
}
validateSquareColors(outerColor = green, innerColor = blue, size = 10, offset = -10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
offset.value = 20
}
validateSquareColors(
outerColor = green,
innerColor = blue,
size = 20,
offset = -5,
totalSize = 30
)
}
@Test
fun layoutModifier_convenienceApi() {
val size = 100
val offset = 15
val latch = CountDownLatch(1)
var resultCoordinates: LayoutCoordinates? = null
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(
size = size,
modifier = Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(offset, offset)
}
}.onGloballyPositioned {
resultCoordinates = it
latch.countDown()
}
)
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
activity.runOnUiThread {
assertEquals(size, resultCoordinates?.size?.height)
assertEquals(size, resultCoordinates?.size?.width)
assertEquals(IntOffset(offset, offset).toOffset(), resultCoordinates?.positionInRoot())
}
}
@Test
fun layoutModifier_convenienceApi_equivalent() {
val size = 100
val offset = 15
val latch = CountDownLatch(2)
var convenienceCoordinates: LayoutCoordinates? = null
var coordinates: LayoutCoordinates? = null
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(
size = size,
modifier = Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(offset, offset)
}
}.onGloballyPositioned {
convenienceCoordinates = it
latch.countDown()
}
)
val layoutModifier = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.place(offset, offset)
}
}
}
FixedSize(
size = size,
modifier = layoutModifier.onGloballyPositioned {
coordinates = it
latch.countDown()
}
)
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
activity.runOnUiThread {
assertEquals(coordinates?.size?.height, convenienceCoordinates?.size?.height)
assertEquals(coordinates?.size?.width, convenienceCoordinates?.size?.width)
assertEquals(coordinates?.positionInRoot(), convenienceCoordinates?.positionInRoot())
}
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun modifier_combinedModifiers() {
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(30, Modifier.background(Color.Blue).drawLatchModifier()) {
JustConstraints(LayoutAndDrawModifier(Color.White)) {
}
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
}
// Tests that show layout bounds draws outlines around content and modifiers
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
@OptIn(InternalComposeUiApi::class)
fun showLayoutBounds_content() {
activityTestRule.runOnUiThreadIR {
activity.setContent {
FixedSize(size = 30, modifier = Modifier.background(Color.White)) {
FixedSize(
size = 10,
modifier = Modifier.padding(5)
.padding(5)
.drawLatchModifier()
)
}
}
val composeView = activityTestRule.findAndroidComposeView() as AndroidComposeView
composeView.showLayoutBounds = true
}
activityTestRule.waitAndScreenShot().apply {
assertRect(Color.White, size = 8)
assertRect(Color.Red, size = 10, holeSize = 8)
assertRect(Color.White, size = 18, holeSize = 10)
assertRect(Color.Blue, size = 20, holeSize = 18)
assertRect(Color.White, size = 28, holeSize = 20)
assertRect(Color.Red, size = 30, holeSize = 28)
}
}
// Ensure that showLayoutBounds is reset in onResume() to whatever is set in the
// settings.
@Test
@OptIn(InternalComposeUiApi::class)
fun showLayoutBounds_resetOnResume() {
activityTestRule.runOnUiThreadIR {
activity.setContent {
}
}
val composeView = activityTestRule.findAndroidComposeView() as AndroidComposeView
// find out whatever the current setting value is for showLayoutBounds
val startShowLayoutBounds = composeView.showLayoutBounds
activityTestRule.runOnUiThread {
val intent = Intent(activity, TestActivity::class.java)
activity.startActivity(intent)
}
assertTrue(activity.stopLatch.await(5, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
// set showLayoutBounds to something different
composeView.showLayoutBounds = !startShowLayoutBounds
activity.resumeLatch = CountDownLatch(1)
TestActivity.resumedActivity!!.finish()
}
assertTrue(activity.resumeLatch.await(5, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
// ensure showLayoutBounds was reset in onResume()
assertEquals(startShowLayoutBounds, composeView.showLayoutBounds)
}
}
@Test
fun requestRemeasureForAlreadyMeasuredChildWhileTheParentIsStillMeasuring() {
val drawlatch = CountDownLatch(1)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
val state = remember { mutableStateOf(false) }
var lastLayoutValue: Boolean = false
Layout(
content = {},
modifier = Modifier.drawBehind {
// this verifies the layout was remeasured before being drawn
assertTrue(lastLayoutValue)
drawlatch.countDown()
}
) { _, _ ->
lastLayoutValue = state.value
// this registers the value read
if (!state.value) {
// change the value right inside the measure block
// it will cause one more remeasure pass as we also read this value
state.value = true
}
layout(100, 100) {}
}
FixedSize(30, content = {})
}
) { measurables, constraints ->
val (first, second) = measurables
val firstPlaceable = first.measure(constraints)
// switch frame, as inside the measure block we changed the model value
// this will trigger requestRemeasure on this first layout
Snapshot.sendApplyNotifications()
val secondPlaceable = second.measure(constraints)
layout(30, 30) {
firstPlaceable.place(0, 0)
secondPlaceable.place(0, 0)
}
}
}
}
assertTrue(drawlatch.await(1, TimeUnit.SECONDS))
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun layerModifier_scaleDraw() {
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
size = 30,
modifier = Modifier.background(Color.Blue)
) {
FixedSize(
size = 20,
modifier = AlignTopLeft
.padding(5)
.scale(0.5f)
.background(Color.Red)
.latch(drawLatch)
) {}
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Red, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun layerModifier_scaleChange() {
val scale = mutableStateOf(1f)
val layerModifier = Modifier.graphicsLayer {
scaleX = scale.value
scaleY = scale.value
}
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
size = 30,
modifier = Modifier.background(Color.Blue)
) {
FixedSize(
size = 10,
modifier = Modifier.padding(10)
.then(layerModifier)
.background(Color.Red)
.latch(drawLatch)
) {}
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Red, size = 10)
activityTestRule.runOnUiThread {
scale.value = 2f
}
activityTestRule.waitAndScreenShot().apply {
assertRect(Color.Red, size = 20, centerX = 15, centerY = 15)
}
}
// Test that when no clip to outline is set that it still draws properly.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun layerModifier_noClip() {
val triangleShape = object : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
) = Outline.Generic(
Path().apply {
moveTo(size.width / 2f, 0f)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
)
}
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
size = 30
) {
FixedSize(
size = 10,
modifier = Modifier.padding(10)
.graphicsLayer(shape = triangleShape)
.drawBehind {
drawRect(
Color.Blue,
topLeft = Offset(-10f, -10f),
size = Size(30.0f, 30.0f)
)
}
.background(Color.Red)
.latch(drawLatch)
) {}
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Red, size = 10)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testInvalidationMultipleLayers() {
val innerColor = mutableStateOf(Color.Red)
activityTestRule.runOnUiThread {
activity.setContent {
val content: @Composable () -> Unit = remember {
@Composable {
FixedSize(
size = 10,
modifier = Modifier.graphicsLayer()
.padding(10)
.background(innerColor.value)
.latch(drawLatch)
) {}
}
}
FixedSize(
size = 30,
modifier = Modifier.graphicsLayer().background(Color.Blue)
) {
FixedSize(
size = 30,
modifier = Modifier.graphicsLayer(),
content = content
)
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Red, size = 10)
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
innerColor.value = Color.White
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
}
@Test
fun doubleDraw() {
val offset = mutableStateOf(0)
var outerLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
30,
Modifier.drawBehind { outerLatch.countDown() }.graphicsLayer()
) {
FixedSize(
10,
Modifier.drawBehind {
drawLine(
Color.Blue,
Offset(offset.value.toFloat(), 0f),
Offset(0f, offset.value.toFloat()),
strokeWidth = Stroke.HairlineWidth
)
drawLatch.countDown()
}
)
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
assertTrue(outerLatch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
drawLatch = CountDownLatch(1)
outerLatch = CountDownLatch(1)
offset.value = 10
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
assertFalse(outerLatch.await(200, TimeUnit.MILLISECONDS))
}
// When a child with a layer is removed with its children, it shouldn't crash.
@Test
fun detachChildWithLayer() {
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(10, Modifier.graphicsLayer()) {
FixedSize(8)
}
}
activity.setContentView(View(activity)) // Replace content view with empty
}
}
// When a layer moves, it should redraw properly
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun drawOnLayerMove() {
val offset = mutableStateOf(10)
var placeLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
activity.setContent {
val yellowSquare = @Composable {
FixedSize(
10, Modifier.graphicsLayer().background(Color.Yellow).drawLatchModifier()
) {
}
}
Layout(
modifier = Modifier.background(Color.Red),
content = yellowSquare
) { measurables, _ ->
val childConstraints = Constraints.fixed(10, 10)
val p = measurables[0].measure(childConstraints)
layout(30, 30) {
p.place(offset.value, offset.value)
placeLatch.countDown()
}
}
}
}
validateSquareColors(outerColor = Color.Red, innerColor = Color.Yellow, size = 10)
placeLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
offset.value = 5
}
// Wait for layout to complete
assertTrue(placeLatch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
}
activityTestRule.waitAndScreenShot(forceInvalidate = false).apply {
// just test that it is red around the Yellow
assertRect(Color.Red, size = 20, centerX = 10, centerY = 10, holeSize = 10)
// now test that it is red in the lower-right
assertRect(Color.Red, size = 10, centerX = 25, centerY = 25)
assertRect(Color.Yellow, size = 10, centerX = 10, centerY = 10)
}
}
// When a layer property changes, it should redraw properly
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun drawOnLayerPropertyChange() {
val offset = mutableStateOf(0f)
var translationLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(30, Modifier.background(Color.Red).drawLatchModifier()) {
FixedSize(
10,
Modifier.padding(10)
.graphicsLayer {
translationLatch.countDown()
translationX = offset.value
translationY = offset.value
}.background(Color.Yellow)
) {
}
}
}
}
validateSquareColors(outerColor = Color.Red, innerColor = Color.Yellow, size = 10)
translationLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
offset.value = -5f
}
// Wait for translation to complete
assertTrue(translationLatch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
}
activityTestRule.waitAndScreenShot(forceInvalidate = false).apply {
// just test that it is red around the Yellow
assertRect(Color.Red, size = 20, centerX = 10, centerY = 10, holeSize = 10)
// now test that it is red in the lower-right
assertRect(Color.Red, size = 10, centerX = 25, centerY = 25)
assertRect(Color.Yellow, size = 10, centerX = 10, centerY = 10)
}
}
// Delegates don't change when the modifier types remain the same
@Test
fun instancesKeepDelegates() {
var color by mutableStateOf(Color.Red)
var size by mutableStateOf(30)
var m: Measurable? = null
val layoutCaptureModifier = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
m = measurable
val p = measurable.measure(constraints)
return layout(p.width, p.height) {
p.place(0, 0)
}
}
}
val drawCaptureModifier = object : DrawModifier {
override fun ContentDrawScope.draw() {
drawLatch.countDown()
}
}
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
size = size,
modifier = layoutCaptureModifier.background(color).then(drawCaptureModifier)
) {}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
var firstMeasurable = m
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
m = null
size = 40
color = Color.Blue
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
assertNotNull(m)
assertSame(firstMeasurable, m)
}
// NodeCoordinators remain even when there are multiple for a modifier
@Test
fun replaceMultiImplementationModifier() {
var color by mutableStateOf(Color.Red)
var m: Measurable? = null
var layoutLatch = CountDownLatch(1)
class SpecialModifier : DrawModifier, LayoutModifier {
override fun ContentDrawScope.draw() {
drawContent()
drawLatch.countDown()
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
layoutLatch.countDown()
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
val layoutCaptureModifier = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
m = measurable
val p = measurable.measure(constraints)
return layout(p.width, p.height) {
p.place(0, 0)
}
}
}
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
30,
layoutCaptureModifier
.then(SpecialModifier())
.background(color)
) {}
}
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
var firstMeasurable = m
drawLatch = CountDownLatch(1)
layoutLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
m = null
color = Color.Blue
}
// The latches are triggered in the new instance
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
// The new instance's measurable is the same.
assertNotNull(m)
assertSame(firstMeasurable, m)
}
// When some content is drawn on the parent's layer through a modifier, when the modifier
// changes, it should invalidate the parent layer, not layer of the LayoutNode.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun invalidateParentLayer() {
var color by mutableStateOf(Color.Red)
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
size = 10,
modifier = Modifier.background(color = color).drawLatchModifier().then(
Modifier.padding(10)
.graphicsLayer()
.background(Color.White)
)
)
}
}
validateSquareColors(outerColor = Color.Red, innerColor = Color.White, size = 10)
drawLatch = CountDownLatch(1)
color = Color.Blue
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
}
// When zindex has changed, the parent should be invalidated, even if all drawing is done
// within a modifier layer.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun invalidateParentLayerZIndex() {
var zIndex by mutableStateOf(0f)
activityTestRule.runOnUiThread {
activity.setContent {
with(LocalDensity.current) {
FixedSize(
size = 30,
modifier = Modifier.background(color = Color.Blue).drawLatchModifier()
) {
FixedSize(
size = 10,
modifier = Modifier
.graphicsLayer()
.zIndex(zIndex)
.padding(10.toDp())
.background(Color.White)
)
FixedSize(
size = 10,
modifier = Modifier
.graphicsLayer()
.zIndex(0f)
.padding(10.toDp())
.background(Color.Yellow)
)
}
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Yellow, size = 10)
drawLatch = CountDownLatch(1)
zIndex = 1f
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
}
// Make sure that when the child of a layer changes that the drawing changes to match.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun changedLayerChild() {
var showInner by mutableStateOf(true)
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(
size = 10,
modifier = Modifier.background(Color.Blue)
.padding(10)
.graphicsLayer()
.then(if (showInner) Modifier.background(Color.White) else Modifier)
.drawLatchModifier()
)
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
drawLatch = CountDownLatch(1)
showInner = false
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Blue, size = 10)
}
@Test
fun remeasureOnParentDataChanged() {
var measuredLatch = CountDownLatch(1)
var size = 10
var sizeState by mutableStateOf(size)
class ParentInt(val x: Int) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? = x
}
activityTestRule.runOnUiThread {
activity.setContent {
Layout({ Box(ParentInt(sizeState)) }) { measurables, constraints ->
val boxSize = measurables[0].parentData as Int
assertEquals(size, boxSize)
val placeable = measurables[0].measure(constraints)
measuredLatch.countDown()
layout(boxSize, boxSize) {
placeable.place(0, 0)
}
}
}
}
assertTrue(measuredLatch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
size = 20
sizeState = 20
measuredLatch = CountDownLatch(1)
}
assertTrue(measuredLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun reattachingViewKeepsRootNodePlaced() {
lateinit var container1: FrameLayout
lateinit var container2: ComposeView
activityTestRule.runOnUiThread {
val activity = activityTestRule.activity
container1 = FrameLayout(activity)
container2 = ComposeView(activity)
activity.setContentView(container1)
container1.addView(container2)
container2.setContent {
FixedSize(10, Modifier.drawLatchModifier())
}
}
assertTrue(drawLatch.await(10000, TimeUnit.SECONDS))
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
container1.removeView(container2)
}
assertFalse(drawLatch.await(200, TimeUnit.MILLISECONDS))
activityTestRule.runOnUiThread {
container1.addView(container2)
}
// draw modifier will be redrawn if the root node is placed
assertTrue(drawLatch.await(10000, TimeUnit.SECONDS))
}
// When a LayoutNode is removed, but it contains a layout that is being updated, the
// layout should not be remeasured.
@Test
fun disappearingLayoutNode() {
var size by mutableStateOf(10f)
val notShownLatch = CountDownLatch(1)
val measureLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
activity.setContent {
Box(Modifier.background(Color.Red).drawLatchModifier()) {
var animatedSize by remember { mutableStateOf(size) }
animatedSize = animateFloatAsState(size).value
if (animatedSize == 10f) {
Layout(
modifier = Modifier.background(Color.Cyan),
content = {}
) { _, _ ->
if (animatedSize != 10f) {
measureLatch.countDown()
}
val sizePx = animatedSize.roundToInt()
layout(sizePx, sizePx) {}
}
} else {
notShownLatch.countDown()
}
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
drawLatch = CountDownLatch(1)
activityTestRule.runOnUiThread {
size = 20f
}
assertTrue(notShownLatch.await(1, TimeUnit.SECONDS))
assertFalse(measureLatch.await(200, TimeUnit.MILLISECONDS))
}
// Tests that we can draw a layout that isn't attached.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawDetachedLayoutNode() {
lateinit var view: ComposeView
activityTestRule.runOnUiThread {
view = ComposeView(activity)
view.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnLifecycleDestroyed(activity)
)
view.setContent {
with(LocalDensity.current) {
Box(
Modifier
.background(Color.Blue)
.requiredSize(30.toDp())
.padding(10.toDp())
.background(Color.White)
.drawLatchModifier()
)
}
}
activity.setContentView(
view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
)
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
val parent = view.parent as ViewGroup
parent.removeView(view)
}
activityTestRule.runOnUiThread {
val bitmap = Bitmap.createBitmap(30, 30, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
view.draw(canvas)
bitmap.assertRect(Color.Blue, holeSize = 10)
bitmap.assertRect(Color.White, size = 10)
}
}
// Tests that an invalidation on a detached view will draw correctly when attached.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawInvalidationInDetachedLayoutNode() {
lateinit var view: ComposeView
var innerColor by mutableStateOf(Color.White)
activityTestRule.runOnUiThread {
view = ComposeView(activity)
view.setContent {
with(LocalDensity.current) {
Box(
Modifier
.background(Color.Blue)
.requiredSize(30.toDp())
.padding(10.toDp())
.drawBehind {
drawRect(innerColor)
drawLatch.countDown()
}
)
}
}
activity.setContentView(
view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
)
}
validateSquareColors(Color.Blue, Color.White, size = 10)
drawLatch = CountDownLatch(1)
var parent: ViewGroup? = null
activityTestRule.runOnUiThread {
parent = view.parent as ViewGroup
parent!!.removeView(view)
}
activityTestRule.runOnUiThread {} // wait for detach
drawLatch = CountDownLatch(1)
innerColor = Color.Yellow
activityTestRule.runOnUiThread {
parent!!.addView(view)
}
validateSquareColors(Color.Blue, Color.Yellow, size = 10)
}
// Tests that a size invalidation on a detached view will remeasure correctly when attached.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun sizeInvalidationInDetachedLayoutNode() {
lateinit var view: ComposeView
var size by mutableStateOf(10.dp)
var layoutLatch = CountDownLatch(1)
var measuredSize = 0.dp
val sizeModifier = Modifier.layout { measurable, constraints ->
measuredSize = size
layoutLatch.countDown()
val pxSize = size.roundToPx()
layout(pxSize, pxSize) {
measurable.measure(constraints).place(0, 0)
}
}
activityTestRule.runOnUiThread {
view = ComposeView(activity)
view.setContent {
Box(
Modifier
.background(Color.Blue)
.then(sizeModifier)
)
}
activity.setContentView(view)
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(10.dp, measuredSize)
layoutLatch = CountDownLatch(1)
var parent: ViewGroup? = null
activityTestRule.runOnUiThread {
parent = view.parent as ViewGroup
parent!!.removeView(view)
}
activityTestRule.runOnUiThread {} // wait for detach
layoutLatch = CountDownLatch(1)
size = 30.dp
activityTestRule.runOnUiThread {
parent!!.addView(view)
}
assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
assertEquals(measuredSize, 30.dp)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun zeroSizedComposeViewCanDrawOutsideItsBounds() {
val padding = 10
val size = padding * 2
lateinit var frameLayout: FrameLayout
activityTestRule.runOnUiThread {
val composeView = ComposeView(activity)
composeView.setContent {
Box(
Modifier
.fillMaxSize()
.drawBehind {
val marginFloat = padding.toFloat()
drawRect(
color = Color.Red,
topLeft = Offset(-marginFloat, -marginFloat),
size = Size(marginFloat * 2, marginFloat * 2)
)
}
)
}
frameLayout = FrameLayout(activity)
frameLayout.clipToPadding = false
frameLayout.clipChildren = false
frameLayout.setPadding(padding, padding, padding, padding)
frameLayout.addView(composeView, ViewGroup.LayoutParams(0, 0))
activity.setContentView(
frameLayout,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
)
}
activityTestRule.waitAndScreenShot(frameLayout).asImageBitmap()
.assertPixels(expectedSize = IntSize(size, size)) {
Color.Red
}
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun layoutUsesPlaceWithLayer() {
val yellow = Color(0xFFFFFF00)
val red = Color(0xFF800000)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
AtLeastSize(
size = 10,
modifier = Modifier.drawBehind {
drawRect(red)
}
)
},
modifier = Modifier.drawBehind {
drawRect(yellow)
drawLatch.countDown()
}
) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(30, 30) {
placeable.placeWithLayer(10, 10)
}
}
}
}
validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun layoutUsesPlaceWithLayerWithScale() {
val yellow = Color(0xFFFFFF00)
val red = Color(0xFF800000)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
AtLeastSize(
size = 20,
modifier = Modifier.drawBehind {
drawRect(red)
}
)
},
modifier = Modifier.drawBehind {
drawRect(yellow)
drawLatch.countDown()
}
) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(30, 30) {
placeable.placeWithLayer(5, 5) {
scaleX = 0.5f
scaleY = 0.5f
}
}
}
}
}
validateSquareColors(outerColor = yellow, innerColor = red, size = 10)
}
@Test
fun layoutMovesPlacedWithLayerChild_noInvalidations() {
var parentInvalidationCount = 0
var childInvalidationCount = 0
var offset by mutableStateOf(0)
activityTestRule.runOnUiThreadIR {
activity.setContent {
Layout(
content = {
AtLeastSize(
size = 20,
modifier = Modifier.drawBehind {
childInvalidationCount++
}
)
},
modifier = Modifier.drawWithContent {
drawContent()
parentInvalidationCount++
drawLatch.countDown()
}
) { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(30, 30) {
placeable.placeWithLayer(offset, offset)
}
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
assertEquals(1, parentInvalidationCount)
assertEquals(1, childInvalidationCount)
drawLatch = CountDownLatch(1)
offset = 10
assertFalse(drawLatch.await(300, TimeUnit.MILLISECONDS))
assertEquals(1, parentInvalidationCount)
assertEquals(1, childInvalidationCount)
}
/**
* invalidateDescendants should invalidate all layout layers.
*/
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun invalidateDescendants() {
var color = Color.White
activityTestRule.runOnUiThread {
activity.setContent {
FixedSize(30, Modifier.background(Color.Blue)) {
FixedSize(30, Modifier.graphicsLayer()) {
with(LocalDensity.current) {
Canvas(Modifier.requiredSize(10.toDp())) {
drawRect(color)
drawLatch.countDown()
}
}
}
}
}
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
color = Color.Yellow
activityTestRule.runOnUiThread {
drawLatch = CountDownLatch(1)
val view = activityTestRule.findAndroidComposeView() as AndroidComposeView
view.invalidateDescendants()
}
validateSquareColors(outerColor = Color.Blue, innerColor = Color.Yellow, size = 10)
}
@Test
fun placeableMeasuredSize() = with(density) {
val realSize = 100.dp
val constrainedSize = 50.dp
val latch = CountDownLatch(1)
activityTestRule.runOnUiThread {
activity.setContent {
Layout(
content = {
Box(Modifier.requiredSize(realSize))
}
) { measurables, _ ->
val placeable = measurables[0].measure(
Constraints.fixed(constrainedSize.roundToPx(), constrainedSize.roundToPx())
)
assertEquals(realSize.roundToPx(), placeable.measuredWidth)
assertEquals(realSize.roundToPx(), placeable.measuredHeight)
assertEquals(constrainedSize.roundToPx(), placeable.width)
assertEquals(constrainedSize.roundToPx(), placeable.height)
latch.countDown()
layout(1, 1) { }
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
}
@Test
fun noRemeasureWhenWeStopUsingStateInMeasuring() = with(density) {
val counter = mutableStateOf(0)
var latch = CountDownLatch(1)
var parentRemeasures = 0
var measurePolicy = mutableStateOf(
MeasurePolicy { measurables, constraints ->
counter.value
parentRemeasures++
measurables.first().measure(constraints)
layout(1, 1) { }
}
)
activityTestRule.runOnUiThread {
activity.setContent {
Layout(
content = {
Layout(
content = {}
) { _, _ ->
counter.value
latch.countDown()
layout(1, 1) { }
}
},
measurePolicy = measurePolicy.value
)
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
assertEquals(1, parentRemeasures)
latch = CountDownLatch(1)
measurePolicy.value = MeasurePolicy { measurables, constraints ->
// not using counter anymore
parentRemeasures++
measurables.first().measure(constraints)
layout(1, 1) { }
}
assertTrue(latch.await(10000, TimeUnit.SECONDS))
assertEquals(2, parentRemeasures)
latch = CountDownLatch(1)
counter.value = 1
assertTrue(latch.await(10000, TimeUnit.SECONDS))
assertEquals(2, parentRemeasures)
}
@Test
fun updatingModifierIsNotCausingParentsRelayout() {
var parentLayoutsCount = 0
var latch = CountDownLatch(1)
var modifier by mutableStateOf(Modifier.layout(onLayout = { println("1") }))
val parentMeasurePolicy = MeasurePolicy { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
layout(placeable.width, placeable.height) {
parentLayoutsCount++
placeable.place(0, 0)
}
}
activityTestRule.runOnUiThread {
activity.setContent {
Layout(
content = {
Layout({}, modifier) { _, _ ->
layout(10, 10) {
latch.countDown()
}
}
},
measurePolicy = parentMeasurePolicy
)
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
latch = CountDownLatch(1)
activityTestRule.runOnUiThread {
assertEquals(1, parentLayoutsCount)
modifier = Modifier.layout(onLayout = { println("2") })
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
assertEquals(1, parentLayoutsCount)
}
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun drawnInCorrectLayer() {
var innerDrawLatch = CountDownLatch(1)
var outerDrawLatch = CountDownLatch(1)
var outerColor by mutableStateOf(Color.Blue)
var innerColor by mutableStateOf(Color.White)
activityTestRule.runOnUiThread {
activity.setContent {
with(LocalDensity.current) {
Box(Modifier.size(30.toDp())
.drawBehind {
drawRect(outerColor)
outerDrawLatch.countDown()
}
.drawLatchModifier()
.padding(10.toDp())
.clipToBounds()
.drawBehind {
// clipped by the layer
drawRect(innerColor, Offset(-10f, -10f), Size(30f, 30f))
innerDrawLatch.countDown()
}
.drawLatchModifier()
.size(10.toDp())
)
}
}
}
assertTrue(innerDrawLatch.await(1, TimeUnit.SECONDS))
assertTrue(outerDrawLatch.await(1, TimeUnit.SECONDS))
validateSquareColors(
outerColor = Color.Blue,
innerColor = Color.White,
size = 10
)
innerDrawLatch = CountDownLatch(1)
outerDrawLatch = CountDownLatch(1)
drawLatch = CountDownLatch(1)
// changing the inner color should only affect the inner layer
innerColor = Color.Yellow
assertTrue(innerDrawLatch.await(1, TimeUnit.SECONDS))
validateSquareColors(
outerColor = Color.Blue,
innerColor = Color.Yellow,
size = 10
)
assertEquals(1, outerDrawLatch.count)
innerDrawLatch = CountDownLatch(1)
drawLatch = CountDownLatch(1)
// changing the outer color should only affect the outer layer
outerColor = Color.Red
assertTrue(outerDrawLatch.await(1, TimeUnit.SECONDS))
validateSquareColors(
outerColor = Color.Red,
innerColor = Color.Yellow,
size = 10
)
assertEquals(1, innerDrawLatch.count)
}
/**
* Android Transitions should be possible with Compose Views. View layers can
* confuse the Android Transition system.
*/
@Test
fun worksWithTransitions() {
val frameLayout = FrameLayout(activity)
activityTestRule.runOnUiThread {
activity.setContentView(frameLayout)
val composeView = ComposeView(activity).apply {
setContent {
Box {}
}
}
frameLayout.addView(composeView)
}
activityTestRule.runOnUiThread {
TransitionManager.beginDelayedTransition(frameLayout)
frameLayout.removeAllViews()
val composeView = ComposeView(activity).apply {
setContent {
Box(Modifier.drawLatchModifier()) {}
}
}
frameLayout.addView(composeView)
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
}
@Test
fun attachingLayerDoesNotCauseRelayout() {
var latch = CountDownLatch(1)
lateinit var root: RequestLayoutTrackingFrameLayout
lateinit var composeView: ComposeView
var showLayer by mutableStateOf(false)
activityTestRule.runOnUiThread {
root = RequestLayoutTrackingFrameLayout(activity)
composeView = ComposeView(activity)
activity.setContentView(root)
root.addView(composeView)
composeView.setContent {
val modifier = if (showLayer) Modifier.graphicsLayer() else Modifier
Box(Modifier.drawBehind { latch.countDown() }.then(modifier))
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
Truth.assertThat(root.requestLayoutCalled).isTrue()
latch = CountDownLatch(1)
root.requestLayoutCalled = false
showLayer = true
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
Truth.assertThat(root.requestLayoutCalled).isFalse()
}
}
private fun Modifier.layout(onLayout: () -> Unit) = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
onLayout()
placeable.place(0, 0)
}
}
private fun composeSquares(model: SquareModel) {
activityTestRule.runOnUiThreadIR {
activity.setContent {
Padding(
size = model.size,
modifier = Modifier.drawBehind {
drawRect(model.outerColor)
}
) {
AtLeastSize(
size = model.size,
modifier = Modifier.drawBehind {
drawLatch.countDown()
drawRect(model.innerColor)
}
)
}
}
}
}
private fun composeSquaresWithNestedRepaintBoundaries(model: SquareModel) {
activityTestRule.runOnUiThreadIR {
activity.setContent {
Padding(
size = model.size,
modifier = Modifier.fillColor(model, isInner = false, doCountDown = false)
.graphicsLayer()
) {
AtLeastSize(
size = model.size,
modifier = Modifier.graphicsLayer().fillColor(model, isInner = true)
) {
}
}
}
}
}
private fun composeMovingSquaresWithRepaintBoundary(model: SquareModel, offset: State<Int>) {
activityTestRule.runOnUiThreadIR {
activity.setContent {
Position(
size = model.size * 3,
offset = offset,
modifier = Modifier.fillColor(model, isInner = false, doCountDown = false)
) {
AtLeastSize(
size = model.size,
modifier = Modifier.graphicsLayer().fillColor(model, isInner = true)
) {
}
}
}
}
}
private fun composeMovingSquares(model: SquareModel, offset: State<Int>) {
activityTestRule.runOnUiThreadIR {
activity.setContent {
Position(
size = model.size * 3,
offset = offset,
modifier = Modifier.fillColor(model, isInner = false, doCountDown = false)
) {
AtLeastSize(
size = model.size,
modifier = Modifier.fillColor(model, isInner = true)
) {
}
}
}
}
}
private fun composeNestedSquares(model: SquareModel) {
activityTestRule.runOnUiThreadIR {
activity.setContent {
val fillColorModifier = Modifier.drawBehind {
drawRect(model.innerColor)
drawLatch.countDown()
}
val innerDrawWithContentModifier = Modifier.drawWithContent {
drawRect(model.outerColor)
val start = model.size.toFloat()
val end = start * 2
clipRect(start, start, end, end) {
this@drawWithContent.drawContent()
}
}
AtLeastSize(size = (model.size * 3), modifier = innerDrawWithContentModifier) {
AtLeastSize(size = (model.size * 3), modifier = fillColorModifier)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun validateSquareColors(
outerColor: Color,
innerColor: Color,
size: Int,
offset: Int = 0,
totalSize: Int = size * 3
) {
activityTestRule.validateSquareColors(
drawLatch,
outerColor,
innerColor,
size,
offset,
totalSize
)
}
private fun Modifier.fillColor(color: Color, doCountDown: Boolean = true): Modifier =
drawBehind {
drawRect(color)
if (doCountDown) {
drawLatch.countDown()
}
}
private fun Modifier.fillColor(
squareModel: SquareModel,
isInner: Boolean,
doCountDown: Boolean = true
): Modifier = drawBehind {
drawRect(if (isInner) squareModel.innerColor else squareModel.outerColor)
if (doCountDown) {
drawLatch.countDown()
}
}
private var positionLatch: CountDownLatch? = null
@Composable
fun Position(
size: Int,
offset: State<Int>,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map { m ->
m.measure(constraints)
}
layout(size, size) {
placeables.forEach { child ->
child.place(offset.value, offset.value)
}
positionLatch?.countDown()
}
}
}
fun Modifier.drawLatchModifier() = drawBehind { drawLatch.countDown() }
}
fun Bitmap.assertRect(
color: Color,
holeSize: Int = 0,
size: Int = width,
centerX: Int = width / 2,
centerY: Int = height / 2
) {
assertTrue(centerX + size / 2 <= width)
assertTrue(centerX - size / 2 >= 0)
assertTrue(centerY + size / 2 <= height)
assertTrue(centerY - size / 2 >= 0)
val halfHoleSize = holeSize / 2
for (x in centerX - size / 2 until centerX + size / 2) {
for (y in centerY - size / 2 until centerY + size / 2) {
if (abs(x - centerX) > halfHoleSize &&
abs(y - centerY) > halfHoleSize
) {
val currentColor = Color(getPixel(x, y))
assertColorsEqual(color, currentColor)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Suppress("DEPRECATION")
fun androidx.test.rule.ActivityTestRule<*>.validateSquareColors(
drawLatch: CountDownLatch,
outerColor: Color,
innerColor: Color,
size: Int,
offset: Int = 0,
totalSize: Int = size * 3
) {
assertTrue("drawLatch timed out", drawLatch.await(1, TimeUnit.SECONDS))
val bitmap = waitAndScreenShot()
assertEquals(totalSize, bitmap.width)
assertEquals(totalSize, bitmap.height)
val squareStart = (totalSize - size) / 2 + offset
val squareEnd = totalSize - ((totalSize - size) / 2) + offset
for (x in 0 until totalSize) {
for (y in 0 until totalSize) {
val pixel = Color(bitmap.getPixel(x, y))
val expected =
if (!(x < squareStart || x >= squareEnd || y < squareStart || y >= squareEnd)) {
innerColor
} else {
outerColor
}
assertColorsEqual(expected, pixel) {
"Pixel within drawn rect[$x, $y] is $expected, but was $pixel"
}
}
}
}
fun assertColorsEqual(
expected: Color,
color: Color,
error: () -> String = { "$expected and $color are not similar!" }
) {
val errorString = error()
assertEquals(errorString, expected.red, color.red, 0.05f)
assertEquals(errorString, expected.green, color.green, 0.05f)
assertEquals(errorString, expected.blue, color.blue, 0.05f)
assertEquals(errorString, expected.alpha, color.alpha, 0.05f)
}
@Composable
fun AtLeastSize(
size: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {}
) {
Layout(
measurePolicy = { measurables, constraints ->
val newConstraints = Constraints(
minWidth = max(size, constraints.minWidth),
maxWidth = if (constraints.hasBoundedWidth) {
max(size, constraints.maxWidth)
} else {
Constraints.Infinity
},
minHeight = max(size, constraints.minHeight),
maxHeight = if (constraints.hasBoundedHeight) {
max(size, constraints.maxHeight)
} else {
Constraints.Infinity
}
)
val placeables = measurables.map { m ->
m.measure(newConstraints)
}
var maxWidth = size
var maxHeight = size
placeables.forEach { child ->
maxHeight = max(child.height, maxHeight)
maxWidth = max(child.width, maxWidth)
}
layout(maxWidth, maxHeight) {
placeables.forEach { child ->
child.place(0, 0)
}
}
},
modifier = modifier,
content = content
)
}
@Composable
fun FixedSize(
size: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {}
) {
Layout(content = content, modifier = modifier) { measurables, _ ->
val newConstraints = Constraints.fixed(size, size)
val placeables = measurables.map { m ->
m.measure(newConstraints)
}
layout(size, size) {
placeables.forEach { child ->
child.placeRelative(0, 0)
}
}
}
}
@Composable
fun Align(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(
modifier = modifier,
measurePolicy = { measurables, constraints ->
val newConstraints = Constraints(
minWidth = 0,
maxWidth = constraints.maxWidth,
minHeight = 0,
maxHeight = constraints.maxHeight
)
val placeables = measurables.map { m ->
m.measure(newConstraints)
}
var maxWidth = constraints.minWidth
var maxHeight = constraints.minHeight
placeables.forEach { child ->
maxHeight = max(child.height, maxHeight)
maxWidth = max(child.width, maxWidth)
}
layout(maxWidth, maxHeight) {
placeables.forEach { child ->
child.placeRelative(0, 0)
}
}
},
content = content
)
}
@Composable
internal fun Padding(
size: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
measurePolicy = { measurables, constraints ->
val totalDiff = size * 2
val targetMinWidth = constraints.minWidth - totalDiff
val targetMaxWidth = if (constraints.hasBoundedWidth) {
constraints.maxWidth - totalDiff
} else {
Constraints.Infinity
}
val targetMinHeight = constraints.minHeight - totalDiff
val targetMaxHeight = if (constraints.hasBoundedHeight) {
constraints.maxHeight - totalDiff
} else {
Constraints.Infinity
}
val newConstraints = Constraints(
minWidth = targetMinWidth.coerceAtLeast(0),
maxWidth = targetMaxWidth.coerceAtLeast(0),
minHeight = targetMinHeight.coerceAtLeast(0),
maxHeight = targetMaxHeight.coerceAtLeast(0)
)
val placeables = measurables.map { m ->
m.measure(newConstraints)
}
var maxWidth = size
var maxHeight = size
placeables.forEach { child ->
maxHeight = max(child.height + totalDiff, maxHeight)
maxWidth = max(child.width + totalDiff, maxWidth)
}
layout(maxWidth, maxHeight) {
placeables.forEach { child ->
child.placeRelative(size, size)
}
}
},
content = content
)
}
@Composable
fun Wrap(
modifier: Modifier = Modifier,
minWidth: Int = 0,
minHeight: Int = 0,
content: @Composable () -> Unit = {}
) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = max(placeables.maxByOrNull { it.width }?.width ?: 0, minWidth)
val height = max(placeables.maxByOrNull { it.height }?.height ?: 0, minHeight)
layout(width, height) {
placeables.forEach { it.placeRelative(0, 0) }
}
}
}
@Composable
fun Scroller(
modifier: Modifier = Modifier,
onScrollPositionChanged: (position: Int, maxPosition: Int) -> Unit,
offset: State<Int>,
content: @Composable () -> Unit
) {
val maxPosition = remember { mutableStateOf(Constraints.Infinity) }
ScrollerLayout(
modifier = modifier,
maxPosition = maxPosition.value,
onMaxPositionChanged = {
maxPosition.value = 0
onScrollPositionChanged(offset.value, 0)
},
content = content
)
}
@Composable
private fun ScrollerLayout(
modifier: Modifier = Modifier,
@Suppress("UNUSED_PARAMETER") maxPosition: Int,
onMaxPositionChanged: () -> Unit,
content: @Composable () -> Unit
) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val childConstraints = constraints.copy(
maxHeight = constraints.maxHeight,
maxWidth = Constraints.Infinity
)
val childMeasurable = measurables.first()
val placeable = childMeasurable.measure(childConstraints)
val width = min(placeable.width, constraints.maxWidth)
layout(width, placeable.height) {
onMaxPositionChanged()
placeable.placeRelative(0, 0)
}
}
}
@Composable
fun WrapForceRelayout(
model: State<Int>,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxByOrNull { it.width }?.width ?: 0
val height = placeables.maxByOrNull { it.height }?.height ?: 0
layout(width, height) {
model.value
placeables.forEach { it.placeRelative(0, 0) }
}
}
}
@Composable
fun SimpleRow(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
var width = 0
var height = 0
val placeables = measurables.map {
it.measure(constraints.copy(maxWidth = constraints.maxWidth - width)).also {
width += it.width
height = max(height, it.height)
}
}
layout(width, height) {
var currentWidth = 0
placeables.forEach {
it.placeRelative(currentWidth, 0)
currentWidth += it.width
}
}
}
}
@Composable
fun JustConstraints(modifier: Modifier, content: @Composable () -> Unit) {
Layout(content, modifier) { _, constraints ->
layout(constraints.minWidth, constraints.minHeight) {}
}
}
class DrawCounterListener(private val view: View) :
ViewTreeObserver.OnPreDrawListener {
val latch = CountDownLatch(5)
override fun onPreDraw(): Boolean {
latch.countDown()
if (latch.count > 0) {
view.postInvalidate()
} else {
view.viewTreeObserver.removeOnPreDrawListener(this)
}
return true
}
}
fun Modifier.padding(padding: Int) = this.then(PaddingModifier(padding, padding, padding, padding))
private data class PaddingModifier(
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(
constraints.offset(
horizontal = -left - right,
vertical = -top - bottom
)
)
return layout(
constraints.constrainWidth(left + placeable.width + right),
constraints.constrainHeight(top + placeable.height + bottom)
) {
placeable.placeRelative(left, top)
}
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
): Int = measurable.minIntrinsicWidth((height - (top + bottom)).coerceAtLeast(0)) +
(left + right)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
): Int = measurable.maxIntrinsicWidth((height - (top + bottom)).coerceAtLeast(0)) +
(left + right)
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
): Int = measurable.minIntrinsicHeight((width - (left + right)).coerceAtLeast(0)) +
(top + bottom)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
): Int = measurable.maxIntrinsicHeight((width - (left + right)).coerceAtLeast(0)) +
(top + bottom)
}
internal val AlignTopLeft = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
return layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelative(0, 0)
}
}
}
@Stable
class SquareModel(
size: Int = 10,
outerColor: Color = Color(0xFF000080),
innerColor: Color = Color(0xFFFFFFFF)
) {
var size: Int by mutableStateOf(size)
var outerColor: Color by mutableStateOf(outerColor)
var innerColor: Color by mutableStateOf(innerColor)
}
@Suppress("DEPRECATION")
// We only need this because IR compiler doesn't like converting lambdas to Runnables
fun androidx.test.rule.ActivityTestRule<*>.runOnUiThreadIR(block: () -> Unit) {
val runnable: Runnable = object : Runnable {
override fun run() {
block()
}
}
runOnUiThread(runnable)
}
@Suppress("DEPRECATION")
fun androidx.test.rule.ActivityTestRule<*>.findAndroidComposeView(): ViewGroup {
val contentViewGroup = activity.findViewById<ViewGroup>(android.R.id.content)
return findAndroidComposeView(contentViewGroup)!!
}
fun findAndroidComposeView(parent: ViewGroup): ViewGroup? {
for (index in 0 until parent.childCount) {
val child = parent.getChildAt(index)
if (child is ViewGroup) {
if (child is Owner)
return child
else {
val composeView = findAndroidComposeView(child)
if (composeView != null) {
return composeView
}
}
}
}
return null
}
@Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.O)
fun androidx.test.rule.ActivityTestRule<*>.waitAndScreenShot(
forceInvalidate: Boolean = true
): Bitmap = waitAndScreenShot(findAndroidComposeView(), forceInvalidate)
@Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.O)
fun androidx.test.rule.ActivityTestRule<*>.waitAndScreenShot(
view: View,
forceInvalidate: Boolean = true
): Bitmap {
val flushListener = DrawCounterListener(view)
val offset = intArrayOf(0, 0)
var handler: Handler? = null
runOnUiThread {
view.getLocationInWindow(offset)
if (forceInvalidate) {
view.viewTreeObserver.addOnPreDrawListener(flushListener)
view.invalidate()
}
handler = Handler(Looper.getMainLooper())
}
if (forceInvalidate) {
assertTrue("Drawing latch timed out", flushListener.latch.await(1, TimeUnit.SECONDS))
}
val width = view.width
val height = view.height
val dest =
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val srcRect = android.graphics.Rect(0, 0, width, height)
srcRect.offset(offset[0], offset[1])
val latch = CountDownLatch(1)
var copyResult = 0
val onCopyFinished = object : PixelCopy.OnPixelCopyFinishedListener {
override fun onPixelCopyFinished(result: Int) {
copyResult = result
latch.countDown()
}
}
PixelCopy.request(activity.window, srcRect, dest, onCopyFinished, handler!!)
assertTrue("Pixel copy latch timed out", latch.await(1, TimeUnit.SECONDS))
assertEquals(PixelCopy.SUCCESS, copyResult)
return dest
}
fun Modifier.background(color: Color) = drawBehind {
drawRect(color)
}
fun Modifier.background(model: SquareModel, isInner: Boolean) = drawBehind {
drawRect(if (isInner) model.innerColor else model.outerColor)
}
class LayoutAndDrawModifier(val color: Color) : LayoutModifier, DrawModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(Constraints.fixed(10, 10))
return layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelative(
(constraints.maxWidth - placeable.width) / 2,
(constraints.maxHeight - placeable.height) / 2
)
}
}
override fun ContentDrawScope.draw() {
drawRect(color)
}
}
fun Modifier.scale(scale: Float) = then(LayoutScale(scale))
.graphicsLayer(scaleX = scale, scaleY = scale)
class LayoutScale(val scale: Float) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(
Constraints(
minWidth = (constraints.minWidth / scale).roundToInt(),
minHeight = (constraints.minHeight / scale).roundToInt(),
maxWidth = (constraints.maxWidth / scale).roundToInt(),
maxHeight = (constraints.maxHeight / scale).roundToInt()
)
)
return layout(
(placeable.width * scale).roundToInt(),
(placeable.height * scale).roundToInt()
) {
placeable.placeRelative(0, 0)
}
}
}
fun Modifier.latch(countDownLatch: CountDownLatch) = drawBehind {
countDownLatch.countDown()
}
private class RequestLayoutTrackingFrameLayout(context: Context) : FrameLayout(context) {
var requestLayoutCalled = false
override fun requestLayout() {
super.requestLayout()
requestLayoutCalled = true
}
}