1
1
package expo.modules.ama
2
2
3
3
import android.graphics.Color
4
+ import android.graphics.Rect
4
5
import android.graphics.drawable.ColorDrawable
5
6
import android.view.View
6
7
import android.view.ViewGroup
@@ -9,7 +10,6 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
9
10
import expo.modules.kotlin.AppContext
10
11
import java.util.Collections
11
12
import kotlin.collections.mutableListOf
12
- import kotlin.math.pow
13
13
import kotlin.synchronized
14
14
15
15
data class A11yIssue (
@@ -70,6 +70,8 @@ val LOGGER_RULES: Map<Rule, RuleAction> =
70
70
)
71
71
72
72
class A11yChecker (private val appContext : AppContext , private val config : AMAConfig ) {
73
+ val activity = appContext.activityProvider?.currentActivity
74
+
73
75
private val issues = Collections .synchronizedList(mutableListOf<A11yIssue >())
74
76
private val highlighter = Highlight (appContext)
75
77
private lateinit var rootView: View
@@ -109,6 +111,10 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
109
111
return emptyList()
110
112
}
111
113
114
+ public fun clearAllIssues () {
115
+ issues.forEach { issue -> issue.viewId?.let { highlighter.clearHighlight(it) } }
116
+ }
117
+
112
118
private fun clearFixedIssues (oldIssues : List <A11yIssue >) {
113
119
val fixed = oldIssues.filter { it !in issues }
114
120
@@ -123,7 +129,7 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
123
129
return
124
130
}
125
131
126
- checkView(view, issues )
132
+ checkView(view)
127
133
128
134
if (view is ViewGroup ) {
129
135
for (i in 0 until view.childCount) {
@@ -156,7 +162,7 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
156
162
}
157
163
}
158
164
159
- private fun checkView (view : View , issues : MutableList < A11yIssue > ) {
165
+ private fun checkView (view : View ) {
160
166
val info = view.createAccessibilityNodeInfo()
161
167
val a11yInfo = AccessibilityNodeInfoCompat .wrap(info)
162
168
@@ -166,11 +172,11 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
166
172
checkForMinimumTargetSize(view)
167
173
}
168
174
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
+ // }
174
180
}
175
181
176
182
private fun checkForA11yLabel (view : View , a11yInfo : AccessibilityNodeInfoCompat ) {
@@ -215,20 +221,42 @@ class A11yChecker(private val appContext: AppContext, private val config: AMACon
215
221
}
216
222
}
217
223
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
+
218
233
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
+ }
221
242
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 ) {
222
250
addIssue(
223
251
rule = Rule .MINIMUM_SIZE ,
224
252
label = view.toString(),
225
- reason = " Touchable are found ${view.width} x ${view.height} " ,
253
+ reason = String .format( " %.1f×%.1f dp (< 48 dp) " , widthDp, heightDp) ,
226
254
view = view
227
255
)
228
256
}
229
257
}
230
258
231
- private fun checkColorContrast (textView : TextView , issues : MutableList < A11yIssue > ) {
259
+ private fun checkColorContrast (textView : TextView ) {
232
260
try {
233
261
// Get text color
234
262
val textColor = textView.currentTextColor
@@ -364,3 +392,27 @@ fun View.getTextOrContent(): String {
364
392
365
393
return a11yInfo.contentDescription?.toString().orEmpty()
366
394
}
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