Skip to content

Commit d04684d

Browse files
committed
Add fix for boundsInWindow incorrectly calculating position on screen (#1983)
1 parent 2a467a7 commit d04684d

File tree

3 files changed

+67
-5
lines changed

3 files changed

+67
-5
lines changed

paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ public class AccessibilityRenderExtension : RenderExtension {
9494

9595
private fun View.processAccessibleChildren(processElement: (AccessibilityElement) -> Unit) {
9696
val accessibilityText = this.accessibilityText()
97-
if (isImportantForAccessibility && !accessibilityText.isNullOrBlank() && isVisible) {
98-
val bounds = Rect().also(::getBoundsOnScreen)
97+
val bounds = Rect().also(::getBoundsOnScreen)
9998

99+
if (isImportantForAccessibility && !accessibilityText.isNullOrBlank() && isVisible) {
100100
processElement(
101101
AccessibilityElement(
102102
id = "${this::class.simpleName}($accessibilityText)",
@@ -111,9 +111,18 @@ public class AccessibilityRenderExtension : RenderExtension {
111111
val viewRoot = getChildAt(0) as ViewRootForTest
112112
val unmergedNodes = viewRoot.semanticsOwner.getAllSemanticsNodes(false)
113113

114+
// SemanticsNode.boundsInScreen isn't reported correctly for nodes so locationOnScreen used to correctly calculate displayBounds.
115+
val locationOnScreen = arrayOf(bounds.left, bounds.top).toIntArray()
116+
locationOnScreen[0] += paddingLeft
117+
locationOnScreen[1] += paddingTop
114118
val orderedSemanticsNodes = viewRoot.semanticsOwner.rootSemanticsNode.orderSemanticsNodeGroup()
115119
orderedSemanticsNodes.forEach {
116-
it.processAccessibleChildren(processElement, unmergedNodes)
120+
it.processAccessibleChildren(
121+
processElement = processElement,
122+
locationOnScreen = locationOnScreen,
123+
viewBounds = bounds,
124+
unmergedNodes = unmergedNodes
125+
)
117126
}
118127
}
119128

@@ -173,6 +182,8 @@ public class AccessibilityRenderExtension : RenderExtension {
173182

174183
private fun SemanticsNode.processAccessibleChildren(
175184
processElement: (AccessibilityElement) -> Unit,
185+
locationOnScreen: IntArray,
186+
viewBounds: Rect,
176187
unmergedNodes: List<SemanticsNode>?
177188
) {
178189
val accessibilityText = if (config.isMergingSemanticsOfDescendants) {
@@ -188,9 +199,14 @@ public class AccessibilityRenderExtension : RenderExtension {
188199
}
189200

190201
if (accessibilityText != null) {
191-
val displayBounds = with(boundsInWindow) {
192-
Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
202+
// SemanticsNode.boundsInScreen isn't reported correctly for nodes so boundsInRoot + locationOnScreen used to correctly calculate displayBounds.
203+
val displayBounds = with(boundsInRoot) {
204+
Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()).run {
205+
offset(locationOnScreen[0], locationOnScreen[1])
206+
Rect(left, top, right.coerceIn(0, viewBounds.right), bottom.coerceIn(0, viewBounds.bottom))
207+
}
193208
}
209+
194210
processElement(
195211
AccessibilityElement(
196212
// SemanticsNode.id is backed by AtomicInteger and is not guaranteed consistent across runs.
@@ -200,6 +216,15 @@ public class AccessibilityRenderExtension : RenderExtension {
200216
)
201217
)
202218
}
219+
220+
children.forEach {
221+
it.processAccessibleChildren(
222+
processElement = processElement,
223+
locationOnScreen = locationOnScreen,
224+
viewBounds = viewBounds,
225+
unmergedNodes = unmergedNodes
226+
)
227+
}
203228
}
204229

205230
private fun SemanticsNode.findAllUnmergedNodes(): List<SemanticsNode> {

sample/src/test/java/app/cash/paparazzi/sample/ComposeA11yTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package app.cash.paparazzi.sample
22

3+
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
4+
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
5+
import android.widget.LinearLayout
36
import androidx.compose.foundation.Image
47
import androidx.compose.foundation.clickable
58
import androidx.compose.foundation.layout.Box
@@ -19,6 +22,7 @@ import androidx.compose.material.icons.Icons
1922
import androidx.compose.material.icons.filled.Add
2023
import androidx.compose.ui.Alignment
2124
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.platform.ComposeView
2226
import androidx.compose.ui.semantics.Role
2327
import androidx.compose.ui.semantics.contentDescription
2428
import androidx.compose.ui.semantics.heading
@@ -40,6 +44,39 @@ class ComposeA11yTest {
4044
renderExtensions = setOf(AccessibilityRenderExtension())
4145
)
4246

47+
@Test
48+
fun multiComposeViews() {
49+
val view = LinearLayout(paparazzi.context).apply {
50+
orientation = LinearLayout.VERTICAL
51+
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
52+
setPaddingRelative(25, 25, 25, 25)
53+
54+
addView(
55+
ComposeView(context = paparazzi.context).apply {
56+
setPaddingRelative(32, 32, 32, 32)
57+
setContent { Text("Number 1") }
58+
},
59+
LinearLayout.LayoutParams(WRAP_CONTENT, 0, 1f)
60+
)
61+
addView(
62+
ComposeView(context = paparazzi.context).apply {
63+
setPaddingRelative(32, 32, 32, 32)
64+
setContent { Text("Number 2") }
65+
},
66+
LinearLayout.LayoutParams(WRAP_CONTENT, 0, 1f)
67+
)
68+
addView(
69+
ComposeView(context = paparazzi.context).apply {
70+
setPaddingRelative(32, 32, 32, 32)
71+
setContent { Text("Number 3") }
72+
},
73+
LinearLayout.LayoutParams(WRAP_CONTENT, 0, 1f)
74+
)
75+
}
76+
77+
paparazzi.snapshot(view)
78+
}
79+
4380
@Test
4481
fun compositeItems() {
4582
paparazzi.snapshot {
14.6 KB
Loading

0 commit comments

Comments
 (0)