Merge "Propogate invalidations through CompositionReferences properly" into androidx-master-dev
diff --git a/compose/runtime/src/main/java/androidx/compose/Compose.kt b/compose/runtime/src/main/java/androidx/compose/Compose.kt
index 00e85d0..44f1981 100644
--- a/compose/runtime/src/main/java/androidx/compose/Compose.kt
+++ b/compose/runtime/src/main/java/androidx/compose/Compose.kt
@@ -33,10 +33,10 @@
object Compose {
private class Root : Component() {
- @Suppress("DEPRECATION")
- fun update() = recomposeSync()
+ fun update() = composer.compose()
lateinit var composable: @Composable() () -> Unit
+ lateinit var composer: CompositionContext
@Suppress("PLUGIN_ERROR")
override fun compose() {
val cc = currentComposerNonNull
@@ -127,15 +127,18 @@
root = Root()
root.composable = composable
setRoot(container, root)
- CompositionContext.prepare(
+ val cc = CompositionContext.prepare(
container.context,
container,
root,
parent
- ).compose()
+ )
+ root.composer = cc
+ root.update()
+ return cc
} else {
root.composable = composable
- root.recomposeCallback?.invoke(true)
+ root.update()
}
return null
}
@@ -193,10 +196,12 @@
root = Root()
root.composable = composable
setRoot(container, root)
- CompositionContext.prepare(context, container, root, parent).compose()
+ val cc = CompositionContext.prepare(context, container, root, parent)
+ root.composer = cc
+ root.update()
} else {
root.composable = composable
- root.recomposeCallback?.invoke(true)
+ root.update()
}
}
diff --git a/compose/runtime/src/main/java/androidx/compose/Composer.kt b/compose/runtime/src/main/java/androidx/compose/Composer.kt
index d5c6013..0811e53 100644
--- a/compose/runtime/src/main/java/androidx/compose/Composer.kt
+++ b/compose/runtime/src/main/java/androidx/compose/Composer.kt
@@ -161,7 +161,7 @@
var location: Int
)
-internal class RecomposeScope(val compose: (invalidate: (sync: Boolean) -> Unit) -> Unit) {
+internal class RecomposeScope(var compose: (invalidate: (sync: Boolean) -> Unit) -> Unit) {
var anchor: Anchor? = null
var invalidate: ((sync: Boolean) -> Unit)? = null
val valid: Boolean get() = anchor?.valid ?: false
@@ -196,6 +196,7 @@
private val invalidateStack = Stack<RecomposeScope>()
internal var parentReference: CompositionReference? = null
+ internal var isComposing = false
private val changesAppliedObservers = mutableListOf<() -> Unit>()
@@ -596,7 +597,7 @@
invalidateStack.let { if (it.isNotEmpty()) it.peek() else null }
private fun start(key: Any, action: SlotAction) {
- assert(childrenAllowed) { "A call to creadNode(), emitNode() or useNode() expected" }
+ assert(childrenAllowed) { "A call to createNode(), emitNode() or useNode() expected" }
if (pending == null) {
val slotKey = slots.next()
if (slotKey == key) {
@@ -905,6 +906,8 @@
}
private fun recomposeComponentRange(start: Int, end: Int) {
+ val wasComposing = isComposing
+ isComposing = true
var recomposed = false
var firstInRange = invalidations.firstInRange(start, end)
@@ -937,16 +940,26 @@
recordSkip(START_GROUP)
slots.skipGroup()
}
+ isComposing = wasComposing
}
private fun invalidate(scope: RecomposeScope, sync: Boolean) {
val location = scope.anchor?.location(slotTable) ?: return
assert(location >= 0) { "Invalid anchor" }
invalidations.insertIfMissing(location, scope)
- if (sync) {
- recomposer.recomposeSync(this)
+ if (isComposing && location > slots.current) {
+ // if we are invalidating a scope that is going to be traversed during this
+ // composition, we don't want to schedule a recomposition
+ return
+ }
+ if (parentReference != null) {
+ parentReference?.invalidate(sync)
} else {
- recomposer.scheduleRecompose(this)
+ if (sync) {
+ recomposer.recomposeSync(this)
+ } else {
+ recomposer.scheduleRecompose(this)
+ }
}
}
@@ -985,6 +998,7 @@
slots.startGroup()
@Suppress("UNCHECKED_CAST")
val scope = slots.next() as RecomposeScope
+ scope.compose = compose
invalidateStack.push(scope)
recordStart(START_GROUP)
skipValue()
@@ -1211,7 +1225,7 @@
override fun <T> invalidateConsumers(key: Ambient<T>) {
// need to mark the recompose scope that created the reference as invalid
- invalidate()
+ invalidate(false)
// loop through every child composer
for (composer in composers) {
@@ -1227,11 +1241,11 @@
composers.add(composer)
}
- override fun invalidate() {
+ override fun invalidate(sync: Boolean) {
// continue invalidating up the spine of AmbientReferences
- parentReference?.invalidate()
+ parentReference?.invalidate(sync)
- scope.invalidate?.invoke(false)
+ invalidate(scope, sync)
}
override fun <T> getAmbient(key: Ambient<T>): T {
diff --git a/compose/runtime/src/main/java/androidx/compose/CompositionReference.kt b/compose/runtime/src/main/java/androidx/compose/CompositionReference.kt
index 8ba51d8..e6335a2 100644
--- a/compose/runtime/src/main/java/androidx/compose/CompositionReference.kt
+++ b/compose/runtime/src/main/java/androidx/compose/CompositionReference.kt
@@ -28,7 +28,7 @@
*/
interface CompositionReference {
fun <T> getAmbient(key: Ambient<T>): T
- fun invalidate()
+ fun invalidate(sync: Boolean)
fun <T> invalidateConsumers(key: Ambient<T>)
fun <N> registerComposer(composer: Composer<N>)
}
\ No newline at end of file
diff --git a/compose/runtime/src/main/java/androidx/compose/Key.kt b/compose/runtime/src/main/java/androidx/compose/Key.kt
index 4df25d3..5557f00 100644
--- a/compose/runtime/src/main/java/androidx/compose/Key.kt
+++ b/compose/runtime/src/main/java/androidx/compose/Key.kt
@@ -46,7 +46,7 @@
* Key(parent.id) {
* User(user=child)
* User(user=parent)
-* }
+ * }
* }
*
* This example assumes that `parent.id` is a unique key for each item in the collection,
diff --git a/compose/runtime/src/main/java/androidx/compose/Recomposer.kt b/compose/runtime/src/main/java/androidx/compose/Recomposer.kt
index 1fa38c1..094d849 100644
--- a/compose/runtime/src/main/java/androidx/compose/Recomposer.kt
+++ b/compose/runtime/src/main/java/androidx/compose/Recomposer.kt
@@ -49,10 +49,12 @@
private val composers = mutableSetOf<Composer<*>>()
private fun recompose(component: Component, composer: Composer<*>) {
- val previousComposing = isComposing
composer.runWithCurrent {
+ val previousComposing = isComposing
+ val composerWasComposing = composer.isComposing
try {
isComposing = true
+ composer.isComposing = true
trace("Compose:recompose") {
composer.startRoot()
composer.startGroup(invocation)
@@ -64,6 +66,7 @@
FrameManager.nextFrame()
} finally {
isComposing = previousComposing
+ composer.isComposing = composerWasComposing
}
}
}
diff --git a/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt b/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt
index c46ad69..710680e 100644
--- a/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt
+++ b/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt
@@ -15,6 +15,7 @@
*/
package androidx.ui.core.test
+import android.app.Activity
import android.graphics.Bitmap
import android.os.Build
import android.os.Handler
@@ -44,10 +45,14 @@
import androidx.ui.painting.Paint
import androidx.compose.Children
import androidx.compose.Composable
+import androidx.compose.Compose
import androidx.compose.Model
import androidx.compose.composer
import androidx.compose.setContent
import androidx.test.filters.SdkSuppress
+import androidx.ui.core.ContextAmbient
+import androidx.ui.core.Density
+import androidx.ui.core.DensityAmbient
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -391,6 +396,20 @@
}
}
+ // TODO(lmr): refactor to use the globally provided one when it lands
+ private fun Activity.compose(composable: @Composable() () -> Unit) {
+ val root = AndroidCraneView(this)
+
+ setContentView(root)
+ Compose.composeInto(root.root, context = this) {
+ ContextAmbient.Provider(value = this) {
+ DensityAmbient.Provider(value = Density(this)) {
+ composable()
+ }
+ }
+ }
+ }
+
// When a child's measure() is done within the layout, it should not affect the parent's
// size. The parent's layout shouldn't be called when the child's size changes
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -404,38 +423,36 @@
val layoutLatch = CountDownLatch(2)
runOnUiThread {
- activity.setContent {
- CraneWrapper {
- Draw { canvas, parentSize ->
- val paint = Paint()
- paint.color = model.outerColor
- canvas.drawRect(parentSize.toRect(), paint)
- }
- Layout(children = {
- AtLeastSize(size = model.size) {
- Draw { canvas, parentSize ->
- drawLatch.countDown()
- val paint = Paint()
- paint.color = model.innerColor
- canvas.drawRect(parentSize.toRect(), paint)
- }
- }
- }, layoutBlock = { measurables, constraints ->
- measureCalls++
- layout(30.ipx, 30.ipx) {
- layoutCalls++
- layoutLatch.countDown()
- val placeable = measurables[0].measure(constraints)
- placeable.place(
- (30.ipx - placeable.width) / 2,
- (30.ipx - placeable.height) / 2
- )
- }
- })
+ activity.compose {
+ Draw { canvas, parentSize ->
+ val paint = Paint()
+ paint.color = model.outerColor
+ canvas.drawRect(parentSize.toRect(), paint)
}
+ Layout(children = {
+ AtLeastSize(size = model.size) {
+ Draw { canvas, parentSize ->
+ drawLatch.countDown()
+ val paint = Paint()
+ paint.color = model.innerColor
+ canvas.drawRect(parentSize.toRect(), paint)
+ }
+ }
+ }, layoutBlock = { measurables, constraints ->
+ measureCalls++
+ layout(30.ipx, 30.ipx) {
+ layoutCalls++
+ layoutLatch.countDown()
+ val placeable = measurables[0].measure(constraints)
+ placeable.place(
+ (30.ipx - placeable.width) / 2,
+ (30.ipx - placeable.height) / 2
+ )
+ }
+ })
}
}
- assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
+ assertTrue(layoutLatch.await(10, TimeUnit.SECONDS))
validateSquareColors(outerColor = blue, innerColor = white, size = 10)
diff --git a/ui/framework/src/main/java/androidx/ui/core/Text.kt b/ui/framework/src/main/java/androidx/ui/core/Text.kt
index 03ca3e0..bd55e4b 100644
--- a/ui/framework/src/main/java/androidx/ui/core/Text.kt
+++ b/ui/framework/src/main/java/androidx/ui/core/Text.kt
@@ -36,6 +36,7 @@
import androidx.compose.onCommit
import androidx.compose.state
import androidx.compose.memo
+import androidx.compose.onDispose
import androidx.compose.unaryPlus
private val DefaultTextAlign: TextAlign = TextAlign.Start
@@ -47,15 +48,7 @@
/** The default selection color if none is specified. */
private val DefaultSelectionColor = Color(0x6633B5E5)
-/**
- * Text Widget Crane version.
- *
- * The Text widget displays text that uses multiple different styles. The text to display is
- * described using a tree of [TextSpan] objects, each of which has an associated style that is used
- * for that subtree. The text might break across multiple lines or might all be displayed on the
- * same line depending on the layout constraints.
- */
-// TODO(migration/qqd): Add tests when text widget system is mature and testable.
+
@Composable
fun Text(
/** How the text should be aligned horizontally. */
@@ -89,6 +82,71 @@
*/
@Children child: @Composable TextSpanScope.() -> Unit
) {
+ val rootTextSpan = +memo { TextSpan() }
+ val ref = +compositionReference()
+ compose(rootTextSpan, ref, child)
+ +onDispose { disposeComposition(rootTextSpan, ref) }
+
+ // TODO This is a temporary workaround due to lack of textStyle parameter of Text.
+ val textSpan = if (rootTextSpan.children.size == 1) {
+ rootTextSpan.children[0]
+ } else {
+ rootTextSpan
+ }
+ Text(
+ textAlign = textAlign,
+ textDirection = textDirection,
+ softWrap = softWrap,
+ overflow = overflow,
+ textScaleFactor = textScaleFactor,
+ maxLines = maxLines,
+ selectionColor = selectionColor,
+ text = textSpan
+ )
+}
+
+/**
+ * Text Widget Crane version.
+ *
+ * The Text widget displays text that uses multiple different styles. The text to display is
+ * described using a tree of [TextSpan] objects, each of which has an associated style that is used
+ * for that subtree. The text might break across multiple lines or might all be displayed on the
+ * same line depending on the layout constraints.
+ */
+// TODO(migration/qqd): Add tests when text widget system is mature and testable.
+@Composable
+internal fun Text(
+ /** How the text should be aligned horizontally. */
+ textAlign: TextAlign = DefaultTextAlign,
+ /** The directionality of the text. */
+ textDirection: TextDirection = DefaultTextDirection,
+ /**
+ * Whether the text should break at soft line breaks.
+ * If false, the glyphs in the text will be positioned as if there was unlimited horizontal
+ * space.
+ * If [softWrap] is false, [overflow] and [textAlign] may have unexpected effects.
+ */
+ softWrap: Boolean = DefaultSoftWrap,
+ /** How visual overflow should be handled. */
+ overflow: TextOverflow = DefaultOverflow,
+ /** The number of font pixels for each logical pixel. */
+ textScaleFactor: Float = 1.0f,
+ /**
+ * An optional maximum number of lines for the text to span, wrapping if necessary.
+ * If the text exceeds the given number of lines, it will be truncated according to [overflow]
+ * and [softWrap].
+ * The value may be null. If it is not null, then it must be greater than zero.
+ */
+ maxLines: Int? = DefaultMaxLines,
+ /**
+ * The color used to draw selected region.
+ */
+ selectionColor: Color = DefaultSelectionColor,
+ /**
+ * Composable TextSpan attached after [text].
+ */
+ text: TextSpan
+) {
val context = composer.composer.context
val internalSelection = +state<TextSelection?> { null }
val registrar = +ambient(SelectionRegistrarAmbient)
@@ -106,21 +164,10 @@
}
}
- val rootTextSpan = +memo { TextSpan() }
- val ref = +compositionReference()
- compose(rootTextSpan, ref, child)
-
- // TODO This is a temporary workaround due to lack of textStyle parameter of Text.
- val textSpan = if (rootTextSpan.children.size == 1) {
- rootTextSpan.children[0]
- } else {
- rootTextSpan
- }
-
val style = +ambient(CurrentTextStyleAmbient)
- val mergedStyle = style.merge(textSpan.style)
+ val mergedStyle = style.merge(text.style)
// Make a wrapper to avoid modifying the style on the original element
- val styledText = TextSpan(style = mergedStyle, children = mutableListOf(textSpan))
+ val styledText = TextSpan(style = mergedStyle, children = mutableListOf(text))
Semantics(
label = styledText.toPlainText()
@@ -223,10 +270,11 @@
textDirection = textDirection,
softWrap = softWrap,
overflow = overflow,
- maxLines = maxLines
- ) {
- Span(text = text, style = style)
- }
+ textScaleFactor = 1.0f,
+ maxLines = maxLines,
+ selectionColor = DefaultSelectionColor,
+ text = TextSpan(text = text, style = style)
+ )
}
internal val CurrentTextStyleAmbient = Ambient.of<TextStyle>("current text style") {
diff --git a/ui/framework/src/main/java/androidx/ui/core/TextSpanCompose.kt b/ui/framework/src/main/java/androidx/ui/core/TextSpanCompose.kt
index 642b735..d41059777 100644
--- a/ui/framework/src/main/java/androidx/ui/core/TextSpanCompose.kt
+++ b/ui/framework/src/main/java/androidx/ui/core/TextSpanCompose.kt
@@ -34,10 +34,9 @@
* when the [TextSpan] container is composed for the first time.
*/
private class Root : Component() {
- @Suppress("DEPRECATION")
- fun update() = recomposeSync()
-
+ fun update() = composer.compose()
lateinit var scope: TextSpanScope
+ lateinit var composer: CompositionContext
lateinit var composable: @Composable() TextSpanScope.() -> Unit
@Suppress("PLUGIN_ERROR")
override fun compose() {
@@ -86,18 +85,16 @@
lateinit var composer: TextSpanComposer
root = Root()
setRoot(container, root)
-
- val cc = CompositionContext.prepare(root, parent) {
+ root.composer = CompositionContext.prepare(root, parent) {
TextSpanComposer(container, this).also { composer = it }
}
- val scope = TextSpanScope(TextSpanComposition(composer))
-
- root.scope = scope
+ root.scope = TextSpanScope(TextSpanComposition(composer))
root.composable = composable
- cc.compose()
+ root.update()
} else {
root.composable = composable
+
root.update()
}
}
diff --git a/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/SelectionsControlsDemo.kt b/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/SelectionsControlsDemo.kt
index acd52de..0691fbe 100644
--- a/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/SelectionsControlsDemo.kt
+++ b/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/SelectionsControlsDemo.kt
@@ -39,6 +39,7 @@
import androidx.compose.Composable
import androidx.compose.Model
import androidx.compose.composer
+import androidx.compose.memo
import androidx.compose.state
import androidx.compose.unaryPlus
@@ -127,9 +128,9 @@
@Composable
fun CheckboxDemo() {
Column(crossAxisAlignment = CrossAxisAlignment.Start) {
- val state = CheckboxState(Checked)
- val state2 = CheckboxState(Checked)
- val state3 = CheckboxState(Checked)
+ val state = +memo { CheckboxState(Checked) }
+ val state2 = +memo { CheckboxState(Checked) }
+ val state3 = +memo { CheckboxState(Checked) }
fun calcParentState() = parentCheckboxState(state.value, state2.value, state3.value)
val onParentClick = {
val s = if (calcParentState() == Checked) {
diff --git a/ui/material/src/main/java/androidx/ui/material/MaterialTheme.kt b/ui/material/src/main/java/androidx/ui/material/MaterialTheme.kt
index 42fffd3..676cc7b 100644
--- a/ui/material/src/main/java/androidx/ui/material/MaterialTheme.kt
+++ b/ui/material/src/main/java/androidx/ui/material/MaterialTheme.kt
@@ -37,6 +37,7 @@
import androidx.compose.ambient
import androidx.compose.composer
import androidx.compose.effectOf
+import androidx.compose.memo
import androidx.compose.unaryPlus
/**
@@ -223,17 +224,20 @@
@Composable
fun MaterialRippleTheme(@Children children: @Composable() () -> Unit) {
val materialColors = +ambient(Colors)
- val defaultTheme = RippleTheme(
- factory = DefaultRippleEffectFactory,
- colorCallback = { background ->
- if (background == null || background.alpha == 0f ||
- background.luminance() >= 0.5) { // light bg
- materialColors.primary.copy(alpha = 0.12f)
- } else { // dark bg
- Color(0xFFFFFFFF.toInt()).copy(alpha = 0.24f)
+ val defaultTheme = +memo {
+ RippleTheme(
+ factory = DefaultRippleEffectFactory,
+ colorCallback = { background ->
+ if (background == null || background.alpha == 0f ||
+ background.luminance() >= 0.5
+ ) { // light bg
+ materialColors.primary.copy(alpha = 0.12f)
+ } else { // dark bg
+ Color(0xFFFFFFFF.toInt()).copy(alpha = 0.24f)
+ }
}
- }
- )
+ )
+ }
CurrentRippleTheme.Provider(value = defaultTheme, children = children)
}
diff --git a/ui/material/src/main/java/androidx/ui/material/ripple/RippleSurface.kt b/ui/material/src/main/java/androidx/ui/material/ripple/RippleSurface.kt
index fe4e961..f26f84f 100644
--- a/ui/material/src/main/java/androidx/ui/material/ripple/RippleSurface.kt
+++ b/ui/material/src/main/java/androidx/ui/material/ripple/RippleSurface.kt
@@ -18,20 +18,21 @@
import androidx.annotation.CheckResult
import androidx.ui.animation.transitionsEnabled
+import androidx.ui.core.Draw
import androidx.ui.core.LayoutCoordinates
+import androidx.ui.core.OnPositioned
import androidx.ui.core.toRect
import androidx.ui.graphics.Color
import androidx.compose.Ambient
import androidx.compose.Children
import androidx.compose.Composable
-import androidx.compose.Model
-import androidx.compose.composer
+import androidx.compose.Recompose
import androidx.compose.ambient
+import androidx.compose.composer
import androidx.compose.effectOf
+import androidx.compose.invalidate
import androidx.compose.memo
import androidx.compose.unaryPlus
-import androidx.ui.core.Draw
-import androidx.ui.core.OnPositioned
/**
* An interface for creating [RippleEffect]s on a [RippleSurface].
@@ -95,16 +96,19 @@
owner.backgroundColor = color
OnPositioned(onPositioned = { owner._layoutCoordinates = it })
- Draw { canvas, size ->
- // TODO(Andrey) Find a better way to disable ripples when transitions are disabled.
- val transitionsEnabled = transitionsEnabled
- if (owner.effects.isNotEmpty() && transitionsEnabled) {
- canvas.save()
- canvas.clipRect(size.toRect())
- owner.effects.forEach { it.draw(canvas) }
- canvas.restore()
+ Recompose { recompose ->
+ owner.recompose = recompose
+
+ Draw { canvas, size ->
+ // TODO(Andrey) Find a better way to disable ripples when transitions are disabled.
+ val transitionsEnabled = transitionsEnabled
+ if (owner.effects.isNotEmpty() && transitionsEnabled) {
+ canvas.save()
+ canvas.clipRect(size.toRect())
+ owner.effects.forEach { it.draw(canvas) }
+ canvas.restore()
+ }
}
- owner.recomposeModel.registerForRecomposition()
}
CurrentRippleSurface.Provider(value = owner, children = children)
}
@@ -115,14 +119,12 @@
override val layoutCoordinates
get() = _layoutCoordinates
?: throw IllegalStateException("The surface wasn't yet positioned!")
- internal var recomposeModel = RecomposeModel()
+ internal var recompose: () -> Unit = {}
internal var _layoutCoordinates: LayoutCoordinates? = null
internal var effects = mutableListOf<RippleEffect>()
- override fun markNeedsRedraw() {
- recomposeModel.recompose()
- }
+ override fun markNeedsRedraw() = recompose()
override fun addEffect(feature: RippleEffect) {
assert(!feature.debugDisposed)
@@ -137,19 +139,3 @@
markNeedsRedraw()
}
}
-
-// TODO(Andrey: Temporary workaround for the ripple invalidation)
-@Model
-private class RecomposeModel {
-
- private var ticker = 0
-
- fun recompose() {
- ticker++
- }
-
- fun registerForRecomposition() {
- @Suppress("UNUSED_VARIABLE")
- val ticker = ticker
- }
-}