Add convenience Modifier.layout() modifier
Test: new test
Fixes: 161355194
Relnote: "Added Modifier.layout() that allows to create a custom layout modifier conveniently"
Change-Id: I73b699f2434a2c8ca0400fca1c331997c09a44e9
diff --git a/ui/ui-core/api/0.1.0-dev16.txt b/ui/ui-core/api/0.1.0-dev16.txt
index 2f3d540..4d3d265 100644
--- a/ui/ui-core/api/0.1.0-dev16.txt
+++ b/ui/ui-core/api/0.1.0-dev16.txt
@@ -627,6 +627,10 @@
method public default int minIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope, androidx.ui.core.IntrinsicMeasurable measurable, int height, androidx.compose.ui.unit.LayoutDirection layoutDirection);
}
+ public final class LayoutModifierKt {
+ method public static androidx.ui.core.Modifier layout(androidx.ui.core.Modifier, kotlin.jvm.functions.Function3<? super androidx.ui.core.MeasureScope,? super androidx.ui.core.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measure);
+ }
+
@androidx.ui.core.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.ui.core.Measurable androidx.ui.core.Remeasurement {
ctor public LayoutNode();
method public void attach(androidx.ui.core.Owner owner);
diff --git a/ui/ui-core/api/current.txt b/ui/ui-core/api/current.txt
index 2f3d540..4d3d265 100644
--- a/ui/ui-core/api/current.txt
+++ b/ui/ui-core/api/current.txt
@@ -627,6 +627,10 @@
method public default int minIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope, androidx.ui.core.IntrinsicMeasurable measurable, int height, androidx.compose.ui.unit.LayoutDirection layoutDirection);
}
+ public final class LayoutModifierKt {
+ method public static androidx.ui.core.Modifier layout(androidx.ui.core.Modifier, kotlin.jvm.functions.Function3<? super androidx.ui.core.MeasureScope,? super androidx.ui.core.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measure);
+ }
+
@androidx.ui.core.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.ui.core.Measurable androidx.ui.core.Remeasurement {
ctor public LayoutNode();
method public void attach(androidx.ui.core.Owner owner);
diff --git a/ui/ui-core/api/public_plus_experimental_0.1.0-dev16.txt b/ui/ui-core/api/public_plus_experimental_0.1.0-dev16.txt
index 2f3d540..4d3d265 100644
--- a/ui/ui-core/api/public_plus_experimental_0.1.0-dev16.txt
+++ b/ui/ui-core/api/public_plus_experimental_0.1.0-dev16.txt
@@ -627,6 +627,10 @@
method public default int minIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope, androidx.ui.core.IntrinsicMeasurable measurable, int height, androidx.compose.ui.unit.LayoutDirection layoutDirection);
}
+ public final class LayoutModifierKt {
+ method public static androidx.ui.core.Modifier layout(androidx.ui.core.Modifier, kotlin.jvm.functions.Function3<? super androidx.ui.core.MeasureScope,? super androidx.ui.core.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measure);
+ }
+
@androidx.ui.core.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.ui.core.Measurable androidx.ui.core.Remeasurement {
ctor public LayoutNode();
method public void attach(androidx.ui.core.Owner owner);
diff --git a/ui/ui-core/api/public_plus_experimental_current.txt b/ui/ui-core/api/public_plus_experimental_current.txt
index 2f3d540..4d3d265 100644
--- a/ui/ui-core/api/public_plus_experimental_current.txt
+++ b/ui/ui-core/api/public_plus_experimental_current.txt
@@ -627,6 +627,10 @@
method public default int minIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope, androidx.ui.core.IntrinsicMeasurable measurable, int height, androidx.compose.ui.unit.LayoutDirection layoutDirection);
}
+ public final class LayoutModifierKt {
+ method public static androidx.ui.core.Modifier layout(androidx.ui.core.Modifier, kotlin.jvm.functions.Function3<? super androidx.ui.core.MeasureScope,? super androidx.ui.core.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measure);
+ }
+
@androidx.ui.core.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.ui.core.Measurable androidx.ui.core.Remeasurement {
ctor public LayoutNode();
method public void attach(androidx.ui.core.Owner owner);
diff --git a/ui/ui-core/api/restricted_0.1.0-dev16.txt b/ui/ui-core/api/restricted_0.1.0-dev16.txt
index 4606ee4d..c720a68 100644
--- a/ui/ui-core/api/restricted_0.1.0-dev16.txt
+++ b/ui/ui-core/api/restricted_0.1.0-dev16.txt
@@ -690,6 +690,10 @@
method public default int minIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope, androidx.ui.core.IntrinsicMeasurable measurable, int height, androidx.compose.ui.unit.LayoutDirection layoutDirection);
}
+ public final class LayoutModifierKt {
+ method public static androidx.ui.core.Modifier layout(androidx.ui.core.Modifier, kotlin.jvm.functions.Function3<? super androidx.ui.core.MeasureScope,? super androidx.ui.core.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measure);
+ }
+
@androidx.ui.core.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.ui.core.Measurable androidx.ui.core.Remeasurement {
ctor public LayoutNode();
method public void attach(androidx.ui.core.Owner owner);
diff --git a/ui/ui-core/api/restricted_current.txt b/ui/ui-core/api/restricted_current.txt
index 4606ee4d..c720a68 100644
--- a/ui/ui-core/api/restricted_current.txt
+++ b/ui/ui-core/api/restricted_current.txt
@@ -690,6 +690,10 @@
method public default int minIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope, androidx.ui.core.IntrinsicMeasurable measurable, int height, androidx.compose.ui.unit.LayoutDirection layoutDirection);
}
+ public final class LayoutModifierKt {
+ method public static androidx.ui.core.Modifier layout(androidx.ui.core.Modifier, kotlin.jvm.functions.Function3<? super androidx.ui.core.MeasureScope,? super androidx.ui.core.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measure);
+ }
+
@androidx.ui.core.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.ui.core.Measurable androidx.ui.core.Remeasurement {
ctor public LayoutNode();
method public void attach(androidx.ui.core.Owner owner);
diff --git a/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/LayoutSample.kt b/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/LayoutSample.kt
index 99bb875..c5516bf 100644
--- a/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/LayoutSample.kt
+++ b/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/LayoutSample.kt
@@ -25,6 +25,12 @@
import androidx.ui.core.id
import androidx.ui.core.layoutId
import androidx.compose.foundation.Box
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Stack
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.offset
+import androidx.ui.core.layout
import androidx.ui.core.measureBlocksOf
@Sampled
@@ -165,3 +171,22 @@
}
}
}
+
+@Sampled
+@Composable
+fun ConvenienceLayoutModifierSample() {
+ Stack(
+ modifier = Modifier
+ .background(Color.Gray)
+ .layout { measurable, constraints ->
+ // an example modifier that adds 50 pixels of vertical padding
+ val padding = 50
+ val placeable = measurable.measure(constraints.offset(vertical = -padding))
+ this.layout(placeable.width, placeable.height + padding) {
+ placeable.place(0, padding)
+ }
+ }
+ ) {
+ Stack(Modifier.fillMaxSize().background(Color.DarkGray)) {}
+ }
+}
diff --git a/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/AndroidLayoutDrawTest.kt b/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/AndroidLayoutDrawTest.kt
index 92c34cc..b95a937 100644
--- a/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/AndroidLayoutDrawTest.kt
+++ b/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/AndroidLayoutDrawTest.kt
@@ -90,6 +90,10 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.ui.core.LayoutCoordinates
+import androidx.ui.core.layout
+import androidx.ui.core.onPositioned
+import androidx.ui.core.positionInRoot
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -355,7 +359,8 @@
this@draw.drawContent()
// Fill bottom half with innerColor -- should be clipped
- drawRect(model.innerColor,
+ drawRect(
+ model.innerColor,
topLeft = Offset(0f, size.height / 2f),
size = Size(size.width, size.height / 2f)
)
@@ -1508,27 +1513,30 @@
layout(Modifier.assertLines(10, 20))
layout(Modifier.assertLines(30, 30).offset(20.toDp(), 10.toDp()))
- layout(Modifier
- .assertLines(30, 30)
- .drawLayer()
- .offset(20.toDp(), 10.toDp())
+ layout(
+ Modifier
+ .assertLines(30, 30)
+ .drawLayer()
+ .offset(20.toDp(), 10.toDp())
)
- layout(Modifier
- .assertLines(30, 30)
- .background(Color.Blue)
- .drawLayer()
- .offset(20.toDp(), 10.toDp())
- .drawLayer()
- .background(Color.Blue)
+ layout(
+ Modifier
+ .assertLines(30, 30)
+ .background(Color.Blue)
+ .drawLayer()
+ .offset(20.toDp(), 10.toDp())
+ .drawLayer()
+ .background(Color.Blue)
)
- layout(Modifier
- .background(Color.Blue)
- .assertLines(30, 30)
- .background(Color.Blue)
- .drawLayer()
- .offset(20.toDp(), 10.toDp())
- .drawLayer()
- .background(Color.Blue)
+ layout(
+ Modifier
+ .background(Color.Blue)
+ .assertLines(30, 30)
+ .background(Color.Blue)
+ .drawLayer()
+ .offset(20.toDp(), 10.toDp())
+ .drawLayer()
+ .background(Color.Blue)
)
Wrap(
Modifier
@@ -2076,6 +2084,98 @@
)
}
+ @Test
+ fun layoutModifier_convenienceApi() {
+ val size = 100
+ val offset = 15f
+ 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, offset))
+ }
+ }.onPositioned {
+ resultCoordinates = it
+ latch.countDown()
+ }
+ )
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ activity.runOnUiThread {
+ assertEquals(size, resultCoordinates?.size?.height)
+ assertEquals(size, resultCoordinates?.size?.width)
+ assertEquals(Offset(offset, offset), resultCoordinates?.positionInRoot)
+ }
+ }
+
+ @Test
+ fun layoutModifier_convenienceApi_equivalent() {
+ val size = 100
+ val offset = 15f
+ 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, offset))
+ }
+ }.onPositioned {
+ convenienceCoordinates = it
+ latch.countDown()
+ }
+ )
+
+ val layoutModifier = object : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): MeasureScope.MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(Offset(offset, offset))
+ }
+ }
+ }
+ FixedSize(
+ size = size,
+ modifier = layoutModifier.plus(
+ onPositioned {
+ 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() {
@@ -2623,16 +2723,20 @@
linearLayout.orientation = LinearLayout.VERTICAL
val child = FrameLayout(activity)
activity.setContentView(linearLayout)
- linearLayout.addView(child, LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- 1f
- ))
- linearLayout.addView(View(activity), LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- 0,
- 10000f
- ))
+ linearLayout.addView(
+ child, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ 1f
+ )
+ )
+ linearLayout.addView(
+ View(activity), LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 0,
+ 10000f
+ )
+ )
child.viewTreeObserver.addOnPreDrawListener {
actualHeight = child.measuredHeight
latch.countDown()
diff --git a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutModifier.kt b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutModifier.kt
index 35b4b71..6ea7d98 100644
--- a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutModifier.kt
+++ b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutModifier.kt
@@ -253,3 +253,30 @@
private enum class IntrinsicMinMax { Min, Max }
private enum class IntrinsicWidthHeight { Width, Height }
}
+
+/**
+ * Creates a [LayoutModifier] that allows changing how the wrapped element is measured and laid out.
+ *
+ * This is a convenience API of creating a custom [LayoutModifier] modifier, without having to
+ * create a class or an object that implements the [LayoutModifier] interface. The intrinsic
+ * measurements follow the default logic provided by the [LayoutModifier].
+ *
+ * Example usage:
+ *
+ * @sample androidx.ui.core.samples.ConvenienceLayoutModifierSample
+ *
+ * @see androidx.ui.core.LayoutModifier
+ */
+fun Modifier.layout(
+ measure: MeasureScope.(Measurable, Constraints) -> MeasureScope.MeasureResult
+) = this.then(LayoutModifierImpl(measure))
+
+private data class LayoutModifierImpl(
+ val measure: MeasureScope.(Measurable, Constraints) -> MeasureScope.MeasureResult
+) : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ) = measure(measurable, constraints)
+}
\ No newline at end of file