[go: nahoru, domu]

blob: fd628209ebd86dd9db977de51a53088f4dbcaace [file] [log] [blame]
Andrey Kulikov2ca07012020-07-13 16:14:03 +01001/*
2 * Copyright 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.ui.core
18
19import androidx.compose.Applier
20import androidx.compose.Composable
21import androidx.compose.ComposeCompilerApi
22import androidx.compose.Composition
23import androidx.compose.CompositionLifecycleObserver
24import androidx.compose.CompositionReference
25import androidx.compose.ExperimentalComposeApi
26import androidx.compose.Recomposer
27import androidx.compose.compositionReference
28import androidx.compose.currentComposer
29import androidx.compose.emit
30import androidx.compose.remember
31import androidx.ui.core.LayoutNode.LayoutState.LayingOut
32import androidx.ui.core.LayoutNode.LayoutState.Measuring
33import androidx.ui.core.MeasureScope.MeasureResult
34import androidx.ui.util.fastForEach
35
36@RequiresOptIn(
37 "This is an experimental API for being able to perform subcomposition during the " +
38 "measuring. API is likely to change before becoming stable."
39)
40annotation class ExperimentalSubcomposeLayoutApi
41
42/**
43 * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
44 * for example to use the values calculated during the measurement as params for the composition
45 * of the children.
46 *
47 * Possible use cases:
48 * * You need to know the constraints passed by the parent during the composition and can't solve
49 * your use case with just custom [Layout] or [LayoutModifier]. See [WithConstraints].
50 * * You want to use the size of one child during the composition of the second child. Example is
51 * using the sizes of the tabs in TabRow as a input in tabs indicator composable
52 * * You want to compose your items lazily based on the available size. For example you have a
53 * list of 100 items and instead of composing all of them you only compose the ones which are
54 * currently visible(say 5 of them) and compose next items when the component is scrolled.
55 *
56 * @sample androidx.ui.core.samples.SubcomposeLayoutSample
57 *
58 * @param modifier [Modifier] to apply for the layout.
59 * @param measureBlock Measure block which provides ability to subcompose during the measuring.
60 */
61@Composable
62@OptIn(ExperimentalLayoutNodeApi::class, ExperimentalComposeApi::class, ComposeCompilerApi::class)
63@ExperimentalSubcomposeLayoutApi
64fun <T> SubcomposeLayout(
65 modifier: Modifier = Modifier,
66 measureBlock: SubcomposeMeasureScope<T>.(Constraints) -> MeasureResult
67) {
68 val state = remember { SubcomposeLayoutState<T>() }
69 // TODO(lelandr): refactor these APIs so that recomposer isn't necessary
70 state.recomposer = currentComposer.recomposer
71 state.compositionRef = compositionReference()
72
73 val materialized = currentComposer.materialize(modifier)
74 emit<LayoutNode, Applier<Any>>(
75 ctor = LayoutEmitHelper.constructor,
76 update = {
77 set(Unit, state.setRoot)
78 set(materialized, LayoutEmitHelper.setModifier)
79 set(measureBlock, state.setMeasureBlock)
80 }
81 )
82
83 state.subcomposeIfRemeasureNotScheduled()
84}
85
86/**
87 * The receiver scope of a [SubcomposeLayout]'s measure lambda which adds ability to dynamically
88 * subcompose a content during the measuring on top of the features provided by [MeasureScope].
89 */
90@ExperimentalSubcomposeLayoutApi
91abstract class SubcomposeMeasureScope<T> : MeasureScope() {
92 /**
93 * Performs subcomposition of the provided [content] with given [slotId].
94 *
95 * @param slotId unique id which represents the slot we are composing into. If you have fixed
96 * amount or slots you can use enums as slot ids, or if you have a list of items maybe an
97 * index in the list or some other unique key can work. To be able to correctly match the
98 * content between remeasures you should provide the object which is equals to the one you
99 * used during the previous measuring.
100 * @param content the composable content which defines the slot. It could emit multiple
101 * layouts, in this case the returned list of [Measurable]s will have multiple elements.
102 */
103 abstract fun subcompose(slotId: T, content: @Composable () -> Unit): List<Measurable>
104}
105
106@OptIn(ExperimentalLayoutNodeApi::class, ExperimentalSubcomposeLayoutApi::class)
107private class SubcomposeLayoutState<T> : SubcomposeMeasureScope<T>(),
108 CompositionLifecycleObserver {
109 // Values set during the composition
110 var recomposer: Recomposer? = null
111 var compositionRef: CompositionReference? = null
112
113 // MeasureScope delegation
114 override var layoutDirection: LayoutDirection = LayoutDirection.Rtl
115 override var density: Float = 0f
116 override var fontScale: Float = 0f
117
118 // Pre-allocated lambdas to update LayoutNode
119 val setRoot: LayoutNode.(Unit) -> Unit = { root = this }
120 val setMeasureBlock:
121 LayoutNode.(SubcomposeMeasureScope<T>.(Constraints) -> MeasureResult) -> Unit =
122 { measureBlocks = createMeasureBlocks(it) }
123
124 // inner state
125 private var root: LayoutNode? = null
126 private var currentIndex = 0
127 private val nodeToNodeState = mutableMapOf<LayoutNode, NodeState<T>>()
128 private val slodIdToNode = mutableMapOf<T, LayoutNode>()
129
130 override fun subcompose(slotId: T, content: @Composable () -> Unit): List<Measurable> {
131 val root = root!!
132 val layoutState = root.layoutState
133 check(layoutState == Measuring || layoutState == LayingOut) {
134 "subcompose can only be used inside the measure or layout blocks"
135 }
136
137 val node = slodIdToNode.getOrPut(slotId) {
138 LayoutNode(isVirtual = true).also {
139 root.insertAt(currentIndex, it)
140 }
141 }
142
143 val itemIndex = root.foldedChildren.indexOf(node)
144 if (itemIndex < currentIndex) {
145 throw IllegalArgumentException(
146 "$slotId was already used with subcompose during this measuring pass"
147 )
148 }
149 if (currentIndex != itemIndex) {
150 root.move(itemIndex, currentIndex, 1)
151 }
152 currentIndex++
153
154 val nodeState = nodeToNodeState.getOrPut(node) {
155 NodeState(slotId, content)
156 }
157 nodeState.content = content
158 subcompose(node, nodeState)
159 return node.children
160 }
161
162 fun subcomposeIfRemeasureNotScheduled() {
163 val root = root!!
164 if (root.layoutState != LayoutNode.LayoutState.NeedsRemeasure) {
165 root.foldedChildren.fastForEach {
166 subcompose(it, nodeToNodeState.getValue(it))
167 }
168 }
169 }
170
171 private fun subcompose(node: LayoutNode, nodeState: NodeState<T>) {
172 node.ignoreModelReads {
173 val content = nodeState.content
174 nodeState.composition = subcomposeInto(node, recomposer!!, compositionRef!!) {
175 content()
176 }
177 }
178 }
179
180 private fun disposeAfterIndex(currentIndex: Int) {
181 val root = root!!
182 for (i in currentIndex until root.foldedChildren.size) {
183 val node = root.foldedChildren[i]
184 val nodeState = nodeToNodeState.remove(node)!!
185 nodeState.composition!!.dispose()
186 slodIdToNode.remove(nodeState.slotId)
187 }
188 root.removeAt(currentIndex, root.foldedChildren.size - currentIndex)
189 }
190
191 private fun createMeasureBlocks(
192 block: SubcomposeMeasureScope<T>.(Constraints) -> MeasureResult
193 ): LayoutNode.MeasureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks(
194 error = "Intrinsic measurements are not currently supported by SubcomposeLayout"
195 ) {
196 override fun measure(
197 measureScope: MeasureScope,
198 measurables: List<Measurable>,
199 constraints: Constraints,
200 layoutDirection: LayoutDirection
201 ): MeasureResult {
202 this@SubcomposeLayoutState.layoutDirection = measureScope.layoutDirection
203 this@SubcomposeLayoutState.density = measureScope.density
204 this@SubcomposeLayoutState.fontScale = measureScope.fontScale
205 currentIndex = 0
206 val result = block(constraints)
207 val indexAfterMeasure = currentIndex
208 return object : MeasureResult {
209 override val width: Int
210 get() = result.width
211 override val height: Int
212 get() = result.height
213 override val alignmentLines: Map<AlignmentLine, Int>
214 get() = result.alignmentLines
215
216 override fun placeChildren(layoutDirection: LayoutDirection) {
217 currentIndex = indexAfterMeasure
218 result.placeChildren(layoutDirection)
219 disposeAfterIndex(currentIndex)
220 }
221 }
222 }
223 }
224
225 override fun onEnter() {
226 // do nothing
227 }
228
229 override fun onLeave() {
230 nodeToNodeState.values.forEach {
231 it.composition!!.dispose()
232 }
233 nodeToNodeState.clear()
234 slodIdToNode.clear()
235 }
236
237 private class NodeState<T>(
238 val slotId: T,
239 var content: @Composable () -> Unit,
240 var composition: Composition? = null
241 )
242}