Merge "Take into account layers transformations from outer coordinator" into androidx-main
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
index 1164119..3738cf2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
@@ -1502,4 +1502,38 @@
rule.runOnIdle { assertThat(actualPosition).isEqualTo(IntOffset(30, 30)) }
}
}
+
+ @Test
+ fun testLayoutModifierPlacingWithOffsetAndScale() {
+ var actualPosition: IntOffset = IntOffset.Max
+ var actualPositionChild: IntOffset = IntOffset.Max
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box {
+ Box(
+ Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeable.placeWithLayer(10, 10) {
+ scaleX = 2f
+ scaleY = 2f
+ }
+ }
+ }
+ .onLayoutRectChanged(0, 0) { actualPosition = it.positionInRoot }
+ ) {
+ Box(
+ Modifier.onLayoutRectChanged(0, 0) {
+ actualPositionChild = it.positionInRoot
+ }
+ .size(10.dp)
+ )
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle { assertThat(actualPosition).isEqualTo(IntOffset(5, 5)) }
+ rule.runOnIdle { assertThat(actualPositionChild).isEqualTo(IntOffset(5, 5)) }
+ }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
index c0a0395..095b038 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
@@ -645,7 +645,7 @@
@Test
@SmallTest
- fun testScaledBox() {
+ fun testTranslatedAndRotatedBox() {
var toggle by mutableStateOf(true)
rule.setContent {
Box(
@@ -666,7 +666,7 @@
@Test
@SmallTest
- fun testScaledBoxUpdate() {
+ fun testScaledBox() {
rule.setContent {
Box(Modifier.testTag("outer").padding(10.dp).scale(2f)) {
Box(Modifier.testTag("inner").size(10.dp))
@@ -844,6 +844,48 @@
}
}
+ @Test
+ @SmallTest
+ fun testLayoutPlacingWithOffsetAndScale() {
+ rule.setContent {
+ Layout(content = { Box(Modifier.testTag("inner").size(10.dp)) }) {
+ measurables,
+ constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ val offset = 10.dp.roundToPx()
+ placeable.placeWithLayer(offset, offset) {
+ scaleX = 2f
+ scaleY = 2f
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("inner").assertRectDp(5.dp, 5.dp, 25.dp, 25.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testLayoutPlacingWithTranslateOnLayer() {
+ rule.setContent {
+ Layout(content = { Box(Modifier.testTag("inner").size(10.dp)) }) {
+ measurables,
+ constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ val offset = 5.dp.roundToPx()
+ placeable.placeWithLayer(offset, offset) {
+ translationX = 10.dp.toPx()
+ translationY = 20.dp.toPx()
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("inner").assertRectDp(15.dp, 25.dp, 25.dp, 35.dp)
+ }
+
/**
* Lazy Column example that can be used to reproduce issues related to re-using LayoutNodes
* where each item has the same Layout.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
index 36055d9..ed3fb5e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
@@ -24,7 +24,6 @@
import androidx.compose.ui.currentTimeMillis
import androidx.compose.ui.focus.FocusTargetModifierNode
import androidx.compose.ui.geometry.MutableRect
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.isIdentity
import androidx.compose.ui.node.DelegatableNode
@@ -275,7 +274,13 @@
}
private fun recalculateOffsetFromRoot(layoutNode: LayoutNode) {
- val position = layoutNode.outerCoordinator.position
+ val outer = layoutNode.outerCoordinator
+ var position = outer.applyLayerTransformation(IntOffset.Zero)
+ if (!position.isSet) {
+ layoutNode.offsetFromRoot = IntOffset.Max
+ return
+ }
+ position += outer.position
val parent = layoutNode.parent
layoutNode.offsetFromRoot =
if (parent != null) {
@@ -384,37 +389,51 @@
var coordinator: NodeCoordinator? = this
while (coordinator != null) {
val layer = coordinator.layer
- rect.translate(coordinator.position.toOffset())
- coordinator = coordinator.wrappedBy
if (layer != null) {
val matrix = layer.underlyingMatrix
if (!matrix.isIdentity()) {
matrix.map(rect)
}
}
+ rect.translate(coordinator.position.toOffset())
+ coordinator = coordinator.wrappedBy
}
}
- private fun LayoutNode.outerToInnerOffset(): IntOffset {
- val terminator = outerCoordinator
- var position = Offset.Zero
- var coordinator: NodeCoordinator? = innerCoordinator
- while (coordinator != null) {
- if (coordinator === terminator) break
- val layer = coordinator.layer
- position += coordinator.position
- coordinator = coordinator.wrappedBy
- if (layer != null) {
- val matrix = layer.underlyingMatrix
- val analysis = matrix.analyzeComponents()
- if (analysis.isIdentity) continue
+ private fun NodeCoordinator.applyLayerTransformation(position: IntOffset): IntOffset {
+ val layer = layer
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ val analysis = matrix.analyzeComponents()
+ if (!analysis.isIdentity) {
if (analysis.hasNonTranslationComponents) {
return IntOffset.Max
}
- position = matrix.map(position)
+ return matrix.map(position.toOffset()).round()
}
}
- return position.round()
+ return position
+ }
+
+ /**
+ * @return combined offset for all coordinators not including the outer one, the outer offset is
+ * added into [LayoutNode.offsetFromRoot] instead. it can also return [IntOffset.Max], if the
+ * layer transformation is too complex.
+ */
+ private fun LayoutNode.outerToInnerOffset(): IntOffset {
+ val terminator = outerCoordinator
+ var position = IntOffset.Zero
+ var coordinator: NodeCoordinator? = innerCoordinator
+ while (coordinator != null) {
+ if (coordinator === terminator) break
+ position = coordinator.applyLayerTransformation(position)
+ if (position == IntOffset.Max) {
+ return IntOffset.Max
+ }
+ position += coordinator.position
+ coordinator = coordinator.wrappedBy
+ }
+ return position
}
fun remove(layoutNode: LayoutNode) {