SubcomposeLayout
It is an analogue of Layout which allows to subcompose the actual content during the measuring stage for example to use the values calculated during the measurement as params for the composition of the children.
Possible use cases:
* You need to know the constraints passed by the parent during the composition and can't solve your use case with just custom Layout or LayoutModifier. See WithConstraints.
* You want to use the size of one child during the composition of the second child. Example is using the sizes of the tabs in TabRow as a input in tabs indicator composable
* You want to compose your items lazily based on the available size. For example you have a list of 100 items and instead of composing all of them you only compose the ones which are currently visible(say 5 of them) and compose next items when the component is scrolled.
The implementation is currently based on an added concept of virtual layout nodes which is considered temporary and will be replaced with a proper subcomposition api which allows to subcompose multiple times into the same LayoutNode when it will be implemented.
Relnote: SubcomposeLayout is added. It is a low level primitive which allows to compose the children during the measuring if we want to use some values available only later during the measure for the subtree composition. For example WithConstraints is not implemented using SubcomposeLayout.
Test: new tests, manually with TabRow, WithConstraints and LazyColumnItems
Change-Id: I25cc8cfe8382db1ef61e93866ba08f4668cbc734
diff --git a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/collection/MutableVector.kt b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/collection/MutableVector.kt
index 555b178..cf44610 100644
--- a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/collection/MutableVector.kt
+++ b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/collection/MutableVector.kt
@@ -34,6 +34,11 @@
size: Int
) {
/**
+ * Stores allocated [MutableList] representation of this vector.
+ */
+ private var list: MutableList<T>? = null
+
+ /**
* The number of elements in the [MutableVector].
*/
var size: Int = size
@@ -207,7 +212,11 @@
/**
* Returns [MutableList] interface access to the [MutableVector].
*/
- fun asMutableList(): MutableList<T> = MutableVectorList(this)
+ fun asMutableList(): MutableList<T> {
+ return list ?: MutableVectorList(this).also {
+ list = it
+ }
+ }
/**
* Removes all elements in the [MutableVector].
diff --git a/ui/ui-core/api/0.1.0-dev16.txt b/ui/ui-core/api/0.1.0-dev16.txt
index 0c120f5..bd995cb 100644
--- a/ui/ui-core/api/0.1.0-dev16.txt
+++ b/ui/ui-core/api/0.1.0-dev16.txt
@@ -354,6 +354,9 @@
@kotlin.RequiresOptIn(level=RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY}) public @interface ExperimentalLayoutNodeApi {
}
+ @kotlin.RequiresOptIn(message="This is an experimental API for being able to perform subcomposition during the " + "measuring. API is likely to change before becoming stable.") public @interface ExperimentalSubcomposeLayoutApi {
+ }
+
@androidx.compose.Immutable public final class FixedScale implements androidx.ui.core.ContentScale {
ctor public FixedScale(float value);
method public float component1();
@@ -460,7 +463,6 @@
method public androidx.ui.core.LayoutCoordinates getCoordinates();
method public int getDepth();
method public int getHeight();
- method public androidx.ui.core.LayoutNode.LayoutState getLayoutState();
method public androidx.ui.core.LayoutNode.MeasureBlocks getMeasureBlocks();
method public androidx.ui.core.MeasureScope getMeasureScope();
method public androidx.ui.core.Modifier getModifier();
@@ -499,7 +501,6 @@
property public final int depth;
property public final int height;
property public final boolean isPlaced;
- property public final androidx.ui.core.LayoutNode.LayoutState layoutState;
property public final androidx.ui.core.LayoutNode.MeasureBlocks measureBlocks;
property public final androidx.ui.core.MeasureScope measureScope;
property public final androidx.ui.core.Modifier modifier;
@@ -511,16 +512,6 @@
property public final int width;
}
- public enum LayoutNode.LayoutState {
- method public static androidx.ui.core.LayoutNode.LayoutState valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.ui.core.LayoutNode.LayoutState[] values();
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState LayingOut;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Measuring;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRelayout;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRemeasure;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Ready;
- }
-
public static interface LayoutNode.MeasureBlocks {
method public int maxIntrinsicHeight(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int w, androidx.ui.core.LayoutDirection layoutDirection);
method public int maxIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int h, androidx.ui.core.LayoutDirection layoutDirection);
@@ -843,6 +834,15 @@
public final class SavedStateDelegateKt {
}
+ public final class SubcomposeLayoutKt {
+ method @androidx.compose.Composable @androidx.ui.core.ExperimentalSubcomposeLayoutApi public static <T> void SubcomposeLayout(androidx.ui.core.Modifier modifier = Modifier, kotlin.jvm.functions.Function2<? super androidx.ui.core.SubcomposeMeasureScope<T>,? super androidx.ui.core.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measureBlock);
+ }
+
+ @androidx.ui.core.ExperimentalSubcomposeLayoutApi public abstract class SubcomposeMeasureScope<T> extends androidx.ui.core.MeasureScope {
+ ctor public SubcomposeMeasureScope();
+ method public abstract java.util.List<androidx.ui.core.Measurable> subcompose(T? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class SubcompositionKt {
method @MainThread public static androidx.compose.Composition subcomposeInto(androidx.ui.core.LayoutNode container, androidx.compose.Recomposer recomposer, androidx.compose.CompositionReference? parent = null, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
}
diff --git a/ui/ui-core/api/current.txt b/ui/ui-core/api/current.txt
index 0c120f5..bd995cb 100644
--- a/ui/ui-core/api/current.txt
+++ b/ui/ui-core/api/current.txt
@@ -354,6 +354,9 @@
@kotlin.RequiresOptIn(level=RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY}) public @interface ExperimentalLayoutNodeApi {
}
+ @kotlin.RequiresOptIn(message="This is an experimental API for being able to perform subcomposition during the " + "measuring. API is likely to change before becoming stable.") public @interface ExperimentalSubcomposeLayoutApi {
+ }
+
@androidx.compose.Immutable public final class FixedScale implements androidx.ui.core.ContentScale {
ctor public FixedScale(float value);
method public float component1();
@@ -460,7 +463,6 @@
method public androidx.ui.core.LayoutCoordinates getCoordinates();
method public int getDepth();
method public int getHeight();
- method public androidx.ui.core.LayoutNode.LayoutState getLayoutState();
method public androidx.ui.core.LayoutNode.MeasureBlocks getMeasureBlocks();
method public androidx.ui.core.MeasureScope getMeasureScope();
method public androidx.ui.core.Modifier getModifier();
@@ -499,7 +501,6 @@
property public final int depth;
property public final int height;
property public final boolean isPlaced;
- property public final androidx.ui.core.LayoutNode.LayoutState layoutState;
property public final androidx.ui.core.LayoutNode.MeasureBlocks measureBlocks;
property public final androidx.ui.core.MeasureScope measureScope;
property public final androidx.ui.core.Modifier modifier;
@@ -511,16 +512,6 @@
property public final int width;
}
- public enum LayoutNode.LayoutState {
- method public static androidx.ui.core.LayoutNode.LayoutState valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.ui.core.LayoutNode.LayoutState[] values();
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState LayingOut;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Measuring;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRelayout;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRemeasure;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Ready;
- }
-
public static interface LayoutNode.MeasureBlocks {
method public int maxIntrinsicHeight(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int w, androidx.ui.core.LayoutDirection layoutDirection);
method public int maxIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int h, androidx.ui.core.LayoutDirection layoutDirection);
@@ -843,6 +834,15 @@
public final class SavedStateDelegateKt {
}
+ public final class SubcomposeLayoutKt {
+ method @androidx.compose.Composable @androidx.ui.core.ExperimentalSubcomposeLayoutApi public static <T> void SubcomposeLayout(androidx.ui.core.Modifier modifier = Modifier, kotlin.jvm.functions.Function2<? super androidx.ui.core.SubcomposeMeasureScope<T>,? super androidx.ui.core.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measureBlock);
+ }
+
+ @androidx.ui.core.ExperimentalSubcomposeLayoutApi public abstract class SubcomposeMeasureScope<T> extends androidx.ui.core.MeasureScope {
+ ctor public SubcomposeMeasureScope();
+ method public abstract java.util.List<androidx.ui.core.Measurable> subcompose(T? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class SubcompositionKt {
method @MainThread public static androidx.compose.Composition subcomposeInto(androidx.ui.core.LayoutNode container, androidx.compose.Recomposer recomposer, androidx.compose.CompositionReference? parent = null, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
}
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 0c120f5..bd995cb 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
@@ -354,6 +354,9 @@
@kotlin.RequiresOptIn(level=RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY}) public @interface ExperimentalLayoutNodeApi {
}
+ @kotlin.RequiresOptIn(message="This is an experimental API for being able to perform subcomposition during the " + "measuring. API is likely to change before becoming stable.") public @interface ExperimentalSubcomposeLayoutApi {
+ }
+
@androidx.compose.Immutable public final class FixedScale implements androidx.ui.core.ContentScale {
ctor public FixedScale(float value);
method public float component1();
@@ -460,7 +463,6 @@
method public androidx.ui.core.LayoutCoordinates getCoordinates();
method public int getDepth();
method public int getHeight();
- method public androidx.ui.core.LayoutNode.LayoutState getLayoutState();
method public androidx.ui.core.LayoutNode.MeasureBlocks getMeasureBlocks();
method public androidx.ui.core.MeasureScope getMeasureScope();
method public androidx.ui.core.Modifier getModifier();
@@ -499,7 +501,6 @@
property public final int depth;
property public final int height;
property public final boolean isPlaced;
- property public final androidx.ui.core.LayoutNode.LayoutState layoutState;
property public final androidx.ui.core.LayoutNode.MeasureBlocks measureBlocks;
property public final androidx.ui.core.MeasureScope measureScope;
property public final androidx.ui.core.Modifier modifier;
@@ -511,16 +512,6 @@
property public final int width;
}
- public enum LayoutNode.LayoutState {
- method public static androidx.ui.core.LayoutNode.LayoutState valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.ui.core.LayoutNode.LayoutState[] values();
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState LayingOut;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Measuring;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRelayout;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRemeasure;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Ready;
- }
-
public static interface LayoutNode.MeasureBlocks {
method public int maxIntrinsicHeight(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int w, androidx.ui.core.LayoutDirection layoutDirection);
method public int maxIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int h, androidx.ui.core.LayoutDirection layoutDirection);
@@ -843,6 +834,15 @@
public final class SavedStateDelegateKt {
}
+ public final class SubcomposeLayoutKt {
+ method @androidx.compose.Composable @androidx.ui.core.ExperimentalSubcomposeLayoutApi public static <T> void SubcomposeLayout(androidx.ui.core.Modifier modifier = Modifier, kotlin.jvm.functions.Function2<? super androidx.ui.core.SubcomposeMeasureScope<T>,? super androidx.ui.core.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measureBlock);
+ }
+
+ @androidx.ui.core.ExperimentalSubcomposeLayoutApi public abstract class SubcomposeMeasureScope<T> extends androidx.ui.core.MeasureScope {
+ ctor public SubcomposeMeasureScope();
+ method public abstract java.util.List<androidx.ui.core.Measurable> subcompose(T? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class SubcompositionKt {
method @MainThread public static androidx.compose.Composition subcomposeInto(androidx.ui.core.LayoutNode container, androidx.compose.Recomposer recomposer, androidx.compose.CompositionReference? parent = null, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
}
diff --git a/ui/ui-core/api/public_plus_experimental_current.txt b/ui/ui-core/api/public_plus_experimental_current.txt
index 0c120f5..bd995cb 100644
--- a/ui/ui-core/api/public_plus_experimental_current.txt
+++ b/ui/ui-core/api/public_plus_experimental_current.txt
@@ -354,6 +354,9 @@
@kotlin.RequiresOptIn(level=RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY}) public @interface ExperimentalLayoutNodeApi {
}
+ @kotlin.RequiresOptIn(message="This is an experimental API for being able to perform subcomposition during the " + "measuring. API is likely to change before becoming stable.") public @interface ExperimentalSubcomposeLayoutApi {
+ }
+
@androidx.compose.Immutable public final class FixedScale implements androidx.ui.core.ContentScale {
ctor public FixedScale(float value);
method public float component1();
@@ -460,7 +463,6 @@
method public androidx.ui.core.LayoutCoordinates getCoordinates();
method public int getDepth();
method public int getHeight();
- method public androidx.ui.core.LayoutNode.LayoutState getLayoutState();
method public androidx.ui.core.LayoutNode.MeasureBlocks getMeasureBlocks();
method public androidx.ui.core.MeasureScope getMeasureScope();
method public androidx.ui.core.Modifier getModifier();
@@ -499,7 +501,6 @@
property public final int depth;
property public final int height;
property public final boolean isPlaced;
- property public final androidx.ui.core.LayoutNode.LayoutState layoutState;
property public final androidx.ui.core.LayoutNode.MeasureBlocks measureBlocks;
property public final androidx.ui.core.MeasureScope measureScope;
property public final androidx.ui.core.Modifier modifier;
@@ -511,16 +512,6 @@
property public final int width;
}
- public enum LayoutNode.LayoutState {
- method public static androidx.ui.core.LayoutNode.LayoutState valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.ui.core.LayoutNode.LayoutState[] values();
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState LayingOut;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Measuring;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRelayout;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRemeasure;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Ready;
- }
-
public static interface LayoutNode.MeasureBlocks {
method public int maxIntrinsicHeight(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int w, androidx.ui.core.LayoutDirection layoutDirection);
method public int maxIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int h, androidx.ui.core.LayoutDirection layoutDirection);
@@ -843,6 +834,15 @@
public final class SavedStateDelegateKt {
}
+ public final class SubcomposeLayoutKt {
+ method @androidx.compose.Composable @androidx.ui.core.ExperimentalSubcomposeLayoutApi public static <T> void SubcomposeLayout(androidx.ui.core.Modifier modifier = Modifier, kotlin.jvm.functions.Function2<? super androidx.ui.core.SubcomposeMeasureScope<T>,? super androidx.ui.core.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measureBlock);
+ }
+
+ @androidx.ui.core.ExperimentalSubcomposeLayoutApi public abstract class SubcomposeMeasureScope<T> extends androidx.ui.core.MeasureScope {
+ ctor public SubcomposeMeasureScope();
+ method public abstract java.util.List<androidx.ui.core.Measurable> subcompose(T? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class SubcompositionKt {
method @MainThread public static androidx.compose.Composition subcomposeInto(androidx.ui.core.LayoutNode container, androidx.compose.Recomposer recomposer, androidx.compose.CompositionReference? parent = null, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
}
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 a6abfad..a12c324 100644
--- a/ui/ui-core/api/restricted_0.1.0-dev16.txt
+++ b/ui/ui-core/api/restricted_0.1.0-dev16.txt
@@ -374,6 +374,9 @@
@kotlin.RequiresOptIn(level=RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY}) public @interface ExperimentalLayoutNodeApi {
}
+ @kotlin.RequiresOptIn(message="This is an experimental API for being able to perform subcomposition during the " + "measuring. API is likely to change before becoming stable.") public @interface ExperimentalSubcomposeLayoutApi {
+ }
+
@androidx.compose.Immutable public final class FixedScale implements androidx.ui.core.ContentScale {
ctor public FixedScale(float value);
method public float component1();
@@ -512,7 +515,6 @@
method public androidx.ui.core.LayoutCoordinates getCoordinates();
method public int getDepth();
method public int getHeight();
- method public androidx.ui.core.LayoutNode.LayoutState getLayoutState();
method public androidx.ui.core.LayoutNode.MeasureBlocks getMeasureBlocks();
method public androidx.ui.core.MeasureScope getMeasureScope();
method public androidx.ui.core.Modifier getModifier();
@@ -551,7 +553,6 @@
property public final int depth;
property public final int height;
property public final boolean isPlaced;
- property public final androidx.ui.core.LayoutNode.LayoutState layoutState;
property public final androidx.ui.core.LayoutNode.MeasureBlocks measureBlocks;
property public final androidx.ui.core.MeasureScope measureScope;
property public final androidx.ui.core.Modifier modifier;
@@ -563,16 +564,6 @@
property public final int width;
}
- public enum LayoutNode.LayoutState {
- method public static androidx.ui.core.LayoutNode.LayoutState valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.ui.core.LayoutNode.LayoutState[] values();
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState LayingOut;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Measuring;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRelayout;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRemeasure;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Ready;
- }
-
public static interface LayoutNode.MeasureBlocks {
method public int maxIntrinsicHeight(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int w, androidx.ui.core.LayoutDirection layoutDirection);
method public int maxIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int h, androidx.ui.core.LayoutDirection layoutDirection);
@@ -895,6 +886,15 @@
public final class SavedStateDelegateKt {
}
+ public final class SubcomposeLayoutKt {
+ method @androidx.compose.Composable @androidx.ui.core.ExperimentalSubcomposeLayoutApi public static <T> void SubcomposeLayout(androidx.ui.core.Modifier modifier = Modifier, kotlin.jvm.functions.Function2<? super androidx.ui.core.SubcomposeMeasureScope<T>,? super androidx.ui.core.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measureBlock);
+ }
+
+ @androidx.ui.core.ExperimentalSubcomposeLayoutApi public abstract class SubcomposeMeasureScope<T> extends androidx.ui.core.MeasureScope {
+ ctor public SubcomposeMeasureScope();
+ method public abstract java.util.List<androidx.ui.core.Measurable> subcompose(T? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class SubcompositionKt {
method @MainThread public static androidx.compose.Composition subcomposeInto(androidx.ui.core.LayoutNode container, androidx.compose.Recomposer recomposer, androidx.compose.CompositionReference? parent = null, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
}
diff --git a/ui/ui-core/api/restricted_current.txt b/ui/ui-core/api/restricted_current.txt
index a6abfad..a12c324 100644
--- a/ui/ui-core/api/restricted_current.txt
+++ b/ui/ui-core/api/restricted_current.txt
@@ -374,6 +374,9 @@
@kotlin.RequiresOptIn(level=RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY}) public @interface ExperimentalLayoutNodeApi {
}
+ @kotlin.RequiresOptIn(message="This is an experimental API for being able to perform subcomposition during the " + "measuring. API is likely to change before becoming stable.") public @interface ExperimentalSubcomposeLayoutApi {
+ }
+
@androidx.compose.Immutable public final class FixedScale implements androidx.ui.core.ContentScale {
ctor public FixedScale(float value);
method public float component1();
@@ -512,7 +515,6 @@
method public androidx.ui.core.LayoutCoordinates getCoordinates();
method public int getDepth();
method public int getHeight();
- method public androidx.ui.core.LayoutNode.LayoutState getLayoutState();
method public androidx.ui.core.LayoutNode.MeasureBlocks getMeasureBlocks();
method public androidx.ui.core.MeasureScope getMeasureScope();
method public androidx.ui.core.Modifier getModifier();
@@ -551,7 +553,6 @@
property public final int depth;
property public final int height;
property public final boolean isPlaced;
- property public final androidx.ui.core.LayoutNode.LayoutState layoutState;
property public final androidx.ui.core.LayoutNode.MeasureBlocks measureBlocks;
property public final androidx.ui.core.MeasureScope measureScope;
property public final androidx.ui.core.Modifier modifier;
@@ -563,16 +564,6 @@
property public final int width;
}
- public enum LayoutNode.LayoutState {
- method public static androidx.ui.core.LayoutNode.LayoutState valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.ui.core.LayoutNode.LayoutState[] values();
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState LayingOut;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Measuring;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRelayout;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState NeedsRemeasure;
- enum_constant public static final androidx.ui.core.LayoutNode.LayoutState Ready;
- }
-
public static interface LayoutNode.MeasureBlocks {
method public int maxIntrinsicHeight(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int w, androidx.ui.core.LayoutDirection layoutDirection);
method public int maxIntrinsicWidth(androidx.ui.core.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.ui.core.IntrinsicMeasurable> measurables, int h, androidx.ui.core.LayoutDirection layoutDirection);
@@ -895,6 +886,15 @@
public final class SavedStateDelegateKt {
}
+ public final class SubcomposeLayoutKt {
+ method @androidx.compose.Composable @androidx.ui.core.ExperimentalSubcomposeLayoutApi public static <T> void SubcomposeLayout(androidx.ui.core.Modifier modifier = Modifier, kotlin.jvm.functions.Function2<? super androidx.ui.core.SubcomposeMeasureScope<T>,? super androidx.ui.core.Constraints,? extends androidx.ui.core.MeasureScope.MeasureResult> measureBlock);
+ }
+
+ @androidx.ui.core.ExperimentalSubcomposeLayoutApi public abstract class SubcomposeMeasureScope<T> extends androidx.ui.core.MeasureScope {
+ ctor public SubcomposeMeasureScope();
+ method public abstract java.util.List<androidx.ui.core.Measurable> subcompose(T? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class SubcompositionKt {
method @MainThread public static androidx.compose.Composition subcomposeInto(androidx.ui.core.LayoutNode container, androidx.compose.Recomposer recomposer, androidx.compose.CompositionReference? parent = null, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
}
diff --git a/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/SubcomposeLayoutSample.kt b/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/SubcomposeLayoutSample.kt
new file mode 100644
index 0000000..507283c
--- /dev/null
+++ b/ui/ui-core/samples/src/main/java/androidx/ui/core/samples/SubcomposeLayoutSample.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 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.core.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.Composable
+import androidx.ui.core.ExperimentalSubcomposeLayoutApi
+import androidx.ui.core.SubcomposeLayout
+import androidx.ui.unit.IntSize
+
+@Sampled
+@Composable
+@OptIn(ExperimentalSubcomposeLayoutApi::class)
+fun SubcomposeLayoutSample(
+ mainContent: @Composable () -> Unit,
+ dependentContent: @Composable (IntSize) -> Unit
+) {
+ SubcomposeLayout<SlotsEnum> { constraints ->
+ val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
+ it.measure(constraints)
+ }
+ val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
+ IntSize(
+ width = maxOf(currentMax.width, placeable.width),
+ height = maxOf(currentMax.height, placeable.height)
+ )
+ }
+ layout(maxSize.width, maxSize.height) {
+ mainPlaceables.forEach { it.place(0, 0) }
+ subcompose(SlotsEnum.Dependent) {
+ dependentContent(maxSize)
+ }.forEach {
+ it.measure(constraints).place(0, 0)
+ }
+ }
+ }
+}
+
+enum class SlotsEnum {
+ Main,
+ Dependent
+}
diff --git a/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/SubcomposeLayoutTest.kt b/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/SubcomposeLayoutTest.kt
new file mode 100644
index 0000000..fdad6b3
--- /dev/null
+++ b/ui/ui-core/src/androidAndroidTest/kotlin/androidx/ui/core/test/SubcomposeLayoutTest.kt
@@ -0,0 +1,435 @@
+/*
+ * Copyright 2020 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.core.test
+
+import android.graphics.Bitmap
+import androidx.compose.Composable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.Stack
+import androidx.compose.foundation.layout.size
+import androidx.compose.mutableStateOf
+import androidx.compose.onActive
+import androidx.compose.onDispose
+import androidx.test.filters.SmallTest
+import androidx.ui.core.ExperimentalSubcomposeLayoutApi
+import androidx.ui.core.Modifier
+import androidx.ui.core.SubcomposeLayout
+import androidx.ui.core.testTag
+import androidx.ui.core.zIndex
+import androidx.ui.graphics.Color
+import androidx.ui.test.assertHeightIsEqualTo
+import androidx.ui.test.assertIsDisplayed
+import androidx.ui.test.assertPositionInRootIsEqualTo
+import androidx.ui.test.assertWidthIsEqualTo
+import androidx.ui.test.captureToBitmap
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.onNodeWithTag
+import androidx.ui.test.runOnIdle
+import androidx.ui.test.waitForIdle
+import androidx.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+@OptIn(ExperimentalSubcomposeLayoutApi::class)
+class SubcomposeLayoutTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+ @get:Rule
+ val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
+
+ @Test
+ fun useSizeOfTheFirstItemInSecondSubcomposition() {
+ val firstTag = "first"
+ val secondTag = "second"
+
+ rule.setContent {
+ SubcomposeLayout<Int> { constraints ->
+ val first = subcompose(0) {
+ Spacer(Modifier.size(50.dp).testTag(firstTag))
+ }.first().measure(constraints)
+
+ // it is an input for the second subcomposition
+ val halfFirstSize = (first.width / 2).toDp()
+
+ val second = subcompose(1) {
+ Spacer(Modifier.size(halfFirstSize).testTag(secondTag))
+ }.first().measure(constraints)
+
+ layout(first.width, first.height) {
+ first.place(0, 0)
+ second.place(first.width - second.width, first.height - second.height)
+ }
+ }
+ }
+
+ onNodeWithTag(firstTag)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+
+ onNodeWithTag(secondTag)
+ .assertPositionInRootIsEqualTo(25.dp, 25.dp)
+ .assertWidthIsEqualTo(25.dp)
+ .assertHeightIsEqualTo(25.dp)
+ }
+
+ @Test
+ fun subcomposeMultipleLayoutsInOneSlot() {
+ val firstTag = "first"
+ val secondTag = "second"
+ val layoutTag = "layout"
+
+ rule.setContent {
+ SubcomposeLayout<Unit>(Modifier.testTag(layoutTag)) { constraints ->
+ val placeables = subcompose(Unit) {
+ Spacer(Modifier.size(50.dp).testTag(firstTag))
+ Spacer(Modifier.size(30.dp).testTag(secondTag))
+ }.map {
+ it.measure(constraints)
+ }
+
+ val maxWidth = placeables.maxByOrNull { it.width }!!.width
+ val height = placeables.sumBy { it.height }
+
+ layout(maxWidth, height) {
+ placeables.fold(0) { top, placeable ->
+ placeable.place(0, top)
+ top + placeable.height
+ }
+ }
+ }
+ }
+
+ onNodeWithTag(firstTag)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+
+ onNodeWithTag(secondTag)
+ .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+ .assertWidthIsEqualTo(30.dp)
+ .assertHeightIsEqualTo(30.dp)
+
+ onNodeWithTag(layoutTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(80.dp)
+ }
+
+ @Test
+ fun recompositionDeepInsideTheSlotDoesntRecomposeUnaffectedLayerOrRemeasure() {
+ val model = mutableStateOf(0)
+ var measuresCount = 0
+ var recompositionsCount1 = 0
+ var recompositionsCount2 = 0
+
+ rule.setContent {
+ SubcomposeLayout<Unit> { constraints ->
+ measuresCount++
+ val placeable = subcompose(Unit) {
+ recompositionsCount1++
+ Stack(Modifier.size(20.dp)) {
+ model.value // model read
+ recompositionsCount2++
+ }
+ }.first().measure(constraints)
+
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+
+ runOnIdle { model.value++ }
+
+ runOnIdle {
+ assertEquals(1, measuresCount)
+ assertEquals(1, recompositionsCount1)
+ assertEquals(2, recompositionsCount2)
+ }
+ }
+
+ @Test
+ fun recompositionOfTheFirstSlotDoestAffectTheSecond() {
+ val model = mutableStateOf(0)
+ var recompositionsCount1 = 0
+ var recompositionsCount2 = 0
+
+ rule.setContent {
+ SubcomposeLayout<Int> {
+ subcompose(1) {
+ recompositionsCount1++
+ model.value // model read
+ }
+ subcompose(2) {
+ recompositionsCount2++
+ }
+
+ layout(100, 100) {
+ }
+ }
+ }
+
+ runOnIdle { model.value++ }
+
+ runOnIdle {
+ assertEquals(2, recompositionsCount1)
+ assertEquals(1, recompositionsCount2)
+ }
+ }
+
+ @Test
+ fun addLayoutOnlyAfterRecomposition() {
+ val addChild = mutableStateOf(false)
+ val childTag = "child"
+ val layoutTag = "layout"
+
+ rule.setContent {
+ SubcomposeLayout<Unit>(Modifier.testTag(layoutTag)) { constraints ->
+ val placeables = subcompose(Unit) {
+ if (addChild.value) {
+ Spacer(Modifier.size(20.dp).testTag(childTag))
+ }
+ }.map { it.measure(constraints) }
+
+ val size = placeables.firstOrNull()?.width ?: 0
+ layout(size, size) {
+ placeables.forEach { it.place(0, 0) }
+ }
+ }
+ }
+
+ onNodeWithTag(layoutTag)
+ .assertWidthIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(0.dp)
+
+ onNodeWithTag(childTag)
+ .assertDoesNotExist()
+
+ runOnIdle {
+ addChild.value = true
+ }
+
+ onNodeWithTag(layoutTag)
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+
+ onNodeWithTag(childTag)
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun providingNewLambdaCausingRecomposition() {
+ val content = mutableStateOf<@Composable () -> Unit>({
+ Spacer(Modifier.size(10.dp))
+ })
+
+ rule.setContent {
+ MySubcomposeLayout(content.value)
+ }
+
+ val updatedTag = "updated"
+
+ runOnIdle {
+ content.value = {
+ Spacer(Modifier.size(10.dp).testTag(updatedTag))
+ }
+ }
+
+ onNodeWithTag(updatedTag)
+ .assertIsDisplayed()
+ }
+
+ @Composable
+ private fun MySubcomposeLayout(slotContent: @Composable () -> Unit) {
+ SubcomposeLayout<Unit> { constraints ->
+ val placeables = subcompose(Unit, slotContent).map { it.measure(constraints) }
+ val maxWidth = placeables.maxByOrNull { it.width }!!.width
+ val height = placeables.sumBy { it.height }
+ layout(maxWidth, height) {
+ placeables.forEach { it.place(0, 0) }
+ }
+ }
+ }
+
+ @Test
+ fun notSubcomposedSlotIsDisposed() {
+ val addSlot = mutableStateOf(true)
+ var composed = false
+ var disposed = false
+
+ rule.setContent {
+ SubcomposeLayout<Unit> {
+ if (addSlot.value) {
+ subcompose(Unit) {
+ onActive {
+ composed = true
+ }
+ onDispose {
+ disposed = true
+ }
+ }
+ }
+ layout(10, 10) {}
+ }
+ }
+
+ runOnIdle {
+ assertThat(composed).isTrue()
+ assertThat(disposed).isFalse()
+
+ addSlot.value = false
+ }
+
+ runOnIdle {
+ assertThat(disposed).isTrue()
+ }
+ }
+
+ @Test
+ fun slotsAreDrawnInTheOrderTheyComposed() {
+ val layoutTag = "layout"
+
+ rule.setContent {
+ SubcomposeLayout<Color>(Modifier.testTag(layoutTag)) { constraints ->
+ val first = subcompose(Color.Red) {
+ Spacer(Modifier.size(10.dp).background(Color.Red))
+ }.first().measure(constraints)
+ val second = subcompose(Color.Green) {
+ Spacer(Modifier.size(10.dp).background(Color.Green))
+ }.first().measure(constraints)
+ layout(first.width, first.height) {
+ first.place(0, 0)
+ second.place(0, 0)
+ }
+ }
+ }
+
+ waitForIdle()
+
+ onNodeWithTag(layoutTag)
+ .captureToBitmap()
+ .assertCenterPixelColor(Color.Green)
+ }
+
+ @Test
+ fun slotsCouldBeReordered() {
+ val layoutTag = "layout"
+ val firstSlotIsRed = mutableStateOf(true)
+
+ rule.setContent {
+ SubcomposeLayout<Color>(Modifier.testTag(layoutTag)) { constraints ->
+ val firstColor = if (firstSlotIsRed.value) Color.Red else Color.Green
+ val secondColor = if (firstSlotIsRed.value) Color.Green else Color.Red
+ val first = subcompose(firstColor) {
+ Spacer(Modifier.size(10.dp).background(firstColor))
+ }.first().measure(constraints)
+ val second = subcompose(secondColor) {
+ Spacer(Modifier.size(10.dp).background(secondColor))
+ }.first().measure(constraints)
+ layout(first.width, first.height) {
+ first.place(0, 0)
+ second.place(0, 0)
+ }
+ }
+ }
+
+ onNodeWithTag(layoutTag)
+ .captureToBitmap()
+ .assertCenterPixelColor(Color.Green)
+
+ runOnIdle {
+ firstSlotIsRed.value = false
+ }
+
+ onNodeWithTag(layoutTag)
+ .captureToBitmap()
+ .assertCenterPixelColor(Color.Red)
+ }
+
+ @Test
+ fun drawingOrderCouldBeChangedUsingZIndex() {
+ val layoutTag = "layout"
+
+ rule.setContent {
+ SubcomposeLayout<Color>(Modifier.testTag(layoutTag)) { constraints ->
+ val first = subcompose(Color.Red) {
+ Spacer(Modifier.size(10.dp).background(Color.Red).zIndex(1f))
+ }.first().measure(constraints)
+ val second = subcompose(Color.Green) {
+ Spacer(Modifier.size(10.dp).background(Color.Green))
+ }.first().measure(constraints)
+ layout(first.width, first.height) {
+ first.place(0, 0)
+ second.place(0, 0)
+ }
+ }
+ }
+
+ onNodeWithTag(layoutTag)
+ .captureToBitmap()
+ .assertCenterPixelColor(Color.Red)
+ }
+
+ @Test
+ fun slotsAreDisposedWhenLayoutIsDisposed() {
+ val addLayout = mutableStateOf(true)
+ var firstDisposed = false
+ var secondDisposed = false
+
+ rule.setContent {
+ if (addLayout.value) {
+ SubcomposeLayout<Int> {
+ subcompose(0) {
+ onDispose {
+ firstDisposed = true
+ }
+ }
+ subcompose(1) {
+ onDispose {
+ secondDisposed = true
+ }
+ }
+ layout(10, 10) {}
+ }
+ }
+ }
+
+ runOnIdle {
+ assertThat(firstDisposed).isFalse()
+ assertThat(secondDisposed).isFalse()
+
+ addLayout.value = false
+ }
+
+ runOnIdle {
+ assertThat(firstDisposed).isTrue()
+ assertThat(secondDisposed).isTrue()
+ }
+ }
+}
+
+fun Bitmap.assertCenterPixelColor(expectedColor: Color) {
+ assertColor(expectedColor, width / 2, height / 2)
+}
diff --git a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/Layout.kt b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/Layout.kt
index 31d7ade..909a269 100644
--- a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/Layout.kt
+++ b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/Layout.kt
@@ -18,23 +18,17 @@
import androidx.compose.Applier
import androidx.compose.Composable
-import androidx.compose.ComposableContract
-import androidx.compose.Composition
-import androidx.compose.CompositionReference
import androidx.compose.ExperimentalComposeApi
-import androidx.compose.Recomposer
import androidx.compose.Stable
-import androidx.compose.compositionReference
import androidx.compose.currentComposer
import androidx.compose.emit
-import androidx.compose.onDispose
import androidx.compose.remember
-import androidx.ui.core.LayoutNode.LayoutState
import androidx.ui.unit.Density
import androidx.ui.unit.Dp
import androidx.ui.unit.IntOffset
import androidx.ui.unit.IntSize
import androidx.ui.util.fastForEach
+import androidx.ui.util.fastMap
import kotlin.math.max
/**
@@ -672,43 +666,27 @@
*
* @param modifier Modifier to be applied to the introduced layout.
*/
+@OptIn(ExperimentalSubcomposeLayoutApi::class)
@Composable
-@OptIn(ExperimentalLayoutNodeApi::class)
fun WithConstraints(
modifier: Modifier = Modifier,
children: @Composable WithConstraintsScope.() -> Unit
) {
- val state = remember { WithConstrainsState() }
- state.children = children
- // TODO(lmr): refactor these APIs so that recomposer isn't necessary
- @OptIn(ExperimentalComposeApi::class)
- state.recomposer = currentComposer.recomposer
- state.compositionRef = compositionReference()
- // if this code was executed subcomposition must be triggered as well
- state.forceRecompose = true
+ SubcomposeLayout<Unit>(modifier) { constraints ->
+ val scope = WithConstraintsScopeImpl(this, constraints, layoutDirection)
+ val placeables = subcompose(Unit) { scope.children() }
+ .fastMap { it.measure(constraints) }
- val materialized = currentComposer.materialize(modifier)
- @OptIn(ExperimentalComposeApi::class)
- emit<LayoutNode, Applier<Any>>(
- ctor = LayoutEmitHelper.constructor,
- update = {
- set(materialized, LayoutEmitHelper.setModifier)
- set(state.measureBlocks, LayoutEmitHelper.setMeasureBlocks)
- set(state.nodeRef, LayoutEmitHelper.setRef)
+ var maxWidth: Int = constraints.minWidth
+ var maxHeight: Int = constraints.minHeight
+ placeables.fastForEach {
+ maxWidth = max(maxWidth, it.width)
+ maxHeight = max(maxHeight, it.height)
}
- )
- // if LayoutNode scheduled the remeasuring no further steps are needed - subcomposition
- // will happen later on the measuring stage. otherwise we can assume the LayoutNode
- // already holds the final Constraints and we should subcompose straight away.
- // if owner is null this means we are not yet attached. once attached the remeasuring
- // will be scheduled which would cause subcomposition
- val layoutNode = state.nodeRef.value!!
- if (layoutNode.layoutState == LayoutState.Ready && layoutNode.owner != null) {
- state.subcompose()
- }
- onDispose {
- state.composition?.dispose()
+ layout(maxWidth, maxHeight) {
+ placeables.fastForEach { it.place(0, 0) }
+ }
}
}
@@ -753,83 +731,17 @@
val maxHeight: Dp
}
-@OptIn(ExperimentalLayoutNodeApi::class)
-private class WithConstrainsState {
- lateinit var recomposer: Recomposer
- var compositionRef: CompositionReference? = null
- val nodeRef = Ref<LayoutNode>()
- var children: @Composable WithConstraintsScope.() -> Unit = { }
- var forceRecompose = false
- var composition: Composition? = null
-
- private var scope: WithConstraintsScope = WithConstraintsScopeImpl(
- Density(1f),
- Constraints.fixed(0, 0),
- LayoutDirection.Ltr
- )
-
- val measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks(
- error = "Intrinsic measurements are not supported by WithConstraints"
- ) {
- override fun measure(
- measureScope: MeasureScope,
- measurables: List<Measurable>,
- constraints: Constraints,
- layoutDirection: LayoutDirection
- ): MeasureScope.MeasureResult {
- val root = nodeRef.value!!
- if (scope.constraints != constraints ||
- scope.layoutDirection != measureScope.layoutDirection ||
- forceRecompose
- ) {
- scope = WithConstraintsScopeImpl(measureScope, constraints, layoutDirection)
- root.ignoreModelReads { subcompose() }
- }
-
- // Measure the obtained children and compute our size.
- val layoutChildren = root.children
- var maxWidth: Int = constraints.minWidth
- var maxHeight: Int = constraints.minHeight
- layoutChildren.fastForEach {
- it.measure(constraints, layoutDirection)
- maxWidth = max(maxWidth, it.width)
- maxHeight = max(maxHeight, it.height)
- }
- maxWidth = maxWidth.coerceAtMost(constraints.maxWidth)
- maxHeight = maxHeight.coerceAtMost(constraints.maxHeight)
-
- return measureScope.layout(maxWidth, maxHeight) {
- layoutChildren.fastForEach { it.place(0, 0) }
- }
- }
- }
-
- @OptIn(ExperimentalComposeApi::class)
- fun subcompose() {
- // TODO(b/150390669): Review use of @ComposableContract(tracked = false)
- composition =
- subcomposeInto(
- nodeRef.value!!,
- recomposer,
- compositionRef
- ) @ComposableContract(tracked = false) {
- scope.children()
- }
- forceRecompose = false
- }
-
- private data class WithConstraintsScopeImpl(
- private val density: Density,
- override val constraints: Constraints,
- override val layoutDirection: LayoutDirection
- ) : WithConstraintsScope {
- override val minWidth: Dp
- get() = with(density) { constraints.minWidth.toDp() }
- override val maxWidth: Dp
- get() = with(density) { constraints.maxWidth.toDp() }
- override val minHeight: Dp
- get() = with(density) { constraints.minHeight.toDp() }
- override val maxHeight: Dp
- get() = with(density) { constraints.maxHeight.toDp() }
- }
-}
\ No newline at end of file
+private data class WithConstraintsScopeImpl(
+ private val density: Density,
+ override val constraints: Constraints,
+ override val layoutDirection: LayoutDirection
+) : WithConstraintsScope {
+ override val minWidth: Dp
+ get() = with(density) { constraints.minWidth.toDp() }
+ override val maxWidth: Dp
+ get() = with(density) { constraints.maxWidth.toDp() }
+ override val minHeight: Dp
+ get() = with(density) { constraints.minHeight.toDp() }
+ override val maxHeight: Dp
+ get() = with(density) { constraints.maxHeight.toDp() }
+}
diff --git a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutNode.kt b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutNode.kt
index 89df797..cb2b84e 100644
--- a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutNode.kt
+++ b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/LayoutNode.kt
@@ -16,6 +16,7 @@
package androidx.ui.core
import androidx.compose.collection.ExperimentalCollectionApi
+import androidx.compose.collection.MutableVector
import androidx.compose.collection.mutableVectorOf
import androidx.ui.core.LayoutNode.LayoutState.Measuring
import androidx.ui.core.LayoutNode.LayoutState.NeedsRelayout
@@ -68,18 +69,81 @@
ExperimentalLayoutNodeApi::class
)
class LayoutNode : Measurable, Remeasurement {
- private val _children = mutableVectorOf<LayoutNode>()
+
+ constructor() : this(false)
+
+ internal constructor(isVirtual: Boolean) {
+ this.isVirtual = isVirtual
+ }
+
+ // Virtual LayoutNode is the temporary concept allows us to a node which is not a real node,
+ // but just a holder for its children - allows us to combine some children into something we
+ // can subcompose in(LayoutNode) without being required to define it as a real layout - we
+ // don't want to define the layout strategy for such nodes, instead the children of the
+ // virtual nodes will be threated as the direct children of the virtual node parent.
+ // This whole concept will be replaced with a proper subcomposition logic which allows to
+ // subcompose multiple times into the same LayoutNode and define offsets.
+
+ private val isVirtual: Boolean
+
+ private var virtualChildrenCount = 0
+
+ // the list of nodes containing the virtual children as is
+ private val _foldedChildren = mutableVectorOf<LayoutNode>()
+ internal val foldedChildren: List<LayoutNode> get() = _foldedChildren.asMutableList()
+
+ // the list of nodes where the virtual children are unfolded (their children are represented
+ // as our direct children)
+ private val _unfoldedChildren = mutableVectorOf<LayoutNode>()
+
+ private fun recreateUnfoldedChildrenIfDirty() {
+ if (unfoldedVirtualChildrenListDirty) {
+ unfoldedVirtualChildrenListDirty = false
+ _unfoldedChildren.clear()
+ _foldedChildren.forEach {
+ if (it.isVirtual) {
+ _unfoldedChildren.addAll(it._children)
+ } else {
+ _unfoldedChildren.add(it)
+ }
+ }
+ }
+ }
+
+ // when the list of our children is modified it will be set to true if we are a virtual node
+ // or it will be set to true on a parent if the parent is a virtual node
+ private var unfoldedVirtualChildrenListDirty = false
+ private fun invalidateUnfoldedVirtualChildren() {
+ if (virtualChildrenCount > 0) {
+ unfoldedVirtualChildrenListDirty = true
+ }
+ if (isVirtual) {
+ parent?.unfoldedVirtualChildrenListDirty = true
+ }
+ }
+
+ private val _children: MutableVector<LayoutNode>
+ get() = if (virtualChildrenCount == 0) {
+ _foldedChildren
+ } else {
+ recreateUnfoldedChildrenIfDirty()
+ _unfoldedChildren
+ }
/**
* The children of this LayoutNode, controlled by [insertAt], [move], and [removeAt].
*/
- val children: List<LayoutNode> = _children.asMutableList()
+ val children: List<LayoutNode> get() = _children.asMutableList()
/**
* The parent node in the LayoutNode hierarchy. This is `null` when the `LayoutNode`
* is attached (has an [owner]) and is the root of the tree or has not had [add] called for it.
*/
var parent: LayoutNode? = null
+ get() {
+ val parent = field
+ return if (parent != null && parent.isVirtual) parent.parent else parent
+ }
private set
/**
@@ -96,8 +160,7 @@
/**
* The layout state the node is currently in.
*/
- var layoutState = Ready
- internal set
+ internal var layoutState = Ready
internal val wasMeasuredDuringThisIteration: Boolean
get() = requireOwner().measureIteration == outerMeasurablePlaceable.measureIteration
@@ -124,7 +187,13 @@
}
instance.parent = this
- _children.add(index, instance)
+ _foldedChildren.add(index, instance)
+
+ if (instance.isVirtual) {
+ require(!isVirtual) { "Virtual LayoutNode can't be added into a virtual parent" }
+ virtualChildrenCount++
+ }
+ invalidateUnfoldedVirtualChildren()
instance.outerLayoutNodeWrapper.wrappedBy = innerLayoutNodeWrapper
@@ -143,7 +212,7 @@
}
val attached = owner != null
for (i in index + count - 1 downTo index) {
- val child = _children.removeAt(i)
+ val child = _foldedChildren.removeAt(i)
if (DebugChanges) {
println("$child removed from $this at index $i")
}
@@ -152,6 +221,11 @@
child.detach()
}
child.parent = null
+
+ if (child.isVirtual) {
+ virtualChildrenCount--
+ }
+ invalidateUnfoldedVirtualChildren()
}
}
@@ -160,14 +234,17 @@
*/
fun removeAll() {
val attached = owner != null
- for (i in _children.size - 1 downTo 0) {
- val child = _children[i]
+ for (i in _foldedChildren.size - 1 downTo 0) {
+ val child = _foldedChildren[i]
if (attached) {
child.detach()
}
child.parent = null
}
- _children.clear()
+ _foldedChildren.clear()
+
+ virtualChildrenCount = 0
+ invalidateUnfoldedVirtualChildren()
}
/**
@@ -186,15 +263,16 @@
// if "from" is after "to," the from index moves because we're inserting before it
val fromIndex = if (from > to) from + i else from
val toIndex = if (from > to) to + i else to + count - 2
- val child = _children.removeAt(fromIndex)
+ val child = _foldedChildren.removeAt(fromIndex)
if (DebugChanges) {
println("$child moved in $this from index $fromIndex to $toIndex")
}
- _children.add(toIndex, child)
+ _foldedChildren.add(toIndex, child)
}
+ invalidateUnfoldedVirtualChildren()
requestRemeasure()
}
@@ -213,7 +291,7 @@
this.owner = owner
this.depth = (parent?.depth ?: -1) + 1
owner.onAttach(this)
- _children.forEach { child ->
+ _foldedChildren.forEach { child ->
child.attach(owner)
}
@@ -249,7 +327,7 @@
owner.onDetach(this)
this.owner = null
depth = 0
- _children.forEach { child ->
+ _foldedChildren.forEach { child ->
child.detach()
}
}
@@ -271,7 +349,7 @@
}
override fun toString(): String {
- return "${simpleIdentityToString(this, null)} children: ${_children.size} " +
+ return "${simpleIdentityToString(this, null)} children: ${children.size} " +
"measureBlocks: $measureBlocks"
}
@@ -520,6 +598,9 @@
var modifier: Modifier = Modifier
set(value) {
if (value == field) return
+ if (modifier != Modifier) {
+ require(!isVirtual) { "Modifiers are not supported on virtual LayoutNodes" }
+ }
field = value
val invalidateParentLayer = shouldInvalidateParentLayer()
@@ -1086,7 +1167,7 @@
/**
* Describes the current state the [LayoutNode] is in.
*/
- enum class LayoutState {
+ internal enum class LayoutState {
/**
* Request remeasure was called on the node.
*/
diff --git a/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/SubcomposeLayout.kt b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/SubcomposeLayout.kt
new file mode 100644
index 0000000..fd62820
--- /dev/null
+++ b/ui/ui-core/src/commonMain/kotlin/androidx/ui/core/SubcomposeLayout.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2020 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.core
+
+import androidx.compose.Applier
+import androidx.compose.Composable
+import androidx.compose.ComposeCompilerApi
+import androidx.compose.Composition
+import androidx.compose.CompositionLifecycleObserver
+import androidx.compose.CompositionReference
+import androidx.compose.ExperimentalComposeApi
+import androidx.compose.Recomposer
+import androidx.compose.compositionReference
+import androidx.compose.currentComposer
+import androidx.compose.emit
+import androidx.compose.remember
+import androidx.ui.core.LayoutNode.LayoutState.LayingOut
+import androidx.ui.core.LayoutNode.LayoutState.Measuring
+import androidx.ui.core.MeasureScope.MeasureResult
+import androidx.ui.util.fastForEach
+
+@RequiresOptIn(
+ "This is an experimental API for being able to perform subcomposition during the " +
+ "measuring. API is likely to change before becoming stable."
+)
+annotation class ExperimentalSubcomposeLayoutApi
+
+/**
+ * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
+ * for example to use the values calculated during the measurement as params for the composition
+ * of the children.
+ *
+ * Possible use cases:
+ * * You need to know the constraints passed by the parent during the composition and can't solve
+ * your use case with just custom [Layout] or [LayoutModifier]. See [WithConstraints].
+ * * You want to use the size of one child during the composition of the second child. Example is
+ * using the sizes of the tabs in TabRow as a input in tabs indicator composable
+ * * You want to compose your items lazily based on the available size. For example you have a
+ * list of 100 items and instead of composing all of them you only compose the ones which are
+ * currently visible(say 5 of them) and compose next items when the component is scrolled.
+ *
+ * @sample androidx.ui.core.samples.SubcomposeLayoutSample
+ *
+ * @param modifier [Modifier] to apply for the layout.
+ * @param measureBlock Measure block which provides ability to subcompose during the measuring.
+ */
+@Composable
+@OptIn(ExperimentalLayoutNodeApi::class, ExperimentalComposeApi::class, ComposeCompilerApi::class)
+@ExperimentalSubcomposeLayoutApi
+fun <T> SubcomposeLayout(
+ modifier: Modifier = Modifier,
+ measureBlock: SubcomposeMeasureScope<T>.(Constraints) -> MeasureResult
+) {
+ val state = remember { SubcomposeLayoutState<T>() }
+ // TODO(lelandr): refactor these APIs so that recomposer isn't necessary
+ state.recomposer = currentComposer.recomposer
+ state.compositionRef = compositionReference()
+
+ val materialized = currentComposer.materialize(modifier)
+ emit<LayoutNode, Applier<Any>>(
+ ctor = LayoutEmitHelper.constructor,
+ update = {
+ set(Unit, state.setRoot)
+ set(materialized, LayoutEmitHelper.setModifier)
+ set(measureBlock, state.setMeasureBlock)
+ }
+ )
+
+ state.subcomposeIfRemeasureNotScheduled()
+}
+
+/**
+ * The receiver scope of a [SubcomposeLayout]'s measure lambda which adds ability to dynamically
+ * subcompose a content during the measuring on top of the features provided by [MeasureScope].
+ */
+@ExperimentalSubcomposeLayoutApi
+abstract class SubcomposeMeasureScope<T> : MeasureScope() {
+ /**
+ * Performs subcomposition of the provided [content] with given [slotId].
+ *
+ * @param slotId unique id which represents the slot we are composing into. If you have fixed
+ * amount or slots you can use enums as slot ids, or if you have a list of items maybe an
+ * index in the list or some other unique key can work. To be able to correctly match the
+ * content between remeasures you should provide the object which is equals to the one you
+ * used during the previous measuring.
+ * @param content the composable content which defines the slot. It could emit multiple
+ * layouts, in this case the returned list of [Measurable]s will have multiple elements.
+ */
+ abstract fun subcompose(slotId: T, content: @Composable () -> Unit): List<Measurable>
+}
+
+@OptIn(ExperimentalLayoutNodeApi::class, ExperimentalSubcomposeLayoutApi::class)
+private class SubcomposeLayoutState<T> : SubcomposeMeasureScope<T>(),
+ CompositionLifecycleObserver {
+ // Values set during the composition
+ var recomposer: Recomposer? = null
+ var compositionRef: CompositionReference? = null
+
+ // MeasureScope delegation
+ override var layoutDirection: LayoutDirection = LayoutDirection.Rtl
+ override var density: Float = 0f
+ override var fontScale: Float = 0f
+
+ // Pre-allocated lambdas to update LayoutNode
+ val setRoot: LayoutNode.(Unit) -> Unit = { root = this }
+ val setMeasureBlock:
+ LayoutNode.(SubcomposeMeasureScope<T>.(Constraints) -> MeasureResult) -> Unit =
+ { measureBlocks = createMeasureBlocks(it) }
+
+ // inner state
+ private var root: LayoutNode? = null
+ private var currentIndex = 0
+ private val nodeToNodeState = mutableMapOf<LayoutNode, NodeState<T>>()
+ private val slodIdToNode = mutableMapOf<T, LayoutNode>()
+
+ override fun subcompose(slotId: T, content: @Composable () -> Unit): List<Measurable> {
+ val root = root!!
+ val layoutState = root.layoutState
+ check(layoutState == Measuring || layoutState == LayingOut) {
+ "subcompose can only be used inside the measure or layout blocks"
+ }
+
+ val node = slodIdToNode.getOrPut(slotId) {
+ LayoutNode(isVirtual = true).also {
+ root.insertAt(currentIndex, it)
+ }
+ }
+
+ val itemIndex = root.foldedChildren.indexOf(node)
+ if (itemIndex < currentIndex) {
+ throw IllegalArgumentException(
+ "$slotId was already used with subcompose during this measuring pass"
+ )
+ }
+ if (currentIndex != itemIndex) {
+ root.move(itemIndex, currentIndex, 1)
+ }
+ currentIndex++
+
+ val nodeState = nodeToNodeState.getOrPut(node) {
+ NodeState(slotId, content)
+ }
+ nodeState.content = content
+ subcompose(node, nodeState)
+ return node.children
+ }
+
+ fun subcomposeIfRemeasureNotScheduled() {
+ val root = root!!
+ if (root.layoutState != LayoutNode.LayoutState.NeedsRemeasure) {
+ root.foldedChildren.fastForEach {
+ subcompose(it, nodeToNodeState.getValue(it))
+ }
+ }
+ }
+
+ private fun subcompose(node: LayoutNode, nodeState: NodeState<T>) {
+ node.ignoreModelReads {
+ val content = nodeState.content
+ nodeState.composition = subcomposeInto(node, recomposer!!, compositionRef!!) {
+ content()
+ }
+ }
+ }
+
+ private fun disposeAfterIndex(currentIndex: Int) {
+ val root = root!!
+ for (i in currentIndex until root.foldedChildren.size) {
+ val node = root.foldedChildren[i]
+ val nodeState = nodeToNodeState.remove(node)!!
+ nodeState.composition!!.dispose()
+ slodIdToNode.remove(nodeState.slotId)
+ }
+ root.removeAt(currentIndex, root.foldedChildren.size - currentIndex)
+ }
+
+ private fun createMeasureBlocks(
+ block: SubcomposeMeasureScope<T>.(Constraints) -> MeasureResult
+ ): LayoutNode.MeasureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks(
+ error = "Intrinsic measurements are not currently supported by SubcomposeLayout"
+ ) {
+ override fun measure(
+ measureScope: MeasureScope,
+ measurables: List<Measurable>,
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): MeasureResult {
+ this@SubcomposeLayoutState.layoutDirection = measureScope.layoutDirection
+ this@SubcomposeLayoutState.density = measureScope.density
+ this@SubcomposeLayoutState.fontScale = measureScope.fontScale
+ currentIndex = 0
+ val result = block(constraints)
+ val indexAfterMeasure = currentIndex
+ return object : MeasureResult {
+ override val width: Int
+ get() = result.width
+ override val height: Int
+ get() = result.height
+ override val alignmentLines: Map<AlignmentLine, Int>
+ get() = result.alignmentLines
+
+ override fun placeChildren(layoutDirection: LayoutDirection) {
+ currentIndex = indexAfterMeasure
+ result.placeChildren(layoutDirection)
+ disposeAfterIndex(currentIndex)
+ }
+ }
+ }
+ }
+
+ override fun onEnter() {
+ // do nothing
+ }
+
+ override fun onLeave() {
+ nodeToNodeState.values.forEach {
+ it.composition!!.dispose()
+ }
+ nodeToNodeState.clear()
+ slodIdToNode.clear()
+ }
+
+ private class NodeState<T>(
+ val slotId: T,
+ var content: @Composable () -> Unit,
+ var composition: Composition? = null
+ )
+}