-
-
Notifications
You must be signed in to change notification settings - Fork 469
Expand file tree
/
Copy pathComposeGestureTargetLocator.kt
More file actions
131 lines (114 loc) · 4.22 KB
/
ComposeGestureTargetLocator.kt
File metadata and controls
131 lines (114 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // to access internal vals
package io.sentry.compose.gestures
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.Owner
import androidx.compose.ui.semantics.SemanticsModifier
import io.sentry.ILogger
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.compose.BuildConfig
import io.sentry.compose.SentryComposeHelper
import io.sentry.compose.boundsInWindow
import io.sentry.internal.gestures.GestureTargetLocator
import io.sentry.internal.gestures.UiElement
import io.sentry.util.AutoClosableReentrantLock
import java.util.LinkedList
import java.util.Queue
@OptIn(InternalComposeUiApi::class)
public class ComposeGestureTargetLocator(private val logger: ILogger) : GestureTargetLocator {
@Volatile private var composeHelper: SentryComposeHelper? = null
private val lock = AutoClosableReentrantLock()
init {
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-compose", BuildConfig.VERSION_NAME)
}
override fun locate(root: Any?, x: Float, y: Float, targetType: UiElement.Type): UiElement? {
if (root !is Owner) {
return null
}
// lazy init composeHelper as it's using some reflection under the hood
if (composeHelper == null) {
lock.acquire().use {
if (composeHelper == null) {
composeHelper = SentryComposeHelper(logger)
}
}
}
val rootLayoutNode = root.root
val queue: Queue<LayoutNode> = LinkedList()
queue.add(rootLayoutNode)
// the final tag to return
var targetTag: String? = null
// the last known tag when iterating the node tree
var lastKnownTag: String? = null
while (!queue.isEmpty()) {
val node = queue.poll() ?: continue
if (node.isPlaced && layoutNodeBoundsContain(rootLayoutNode, node, x, y)) {
var isClickable = false
var isScrollable = false
val modifiers = node.getModifierInfo()
for (index in modifiers.indices) {
val modifierInfo = modifiers[index]
val tag = composeHelper!!.extractTag(modifierInfo.modifier)
if (tag != null) {
lastKnownTag = tag
}
if (modifierInfo.modifier is SemanticsModifier) {
val semanticsModifierCore = modifierInfo.modifier as SemanticsModifier
val semanticsConfiguration = semanticsModifierCore.semanticsConfiguration
for (item in semanticsConfiguration) {
val key: String = item.key.name
if ("ScrollBy" == key) {
isScrollable = true
} else if ("OnClick" == key) {
isClickable = true
}
}
} else {
val modifier = modifierInfo.modifier
// Newer Jetpack Compose 1.5 uses Node modifiers for clicks/scrolls
val type = modifier.javaClass.name
if (
"androidx.compose.foundation.ClickableElement" == type ||
"androidx.compose.foundation.CombinedClickableElement" == type
) {
isClickable = true
} else if (
"androidx.compose.foundation.ScrollingLayoutElement" == type ||
"androidx.compose.foundation.ScrollingContainerElement" == type
) {
isScrollable = true
}
}
}
if (isClickable && targetType == UiElement.Type.CLICKABLE) {
targetTag = lastKnownTag
}
if (isScrollable && targetType == UiElement.Type.SCROLLABLE) {
targetTag = lastKnownTag
// skip any children for scrollable targets
break
}
}
queue.addAll(node.zSortedChildren.asMutableList())
}
return if (targetTag == null) {
null
} else {
UiElement(null, null, null, targetTag, ORIGIN)
}
}
private fun layoutNodeBoundsContain(
root: LayoutNode,
node: LayoutNode,
x: Float,
y: Float,
): Boolean {
val bounds = node.coordinates.boundsInWindow(root.coordinates)
return bounds.contains(Offset(x, y))
}
public companion object {
private const val ORIGIN = "jetpack_compose"
}
}