[go: nahoru, domu]

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