[go: nahoru, domu]

blob: 481bb20a68a2b48eb1bd30fe04f5aef5cdd272d4 [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.
*/
package androidx.compose.foundation.layout
import androidx.compose.Composable
import androidx.test.filters.SmallTest
import androidx.ui.core.Alignment
import androidx.ui.core.Constraints
import androidx.ui.core.Layout
import androidx.ui.core.LayoutCoordinates
import androidx.ui.core.Modifier
import androidx.ui.core.Ref
import androidx.ui.core.onChildPositioned
import androidx.ui.core.onPositioned
import androidx.ui.core.positionInRoot
import androidx.compose.ui.geometry.Offset
import androidx.ui.unit.Dp
import androidx.ui.unit.IntSize
import androidx.ui.unit.dp
import androidx.ui.unit.toSize
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@SmallTest
@RunWith(JUnit4::class)
class LayoutPaddingTest : LayoutTest() {
/**
* Tests that negative start padding is not allowed.
*/
@Test(expected = IllegalArgumentException::class)
fun negativeStartPadding_throws() {
Modifier.padding(start = -1f.dp)
}
/**
* Tests that negative top padding is not allowed.
*/
@Test(expected = IllegalArgumentException::class)
fun negativeTopPadding_throws() {
Modifier.padding(top = -1f.dp)
}
/**
* Tests that negative end padding is not allowed.
*/
@Test(expected = IllegalArgumentException::class)
fun negativeEndPadding_throws() {
Modifier.padding(end = -1f.dp)
}
/**
* Tests that negative bottom padding is not allowed.
*/
@Test(expected = IllegalArgumentException::class)
fun negativeBottomPadding_throws() {
Modifier.padding(bottom = -1f.dp)
}
/**
* Tests that the [padding]-all and [padding] factories return equivalent modifiers.
*/
@Test
fun allEqualToAbsoluteWithExplicitSides() {
Assert.assertEquals(
Modifier.padding(10.dp, 10.dp, 10.dp, 10.dp),
Modifier.padding(10.dp)
)
}
/**
* Tests that the symmetrical-[padding] and [padding] factories return equivalent modifiers.
*/
@Test
fun symmetricEqualToAbsoluteWithExplicitSides() {
Assert.assertEquals(
Modifier.padding(10.dp, 20.dp, 10.dp, 20.dp),
Modifier.padding(10.dp, 20.dp)
)
}
/**
* Tests the top-level [padding] modifier factory with a single "all sides" argument,
* checking that a uniform padding of all sides is applied to a child when plenty of space is
* available for both content and padding.
*/
@Test
fun paddingAllAppliedToChild() = with(density) {
val padding = 10.dp
testPaddingIsAppliedImplementation(padding) { child: @Composable () -> Unit ->
TestBox(modifier = Modifier.padding(padding), body = child)
}
}
/**
* Tests the top-level [padding] modifier factory with a single [androidx.compose.foundation.layout
* .InnerPadding] argument, checking that padding is applied to a child when plenty of space
* is available for both content and padding.
*/
@Test
fun paddingInnerPaddingAppliedToChild() = with(density) {
val padding = InnerPadding(start = 1.dp, top = 3.dp, end = 6.dp, bottom = 10.dp)
testPaddingWithDifferentInsetsImplementation(
padding.start, padding.top, padding.end, padding.bottom
) { child: @Composable () -> Unit ->
TestBox(modifier = Modifier.padding(padding), body = child)
}
}
/**
* Tests the top-level [absolutePadding] modifier factory with different values for left, top,
* right and bottom paddings, checking that this padding is applied as expected when plenty of
* space is available for both the content and padding.
*/
@Test
fun absolutePaddingAppliedToChild() {
val paddingLeft = 10.dp
val paddingTop = 15.dp
val paddingRight = 20.dp
val paddingBottom = 30.dp
val padding = Modifier.absolutePadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
testPaddingWithDifferentInsetsImplementation(
paddingLeft,
paddingTop,
paddingRight,
paddingBottom
) { child: @Composable () -> Unit ->
TestBox(modifier = padding, body = child)
}
}
/**
* Tests the result of the [padding] modifier factory when not enough space is
* available to accommodate both the padding and the content. In this case, the padding
* should still be applied, modifying the final position of the content by its left and top
* paddings even if it would result in constraints that the child content is unable or
* unwilling to satisfy.
*/
@Test
fun insufficientSpaceAvailable() = with(density) {
val padding = 30.dp
testPaddingWithInsufficientSpaceImplementation(padding) { child: @Composable () -> Unit ->
TestBox(modifier = Modifier.padding(padding), body = child)
}
}
@Test
fun intrinsicMeasurements() = with(density) {
val padding = 100.toDp()
val latch = CountDownLatch(1)
var error: Throwable? = null
testIntrinsics(@Composable {
TestBox(modifier = Modifier.padding(padding)) {
Container(Modifier.aspectRatio(2f)) { }
}
}) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
// Spacing is applied on both sides of an axis
val totalAxisSpacing = (padding * 2).toIntPx()
// When the width/height is measured as 3 x the padding
val testDimension = (padding * 3).toIntPx()
// The actual dimension for the AspectRatio will be: test dimension - total padding
val actualAspectRatioDimension = testDimension - totalAxisSpacing
// When we measure the width first, the height will be half
val expectedAspectRatioHeight = (actualAspectRatioDimension / 2f).roundToInt()
// When we measure the height first, the width will be double
val expectedAspectRatioWidth = actualAspectRatioDimension * 2
// Add back the padding on both sides to get the total expected height
val expectedTotalHeight = expectedAspectRatioHeight + totalAxisSpacing
// Add back the padding on both sides to get the total expected height
val expectedTotalWidth = expectedAspectRatioWidth + totalAxisSpacing
try {
// Min width.
assertEquals(totalAxisSpacing, minIntrinsicWidth(0.dp.toIntPx()))
assertEquals(expectedTotalWidth, minIntrinsicWidth(testDimension))
assertEquals(totalAxisSpacing, minIntrinsicWidth(Constraints.Infinity))
// Min height.
assertEquals(totalAxisSpacing, minIntrinsicHeight(0.dp.toIntPx()))
assertEquals(expectedTotalHeight, minIntrinsicHeight(testDimension))
assertEquals(totalAxisSpacing, minIntrinsicHeight(Constraints.Infinity))
// Max width.
assertEquals(totalAxisSpacing, maxIntrinsicWidth(0.dp.toIntPx()))
assertEquals(expectedTotalWidth, maxIntrinsicWidth(testDimension))
assertEquals(totalAxisSpacing, maxIntrinsicWidth(Constraints.Infinity))
// Max height.
assertEquals(totalAxisSpacing, maxIntrinsicHeight(0.dp.toIntPx()))
assertEquals(expectedTotalHeight, maxIntrinsicHeight(testDimension))
assertEquals(totalAxisSpacing, maxIntrinsicHeight(Constraints.Infinity))
} catch (t: Throwable) {
error = t
} finally {
latch.countDown()
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
error?.let { throw it }
Unit
}
@Test
fun testRtlSupport() = with(density) {
val sizeDp = 100.toDp()
val size = sizeDp.toIntPx()
val padding1Dp = 5.dp
val padding2Dp = 10.dp
val padding3Dp = 15.dp
val padding1 = padding1Dp.toIntPx()
val padding2 = padding2Dp.toIntPx()
val padding3 = padding3Dp.toIntPx()
val drawLatch = CountDownLatch(3)
val childSize = Array(3) { IntSize(0, 0) }
val childPosition = Array(3) { Offset(0f, 0f) }
// ltr: P1 S P2 | S P3 | P1 S
// rtl: S P1 | P3 S | P2 S P1
show {
Row(Modifier.fillMaxSize().rtl) {
Stack(
Modifier.padding(start = padding1Dp, end = padding2Dp)
.preferredSize(sizeDp, sizeDp)
.onPositioned { coordinates: LayoutCoordinates ->
childSize[0] = coordinates.size
childPosition[0] = coordinates.positionInRoot
drawLatch.countDown()
}
) {
}
Stack(
Modifier.padding(end = padding3Dp)
.preferredSize(sizeDp, sizeDp)
.onPositioned { coordinates: LayoutCoordinates ->
childSize[1] = coordinates.size
childPosition[1] = coordinates.positionInRoot
drawLatch.countDown()
}
) {
}
Stack(
Modifier.padding(start = padding1Dp)
.preferredSize(sizeDp, sizeDp)
.onPositioned { coordinates: LayoutCoordinates ->
childSize[2] = coordinates.size
childPosition[2] = coordinates.positionInRoot
drawLatch.countDown()
}
) {
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
val root = findOwnerView()
waitForDraw(root)
val rootWidth = root.width
// S P1 | P3 S | P2 S P1
assertEquals(Offset((rootWidth - padding1 - size).toFloat(), 0f), childPosition[0])
assertEquals(IntSize(size, size), childSize[0])
assertEquals(
Offset((rootWidth - padding1 - padding2 - size * 2).toFloat(), 0f),
childPosition[1]
)
assertEquals(IntSize(size, size), childSize[1])
assertEquals(
Offset((rootWidth - size * 3 - padding1 * 2 - padding2 - padding3).toFloat(), 0f),
childPosition[2]
)
assertEquals(IntSize(size, size), childSize[2])
}
@Test
fun testPaddingRtl_whenBetweenLayoutDirectionModifiers() = with(density) {
val padding = 50
val size = 300
val paddingDp = padding.toDp()
val latch = CountDownLatch(1)
val resultPosition = Ref<Offset>()
val resultSize = Ref<IntSize>()
show {
Column(Modifier.fillMaxSize().rtl) {
Stack(Modifier
.preferredSize(size.toDp())
.ltr
.padding(start = paddingDp)
.rtl
.onChildPositioned {
resultPosition.value = it.positionInRoot
resultSize.value = it.size
latch.countDown()
}
) {
Stack(Modifier.fillMaxSize()) {}
}
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
val root = findOwnerView()
waitForDraw(root)
val rootWidth = root.width
assertEquals(
IntSize(size - padding, size).toSize(),
resultSize.value?.toSize()
)
assertEquals(
Offset((rootWidth - size + padding).toFloat(), 0f),
resultPosition.value
)
}
private fun testPaddingIsAppliedImplementation(
padding: Dp,
paddingContainer: @Composable (@Composable () -> Unit) -> Unit
) = with(density) {
val sizeDp = 50.dp
val size = sizeDp.toIntPx()
val paddingPx = padding.toIntPx()
val drawLatch = CountDownLatch(1)
var childSize = IntSize(-1, -1)
var childPosition = Offset(-1f, -1f)
show {
Stack(Modifier.fillMaxSize()) {
ConstrainedBox(
constraints = DpConstraints.fixed(sizeDp, sizeDp),
modifier = Modifier.gravity(Alignment.Center)
) {
val children = @Composable {
Container(Modifier.onPositioned { coordinates: LayoutCoordinates ->
childSize = coordinates.size
childPosition = coordinates.positionInRoot
drawLatch.countDown()
}) {
}
}
paddingContainer(children)
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
val root = findOwnerView()
waitForDraw(root)
val innerSize = (size - paddingPx * 2)
assertEquals(IntSize(innerSize, innerSize), childSize)
val left = ((root.width - size) / 2f).roundToInt() + paddingPx
val top = ((root.height - size) / 2f).roundToInt() + paddingPx
assertEquals(
Offset(left.toFloat(), top.toFloat()),
childPosition
)
}
private fun testPaddingWithDifferentInsetsImplementation(
left: Dp,
top: Dp,
right: Dp,
bottom: Dp,
paddingContainer: @Composable ((@Composable () -> Unit) -> Unit)
) = with(density) {
val sizeDp = 50.dp
val size = sizeDp.toIntPx()
val drawLatch = CountDownLatch(1)
var childSize = IntSize(-1, -1)
var childPosition = Offset(-1f, -1f)
show {
Stack(Modifier.fillMaxSize()) {
ConstrainedBox(
constraints = DpConstraints.fixed(sizeDp, sizeDp),
modifier = Modifier.gravity(Alignment.Center)
) {
val children = @Composable {
Container(Modifier.onPositioned { coordinates: LayoutCoordinates ->
childSize = coordinates.size
childPosition = coordinates.positionInRoot
drawLatch.countDown()
}) {
}
}
paddingContainer(children)
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
val root = findOwnerView()
waitForDraw(root)
val paddingLeft = left.toIntPx()
val paddingRight = right.toIntPx()
val paddingTop = top.toIntPx()
val paddingBottom = bottom.toIntPx()
assertEquals(
IntSize(
size - paddingLeft - paddingRight,
size - paddingTop - paddingBottom
),
childSize
)
val viewLeft = ((root.width - size) / 2f).roundToInt() + paddingLeft
val viewTop = ((root.height - size) / 2f).roundToInt() + paddingTop
assertEquals(
Offset(viewLeft.toFloat(), viewTop.toFloat()),
childPosition
)
}
private fun testPaddingWithInsufficientSpaceImplementation(
padding: Dp,
paddingContainer: @Composable (@Composable () -> Unit) -> Unit
) = with(density) {
val sizeDp = 50.dp
val size = sizeDp.toIntPx()
val paddingPx = padding.toIntPx()
val drawLatch = CountDownLatch(1)
var childSize = IntSize(-1, -1)
var childPosition = Offset(-1f, -1f)
show {
Stack(Modifier.fillMaxSize()) {
ConstrainedBox(
constraints = DpConstraints.fixed(sizeDp, sizeDp),
modifier = Modifier.gravity(Alignment.Center)
) {
paddingContainer {
Container(Modifier.onPositioned { coordinates: LayoutCoordinates ->
childSize = coordinates.size
childPosition = coordinates.positionInRoot
drawLatch.countDown()
}) {
}
}
}
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
val root = findOwnerView()
waitForDraw(root)
assertEquals(IntSize(0, 0), childSize)
val left = ((root.width - size) / 2f).roundToInt() + paddingPx
val top = ((root.height - size) / 2f).roundToInt() + paddingPx
assertEquals(Offset(left.toFloat(), top.toFloat()), childPosition)
}
/**
* A trivial layout that applies a [Modifier] and measures/lays out a single child
* with the same constraints it received.
*/
@Composable
private fun TestBox(modifier: Modifier = Modifier, body: @Composable () -> Unit) {
Layout(children = body, modifier = modifier) { measurables, constraints ->
require(measurables.size == 1) {
"TestBox received ${measurables.size} children; must have exactly 1"
}
val placeable = measurables.first().measure(constraints)
layout(
placeable.width.coerceAtMost(constraints.maxWidth),
placeable.height.coerceAtMost(constraints.maxHeight)
) {
placeable.place(0, 0)
}
}
}
}