Add aspect ratio modifier
Test: gradlew buildOnServer. Unit tests. Rewrote all usages of the AspectRatio
composable with the modifier in samples/unit tests (separate CL for
that) and verified.
Change-Id: I9e607b553f3a8ccde7af4686f2466b53603305d4
diff --git a/ui/ui-layout/src/androidTest/java/androidx/ui/layout/test/AspectRatioModifierTest.kt b/ui/ui-layout/src/androidTest/java/androidx/ui/layout/test/AspectRatioModifierTest.kt
new file mode 100644
index 0000000..d7a3575
--- /dev/null
+++ b/ui/ui-layout/src/androidTest/java/androidx/ui/layout/test/AspectRatioModifierTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.ui.layout.test
+
+import androidx.compose.Children
+import androidx.compose.composer
+import androidx.test.filters.SmallTest
+import androidx.ui.core.Constraints
+import androidx.ui.core.IntPx
+import androidx.ui.core.Layout
+import androidx.ui.core.PxPosition
+import androidx.ui.core.PxSize
+import androidx.ui.core.Ref
+import androidx.ui.core.dp
+import androidx.ui.core.ipx
+import androidx.ui.core.px
+import androidx.ui.core.withDensity
+import androidx.ui.layout.Container
+import androidx.ui.layout.AspectRatio
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.lang.IllegalArgumentException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@SmallTest
+@RunWith(JUnit4::class)
+class AspectRatioModifierTest : LayoutTest() {
+ @Test
+ fun testAspectRatioModifier_intrinsicDimensions() = withDensity(density) {
+ testIntrinsics(@Children {
+ Container(modifier = AspectRatio(2f), width = 30.dp, height = 40.dp) { }
+ }) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
+ assertEquals(40.ipx, minIntrinsicWidth(20.ipx))
+ assertEquals(40.ipx, maxIntrinsicWidth(20.ipx))
+ assertEquals(20.ipx, minIntrinsicHeight(40.ipx))
+ assertEquals(20.ipx, maxIntrinsicHeight(40.ipx))
+
+ assertEquals(30.dp.toIntPx(), minIntrinsicWidth(IntPx.Infinity))
+ assertEquals(30.dp.toIntPx(), maxIntrinsicWidth(IntPx.Infinity))
+ assertEquals(40.dp.toIntPx(), minIntrinsicHeight(IntPx.Infinity))
+ assertEquals(40.dp.toIntPx(), maxIntrinsicHeight(IntPx.Infinity))
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testAspectRatioModifier_zeroRatio() {
+ show {
+ Container(AspectRatio(0f)) { }
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testAspectRatioModifier_negativeRatio() {
+ show {
+ Container(AspectRatio(-2f)) { }
+ }
+ }
+
+ @Test
+ fun testAspectRatio_sizesCorrectly() {
+ assertEquals(PxSize(30.px, 30.px), getSize(1f, Constraints(maxWidth = 30.ipx)))
+ assertEquals(PxSize(30.px, 15.px), getSize(2f, Constraints(maxWidth = 30.ipx)))
+ assertEquals(
+ PxSize(10.px, 10.px),
+ getSize(1f, Constraints(maxWidth = 30.ipx, maxHeight = 10.ipx))
+ )
+ assertEquals(
+ PxSize(20.px, 10.px),
+ getSize(2f, Constraints(maxWidth = 30.ipx, maxHeight = 10.ipx))
+ )
+ assertEquals(
+ PxSize(10.px, 5.px),
+ getSize(2f, Constraints(minWidth = 10.ipx, minHeight = 5.ipx))
+ )
+ assertEquals(
+ PxSize(20.px, 10.px),
+ getSize(2f, Constraints(minWidth = 5.ipx, minHeight = 10.ipx))
+ )
+ }
+
+ private fun getSize(aspectRatio: Float, childContraints: Constraints): PxSize {
+ val positionedLatch = CountDownLatch(1)
+ val size = Ref<PxSize>()
+ val position = Ref<PxPosition>()
+ show {
+ Layout(@Children {
+ Container(AspectRatio(aspectRatio)) {
+ SaveLayoutInfo(size, position, positionedLatch)
+ }
+ }) { measurables, incomingConstraints ->
+ require(measurables.isNotEmpty())
+ val placeable = measurables.first().measure(childContraints)
+ layout(incomingConstraints.maxWidth, incomingConstraints.maxHeight) {
+ placeable.place(0.ipx, 0.ipx)
+ }
+ }
+ }
+ positionedLatch.await(1, TimeUnit.SECONDS)
+ return size.value!!
+ }
+}
\ No newline at end of file
diff --git a/ui/ui-layout/src/main/java/androidx/ui/layout/AspectRatio.kt b/ui/ui-layout/src/main/java/androidx/ui/layout/AspectRatio.kt
index 2856b4b..8415b59 100644
--- a/ui/ui-layout/src/main/java/androidx/ui/layout/AspectRatio.kt
+++ b/ui/ui-layout/src/main/java/androidx/ui/layout/AspectRatio.kt
@@ -16,17 +16,102 @@
package androidx.ui.layout
-import androidx.compose.Children
import androidx.compose.Composable
import androidx.compose.composer
+import androidx.ui.core.AlignmentLine
import androidx.ui.core.Constraints
+import androidx.ui.core.DensityScope
+import androidx.ui.core.IntPx
+import androidx.ui.core.IntPxPosition
import androidx.ui.core.IntPxSize
import androidx.ui.core.Layout
+import androidx.ui.core.LayoutModifier
+import androidx.ui.core.Measurable
import androidx.ui.core.ipx
import androidx.ui.core.isFinite
import androidx.ui.core.satisfiedBy
/**
+ * A layout modifier that attempts to size a layout to match a specified aspect ratio. The layout
+ * modifier will try to match one of the incoming constraints, in the following order: maxWidth,
+ * maxHeight, minWidth, minHeight. The size in the other dimension will then be computed
+ * according to the aspect ratio. Note that the provided aspectRatio will always correspond to
+ * the width/height ratio.
+ *
+ * If a valid size that satisfies the constraints is found this way, the modifier will size the
+ * target layout to it: the layout will be measured with the tight constraints to match the size.
+ * If a child is present, it will be measured with tight constraints to match the size.
+ * If no valid size is found, the aspect ratio will not be satisfied, and the target layout will
+ * be measured with the original constraints.
+ *
+ * Example usage:
+ * @sample androidx.ui.layout.samples.SimpleAspectRatio
+ *
+ * @param value Must be positive non-zero Float.
+ */
+fun AspectRatio(value: Float): LayoutModifier = AspectRatioModifier(value)
+
+/**
+ * A [LayoutModifier] that applies an aspect ratio to the wrapped UI element's size.
+ */
+private data class AspectRatioModifier(val aspectRatio: Float) : LayoutModifier {
+ init {
+ require(aspectRatio > 0) {
+ "Received aspect ratio value $aspectRatio is expected to be positive non-zero."
+ }
+ }
+
+ override fun DensityScope.modifyConstraints(constraints: Constraints): Constraints {
+ val size = constraints.findSizeWith(aspectRatio)
+ return if (size != null)
+ Constraints.tightConstraints(size.width, size.height)
+ else
+ constraints
+ }
+
+ override fun DensityScope.modifySize(
+ constraints: Constraints,
+ childSize: IntPxSize
+ ): IntPxSize {
+ val size = constraints.findSizeWith(aspectRatio)
+ return IntPxSize(
+ size?.width ?: childSize.width,
+ size?.height ?: childSize.height
+ )
+ }
+
+ override fun DensityScope.minIntrinsicWidthOf(measurable: Measurable, height: IntPx): IntPx {
+ return if (height == IntPx.Infinity) measurable.minIntrinsicWidth(height)
+ else height * aspectRatio
+ }
+
+ override fun DensityScope.maxIntrinsicWidthOf(measurable: Measurable, height: IntPx): IntPx {
+ return if (height == IntPx.Infinity) measurable.maxIntrinsicWidth(height)
+ else height * aspectRatio
+ }
+
+ override fun DensityScope.minIntrinsicHeightOf(measurable: Measurable, width: IntPx): IntPx {
+ return if (width == IntPx.Infinity) measurable.minIntrinsicHeight(width)
+ else width / aspectRatio
+ }
+
+ override fun DensityScope.maxIntrinsicHeightOf(measurable: Measurable, width: IntPx): IntPx {
+ return if (width == IntPx.Infinity) measurable.maxIntrinsicHeight(width)
+ else width / aspectRatio
+ }
+
+ override fun DensityScope.modifyPosition(
+ childPosition: IntPxPosition,
+ childSize: IntPxSize,
+ containerSize: IntPxSize
+ ): IntPxPosition = childPosition
+
+ override fun DensityScope.modifyAlignmentLine(line: AlignmentLine, value: IntPx?): IntPx? {
+ return value
+ }
+}
+
+/**
* Layout widget that attempts to size itself and a potential layout child to match a specified
* aspect ratio. The widget will try to match one of the incoming constraints, in the following
* order: maxWidth, maxHeight, minWidth, minHeight. The size in the other dimension will then be
@@ -53,17 +138,11 @@
aspectRatio: Float,
children: @Composable() () -> Unit
) {
+ require(aspectRatio > 0) {
+ "Received aspect ratio value $aspectRatio is expected to be positive non-zero."
+ }
Layout(children) { measurables, constraints ->
- val size = listOf(
- IntPxSize(constraints.maxWidth, constraints.maxWidth / aspectRatio),
- IntPxSize(constraints.maxHeight * aspectRatio, constraints.maxHeight),
- IntPxSize(constraints.minWidth, constraints.minWidth / aspectRatio),
- IntPxSize(constraints.minHeight * aspectRatio, constraints.minHeight)
- ).find {
- constraints.satisfiedBy(it) &&
- it.width != 0.ipx && it.height != 0.ipx &&
- it.width.isFinite() && it.height.isFinite()
- }
+ val size = constraints.findSizeWith(aspectRatio)
val measurable = measurables.firstOrNull()
val childConstraints = if (size != null) {
@@ -81,3 +160,16 @@
}
}
}
+
+private fun Constraints.findSizeWith(aspectRatio: Float): IntPxSize? {
+ return listOf(
+ IntPxSize(this.maxWidth, this.maxWidth / aspectRatio),
+ IntPxSize(this.maxHeight * aspectRatio, this.maxHeight),
+ IntPxSize(this.minWidth, this.minWidth / aspectRatio),
+ IntPxSize(this.minHeight * aspectRatio, this.minHeight)
+ ).find {
+ this.satisfiedBy(it) &&
+ it.width != 0.ipx && it.height != 0.ipx &&
+ it.width.isFinite() && it.height.isFinite()
+ }
+}
\ No newline at end of file