11package expo.modules.ama
22
33import android.graphics.Color
4+ import android.graphics.Rect
45import android.graphics.drawable.ColorDrawable
56import android.view.View
67import android.view.ViewGroup
@@ -9,7 +10,6 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
910import expo.modules.kotlin.AppContext
1011import java.util.Collections
1112import kotlin.collections.mutableListOf
12- import kotlin.math.pow
1313import kotlin.synchronized
1414
1515data class A11yIssue (
@@ -70,6 +70,8 @@ val LOGGER_RULES: Map<Rule, RuleAction> =
7070 )
7171
7272class 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