Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.invisibleToUser
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.progressBarRangeInfo
Expand Down Expand Up @@ -188,13 +190,26 @@ class AccessibilityRenderingTest {
.semantics {
invisibleToUser()
},
text = "Text invisible to user"
text = "Text invisible to user via `invisibleToUser`"
)
Text(
modifier = Modifier
.semantics {
hideFromAccessibility()
},
text = "Text invisible to user via `hideFromAccessibility`"
)
Text(
modifier = Modifier
.alpha(0f),
text = "Text with zero alpha"
)
Text(
modifier = Modifier.graphicsLayer {
alpha = 0f
},
text = "Text with zero alpha via graphicsLayer"
)
Text(text = "Text that is visible!")
}
}
Expand Down
Copy link
Author

@FrancoisBlavoet FrancoisBlavoet Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this dropdown is detected as transparent. That's not ideal, but the "before" screenshot is also incorrect and does not display the dropdown content, so the after is technically correct.

This happens because DropdownMenu is a Popup and AFAIK paparazzi can only render content in the main window
The ideal state would be to display both windows, but that's out of the scope of this PR.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import android.view.WindowManagerImpl
import android.widget.Checkable
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.semantics.LiveRegionMode.Companion.Assertive
Expand All @@ -44,6 +42,7 @@ import androidx.core.view.isVisible
import app.cash.paparazzi.RenderExtension
import app.cash.paparazzi.internal.ComposeViewAdapter
import com.android.internal.view.OneShotPreDrawListener
import java.lang.reflect.Method

/**
* A [RenderExtension] that overlays accessibility property information on top of the rendered view.
Expand Down Expand Up @@ -339,12 +338,9 @@ public class AccessibilityRenderExtension : RenderExtension {

private fun SemanticsNode.accessibilityText(): String? {
val invisibleToUser = config.getOrNull(SemanticsProperties.InvisibleToUser) != null
val hasZeroAlphaModifier = layoutInfo.getModifierInfo().any {
// We don't get direct access to an alpha field but we can inspect the modifiers and see if
// a modifier of 0f was applied to the node.
it.modifier == Modifier.alpha(0f)
}
if (invisibleToUser || hasZeroAlphaModifier) {
val hideFromAccessibility = config.getOrNull(SemanticsProperties.HideFromAccessibility) != null
val hasZeroAlphaModifier = hasZeroAlpha()
if (invisibleToUser || hideFromAccessibility || hasZeroAlphaModifier) {
return null
}

Expand Down Expand Up @@ -452,6 +448,18 @@ public class AccessibilityRenderExtension : RenderExtension {
)
}

private fun SemanticsNode.hasZeroAlpha(): Boolean {
// Resolve and cache the reflection Method once; invoke on each call.
resolveIsTransparentMethod()
val method = cachedIsTransparentMethod ?: return false
return try {
val transparent = method.invoke(this) as? Boolean
transparent == true
} catch (_: Exception) {
false
}
}

private fun View.accessibilityText(): String? {
val nodeInfo = createAccessibilityNodeInfo()
onInitializeAccessibilityNodeInfo(nodeInfo)
Expand Down Expand Up @@ -529,6 +537,34 @@ public class AccessibilityRenderExtension : RenderExtension {
.replace("\t", "\\t")

internal companion object {
// Cached reflection method for SemanticsNode transparency to avoid repeated lookups.
@Volatile
private var cachedIsTransparentMethod: Method? = null

@Volatile
private var attemptedResolveIsTransparentMethod: Boolean = false

private val TRANSPARENT_GETTER_CANDIDATES = arrayOf(
"isTransparent",
"isTransparent\$ui_release",
"getIsTransparent",
"getIsTransparent\$ui_release"
)

private fun resolveIsTransparentMethod() {
if (attemptedResolveIsTransparentMethod) return
attemptedResolveIsTransparentMethod = true
for (name in TRANSPARENT_GETTER_CANDIDATES) {
try {
val method = SemanticsNode::class.java.getDeclaredMethod(name)
method.isAccessible = true
cachedIsTransparentMethod = method
return
} catch (_: Exception) {
// Try next candidate name
}
}
}
private const val ON_CLICK_LABEL = "<on-click>"
private const val DISABLED_LABEL = "<disabled>"
private const val TOGGLEABLE_LABEL = "<toggleable>"
Expand Down