Skip to content

Commit 2d6c899

Browse files
committed
initial doc update
1 parent a28db55 commit 2d6c899

File tree

21 files changed

+1266
-175
lines changed

21 files changed

+1266
-175
lines changed

packages/core/android/src/debug/java/expo/modules/ama/ReactNativeAmaModule.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class ReactNativeAmaModule : Module() {
6666

6767
Function("stop") {
6868
if (isMonitoring) {
69+
a11yChecker.clearAllIssues()
6970
currentDecorView?.let { it.viewTreeObserver.removeOnDrawListener(drawListener) }
7071

7172
isMonitoring = false

packages/core/android/src/debug/java/expo/modules/ama/a11yChecker.kt

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package expo.modules.ama
22

33
import android.graphics.Color
4+
import android.graphics.Rect
45
import android.graphics.drawable.ColorDrawable
56
import android.view.View
67
import android.view.ViewGroup
@@ -9,7 +10,6 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
910
import expo.modules.kotlin.AppContext
1011
import java.util.Collections
1112
import kotlin.collections.mutableListOf
12-
import kotlin.math.pow
1313
import kotlin.synchronized
1414

1515
data class A11yIssue(
@@ -70,6 +70,8 @@ val LOGGER_RULES: Map<Rule, RuleAction> =
7070
)
7171

7272
class A11yChecker(private val appContext: AppContext, private val config: AMAConfig) {
73+
val activity = appContext.activityProvider?.currentActivity
74+
7375
private val issues = Collections.synchronizedList(mutableListOf<A11yIssue>())
7476
private val highlighter = Highlight(appContext)
7577
private lateinit var rootView: View
@@ -109,6 +111,10 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
109111
return emptyList()
110112
}
111113

114+
public fun clearAllIssues() {
115+
issues.forEach { issue -> issue.viewId?.let { highlighter.clearHighlight(it) } }
116+
}
117+
112118
private fun clearFixedIssues(oldIssues: List<A11yIssue>) {
113119
val fixed = oldIssues.filter { it !in issues }
114120

@@ -123,7 +129,7 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
123129
return
124130
}
125131

126-
checkView(view, issues)
132+
checkView(view)
127133

128134
if (view is ViewGroup) {
129135
for (i in 0 until view.childCount) {
@@ -156,7 +162,7 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
156162
}
157163
}
158164

159-
private fun checkView(view: View, issues: MutableList<A11yIssue>) {
165+
private fun checkView(view: View) {
160166
val info = view.createAccessibilityNodeInfo()
161167
val a11yInfo = AccessibilityNodeInfoCompat.wrap(info)
162168

@@ -166,11 +172,11 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
166172
checkForMinimumTargetSize(view)
167173
}
168174

169-
if (view is TextView) {
170-
Logger.debug("checkView", "Check for color contrast")
171-
172-
checkColorContrast(view, issues)
173-
}
175+
// if (view is TextView) {
176+
// Logger.debug("checkView", "Check for color contrast")
177+
//
178+
// checkColorContrast(view)
179+
// }
174180
}
175181

176182
private fun checkForA11yLabel(view: View, a11yInfo: AccessibilityNodeInfoCompat) {
@@ -215,20 +221,42 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
215221
}
216222
}
217223

224+
private val density: Float
225+
get() = activity?.resources?.displayMetrics?.density ?: 1f
226+
227+
/** dp → px */
228+
private fun dpToPx(dp: Float): Int = (dp * density + 0.5f).toInt()
229+
230+
/** px → dp */
231+
private fun pxToDp(px: Int): Float = px / density
232+
218233
private fun checkForMinimumTargetSize(view: View) {
219-
if (view.width < 48 || view.height < 48) {
220-
Logger.info("checkView", "Small touch target")
234+
val absBounds = Rect().also { view.createAccessibilityNodeInfo().getBoundsInScreen(it) }
235+
236+
getHitSlopRect(view)?.let { hitSlop ->
237+
absBounds.left -= hitSlop.left
238+
absBounds.top -= hitSlop.top
239+
absBounds.right += hitSlop.right
240+
absBounds.bottom += hitSlop.bottom
241+
}
221242

243+
val widthPx = absBounds.width()
244+
val heightPx = absBounds.height()
245+
val widthDp = widthPx / view.resources.displayMetrics.density
246+
val heightDp = heightPx / view.resources.displayMetrics.density
247+
248+
// 4) check vs 48dp
249+
if (widthDp < 48 || heightDp < 48) {
222250
addIssue(
223251
rule = Rule.MINIMUM_SIZE,
224252
label = view.toString(),
225-
reason = "Touchable are found ${view.width}x${view.height}",
253+
reason = String.format("%.1f×%.1f dp (< 48 dp)", widthDp, heightDp),
226254
view = view
227255
)
228256
}
229257
}
230258

231-
private fun checkColorContrast(textView: TextView, issues: MutableList<A11yIssue>) {
259+
private fun checkColorContrast(textView: TextView) {
232260
try {
233261
// Get text color
234262
val textColor = textView.currentTextColor
@@ -364,3 +392,27 @@ fun View.getTextOrContent(): String {
364392

365393
return a11yInfo.contentDescription?.toString().orEmpty()
366394
}
395+
396+
fun getHitSlopRect(view: View): Rect? {
397+
return try {
398+
val rvClass = Class.forName("com.facebook.react.views.view.ReactViewGroup")
399+
400+
if (!rvClass.isInstance(view)) {
401+
Logger.info("getHitSlopRect", "no class found")
402+
403+
return null
404+
}
405+
406+
val getter = rvClass.getMethod("getHitSlopRect")
407+
408+
@Suppress("UNCHECKED_CAST") val rect = getter.invoke(view) as? Rect
409+
410+
rect
411+
} catch (e: ClassNotFoundException) {
412+
null
413+
} catch (e: NoSuchMethodException) {
414+
null
415+
} catch (e: Exception) {
416+
null
417+
}
418+
}

0 commit comments

Comments
 (0)