QA How-To
Automate Android Touch Target Size Checks
Automate Android touch target size checks with Espresso, ATF, Kotlin, and a CI-ready 48 dp scanner that catches undersized controls before release safely.
20 min read | 2,418 words
TL;DR
Run ATF from Espresso for broad accessibility feedback, then scan every visible clickable view after layout and fail when its rendered width or height is below 48 dp. Pin the emulator, document exact exceptions, and publish failures as a CI artifact.
Key Takeaways
- Measure rendered clickable bounds in dp, not raw pixels.
- Use Android Accessibility Test Framework for broad Espresso accessibility checks.
- Add a focused view scanner when CI needs deterministic 48 dp enforcement.
- Prefer a clickable 48 dp container around small visual icons.
- Keep TouchDelegate exceptions exact, justified, and separately verified.
- Prove the gate with both a known failing target and a compliant target.
- Combine target-size checks with screen-reader and text-scaling coverage.
Automate Android touch target size checks with an instrumented Espresso test that inspects the rendered accessibility hierarchy, reports undersized controls, and fails CI before a release reaches users. You will enforce Android's 48 dp minimum interactive target while keeping exceptions explicit and reviewable.
This tutorial is a focused part of the Mobile Accessibility Automation Complete Guide. It uses AndroidX Test, Espresso, and the Accessibility Test Framework (ATF), so the check runs against the same views and dimensions that users receive.
You will build the rule, prove it catches a deliberately small button, reduce noisy results, and publish a machine-readable report in CI. The finished approach complements screen-reader checks because a control can have a correct label and still be too small to tap reliably. Use the accessibility testing checklist to track the other requirements that this focused gate does not cover.
What You Will Build
- An Android instrumented test project with Espresso and ATF.
- A broad ATF assertion that checks accessibility issues during UI actions.
- A deterministic custom scanner that measures clickable views in density-independent pixels.
- An allowlist for documented exceptions without hiding unrelated defects.
- A JSON artifact that identifies the screen, view, measured size, and failure reason.
- A CI job that runs the checks on a controlled Android emulator.
The broad ATF check is ideal for existing Espresso suites. The custom scanner adds stable ownership and reporting when you need a dedicated quality gate.
Prerequisites
Use Android Studio with a current stable Android SDK, JDK 17, and an Android app that has at least one Activity. The examples use Kotlin, Gradle Kotlin DSL, AndroidX Test, and a view-based screen. Compose teams can still use ATF through Espresso, but the direct view-tree scanner in this tutorial targets Android Views.
Install or confirm the SDK packages:
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" "emulator" "system-images;android-35;google_apis;x86_64"
adb --version
java -version
Create an emulator if you do not already have one:
avdmanager create avd \
--name touch-target-api-35 \
--package "system-images;android-35;google_apis;x86_64" \
--device pixel_6
Start it with animations disabled for predictable UI tests:
emulator -avd touch-target-api-35 -no-snapshot -no-window &
adb wait-for-device
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
The Android accessibility guideline is a target of at least 48 dp by 48 dp for interactive elements. Measure dp, not raw pixels. A 96 pixel control is 48 dp on a 2.0 density device but only 32 dp on a 3.0 density device.
Step 1: Add the Android Accessibility Test Dependencies
Add the AndroidX runner, Espresso, JUnit extension, and ATF to the app module. Keep versions in your version catalog if the repository already uses one. The following direct declarations show every required dependency:
// app/build.gradle.kts
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
}
dependencies {
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.espresso:espresso-accessibility:3.6.1")
androidTestUtil("androidx.test:orchestrator:1.5.1")
}
ATF is also pulled transitively by Espresso in many setups, but an explicit test dependency makes the API contract visible and prevents an accidental removal during dependency cleanup. If your project requires newer compatible patch versions, update them together and run the verification command.
Verify the instrumentation APK compiles:
./gradlew :app:assembleDebug :app:assembleDebugAndroidTest
Expected result: Gradle ends with BUILD SUCCESSFUL and produces both the app APK and test APK. Resolve dependency conflicts before adding test logic, so later failures describe accessibility rather than setup.
Step 2: Create a Screen with a Known Touch Target Defect
Create a small fixture in a debug or test-only sample screen. The important distinction is between visual size and touch target size. An icon can look 24 dp wide while its clickable container provides a compliant 48 dp target.
This XML intentionally makes small_action fail and gives safe_action enough padding:
<!-- res/layout/activity_touch_target_sample.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/touch_target_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<ImageButton
android:id="@+id/small_action"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="Open filters"
android:src="@drawable/ic_filter"
android:clickable="true"
android:focusable="true" />
<ImageButton
android:id="@+id/safe_action"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Save search"
android:padding="12dp"
android:src="@drawable/ic_save"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
Launch the fixture from a minimal Activity or adapt the IDs to a real screen. Do not make production controls smaller merely to test the rule. A dedicated fixture gives you a permanent positive and negative control.
Verify the rendered bounds manually:
adb shell uiautomator dump /sdcard/window.xml
adb pull /sdcard/window.xml build/window.xml
Open build/window.xml and find both resource IDs. On a density-controlled emulator, the bounds should show the first control occupying fewer density-independent units than the second. The XML reports pixels, so use adb shell wm density when converting values.
Step 3: Enable ATF Checks in Espresso
ATF can run accessibility checks whenever Espresso performs a view action. Enable it once in a @BeforeClass method and include views not considered important for accessibility, because clickable implementation details may otherwise escape inspection.
// app/src/androidTest/java/com/example/accessibility/TouchTargetAtfTest.kt
package com.example.accessibility
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.accessibility.AccessibilityChecks
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TouchTargetAtfTest {
@get:Rule
val activityRule = ActivityScenarioRule(TouchTargetSampleActivity::class.java)
companion object {
@JvmStatic
@BeforeClass
fun enableAccessibilityChecks() {
AccessibilityChecks.enable()
.setRunChecksFromRootView(true)
.setConsiderInvisibleViews(false)
}
}
@Test
fun allRenderedControlsPassAccessibilityChecks() {
onView(withId(R.id.safe_action)).perform(click())
}
}
setRunChecksFromRootView(true) scans the root associated with the action instead of checking only the acted-on view. That is why clicking the compliant button can reveal the nearby undersized button. ATF checks more than target size, so the same run can also report missing labels or other supported accessibility defects.
Verify the deliberate failure:
./gradlew :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.example.accessibility.TouchTargetAtfTest
Expected result: the test fails and the instrumentation output names the small control or reports a touch target size issue. Temporarily change small_action to 48 dp by 48 dp and rerun. The test should pass, proving that the gate detects the defect and its correction.
Step 4: Build a Deterministic 48 dp View Scanner
ATF is broad by design. Add a focused scanner when your CI needs a predictable list of undersized clickable views and a stable report format. Measure the rendered view dimensions after layout, convert pixels using displayMetrics.density, and traverse visible descendants.
// app/src/androidTest/java/com/example/accessibility/TouchTargetScanner.kt
package com.example.accessibility
import android.view.View
import android.view.ViewGroup
data class TouchTargetIssue(
val resourceName: String,
val widthDp: Float,
val heightDp: Float,
val reason: String
)
object TouchTargetScanner {
private const val MIN_TARGET_DP = 48f
fun scan(root: View): List<TouchTargetIssue> {
val density = root.resources.displayMetrics.density
return descendants(root)
.filter { it.visibility == View.VISIBLE && it.isShown }
.filter { it.isClickable || it.isLongClickable }
.mapNotNull { view ->
val widthDp = view.width / density
val heightDp = view.height / density
if (widthDp >= MIN_TARGET_DP && heightDp >= MIN_TARGET_DP) {
null
} else {
TouchTargetIssue(
resourceName = resourceName(view),
widthDp = widthDp,
heightDp = heightDp,
reason = "Clickable target must be at least 48dp x 48dp"
)
}
}
.toList()
}
private fun descendants(view: View): Sequence<View> = sequence {
yield(view)
if (view is ViewGroup) {
for (index in 0 until view.childCount) {
yieldAll(descendants(view.getChildAt(index)))
}
}
}
private fun resourceName(view: View): String = try {
view.resources.getResourceName(view.id)
} catch (_: Exception) {
"<no-id:${view.javaClass.simpleName}>"
}
}
This scanner intentionally treats the rendered clickable view as the target. Android can enlarge a target with a TouchDelegate, so you will account for that in the next step. First establish a strict baseline that exposes where layout dimensions alone are insufficient.
Verify it with an instrumentation assertion:
@Test
fun scannerFindsTheKnownSmallControl() {
activityRule.scenario.onActivity { activity ->
val root = activity.findViewById<View>(R.id.touch_target_root)
val issues = TouchTargetScanner.scan(root)
check(issues.any { it.resourceName.endsWith("small_action") }) {
"Expected small_action in $issues"
}
check(issues.none { it.resourceName.endsWith("safe_action") }) {
"safe_action should be compliant: $issues"
}
}
}
Expected result: this characterization test passes while the known defect exists. It proves the scanner's sensitivity without making the suite red. Your actual gate in Step 6 will require an empty issue list.
Step 5: Account for TouchDelegate and Intentional Exceptions
A small visual child may have a larger effective hit area supplied by its parent through TouchDelegate. A view-only width check cannot infer the delegate's rectangular bounds reliably from the child. Prefer a 48 dp clickable wrapper because it is obvious to accessibility tools, inspectors, and future maintainers.
Use exceptions only when the effective target is verified separately or the element is not truly an independent action. Store exact resource names with reasons:
data class TouchTargetException(
val resourceName: String,
val reason: String
)
val approvedExceptions = listOf(
TouchTargetException(
resourceName = "com.example:id/map_pin",
reason = "Map overlay uses a tested 56dp parent TouchDelegate"
)
)
fun unapprovedIssues(
issues: List<TouchTargetIssue>,
exceptions: List<TouchTargetException>
): List<TouchTargetIssue> {
val approvedNames = exceptions.map { it.resourceName }.toSet()
return issues.filterNot { it.resourceName in approvedNames }
}
Do not ignore an entire class such as ImageButton, and do not suppress all ATF target-size results. Those shortcuts can turn one justified exception into hundreds of invisible regressions.
| Approach | What it measures | CI stability | Recommended use |
|---|---|---|---|
| ATF through Espresso | Multiple accessibility properties on rendered hierarchy | High with controlled screens | Broad regression suite |
| Custom view scanner | Clickable view bounds in dp | High | Dedicated size gate and JSON reporting |
| Manual Layout Inspector | Visual and hierarchy evidence | Not automatable | Debugging a failure |
| Coordinate tap script | Whether one coordinate triggers an action | Low | Exploratory investigation only |
Verify exception behavior by asserting one exact removal:
val filtered = unapprovedIssues(issues, approvedExceptions)
check(filtered.size == issues.size - 1)
check(filtered.none { it.resourceName == "com.example:id/map_pin" })
Expected result: only the named target disappears. If the count drops by more than one, duplicate resource names or an overly broad filter need correction.
Step 6: Automate Android Touch Target Size Checks as a Failing Gate
Now convert discovery into enforcement. Wait until the Activity is resumed and laid out, scan the intended root, remove approved exceptions, and fail with an actionable multiline message.
// app/src/androidTest/java/com/example/accessibility/TouchTargetGateTest.kt
package com.example.accessibility
import android.view.View
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TouchTargetGateTest {
@get:Rule
val activityRule = ActivityScenarioRule(TouchTargetSampleActivity::class.java)
@Test
fun renderedClickableTargetsAreAtLeast48Dp() {
activityRule.scenario.onActivity { activity ->
val root = activity.findViewById<View>(R.id.touch_target_root)
val issues = unapprovedIssues(
TouchTargetScanner.scan(root),
approvedExceptions
)
val message = issues.joinToString(
prefix = "Undersized touch targets:\n",
separator = "\n"
) { issue ->
"%s is %.1fdp x %.1fdp: %s".format(
issue.resourceName,
issue.widthDp,
issue.heightDp,
issue.reason
)
}
assertTrue(message, issues.isEmpty())
}
}
}
Run only the gate while developing:
./gradlew :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.example.accessibility.TouchTargetGateTest
Expected result: with the fixture still at 32 dp, the output includes small_action is 32.0dp x 32.0dp on a normally configured emulator. Change the view to a 48 dp clickable container and rerun. The command must end in BUILD SUCCESSFUL.
This is the central technique to automate Android touch target size checks: inspect the final layout, normalize to dp, filter only documented cases, and fail on every remaining issue.
Step 7: Write a JSON Failure Artifact
Console failures are useful during development, but CI artifacts help teams assign and trend defects. Follow the test automation reporting guide when you integrate this JSON with a larger results pipeline. Write the report to the app's external files directory, where test infrastructure can pull it after the run.
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
fun writeTouchTargetReport(
context: Context,
screen: String,
issues: List<TouchTargetIssue>
): File {
val payload = JSONObject()
.put("screen", screen)
.put("minimumTargetDp", 48)
.put("issueCount", issues.size)
.put("issues", JSONArray().apply {
issues.forEach { issue ->
put(JSONObject()
.put("resourceName", issue.resourceName)
.put("widthDp", issue.widthDp.toDouble())
.put("heightDp", issue.heightDp.toDouble())
.put("reason", issue.reason))
}
})
val directory = context.getExternalFilesDir(null)
?: error("External files directory unavailable")
return File(directory, "touch-target-report.json").apply {
writeText(payload.toString(2))
}
}
Call this function before assertTrue in the gate test:
val report = writeTouchTargetReport(activity, "TouchTargetSample", issues)
println("Touch target report: ${report.absolutePath}")
assertTrue(message, issues.isEmpty())
Verify the artifact path in Logcat or instrumentation output, then pull it:
adb shell find /sdcard/Android/data/com.example/files -name touch-target-report.json
adb pull /sdcard/Android/data/com.example/files/touch-target-report.json build/touch-target-report.json
python3 -m json.tool build/touch-target-report.json
Expected result: the formatter prints valid JSON with minimumTargetDp equal to 48 and the deliberate issue listed. Package paths vary, so use your application ID instead of com.example.
Step 8: Automate Android Touch Target Size Checks in CI
Use Gradle Managed Devices when possible because the device definition belongs in source control. It reduces differences in density, API level, locale, and snapshots across runners.
// app/build.gradle.kts
android {
testOptions {
managedDevices {
devices {
maybeCreate<com.android.build.api.dsl.ManagedVirtualDevice>("pixel2Api35").apply {
device = "Pixel 2"
apiLevel = 35
systemImageSource = "aosp-atd"
}
}
}
}
}
Run the managed-device suite in GitHub Actions:
name: Android accessibility
on:
pull_request:
jobs:
touch-targets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: gradle
- uses: android-actions/setup-android@v3
- uses: gradle/actions/setup-gradle@v4
- name: Run touch target gate
run: ./gradlew :app:pixel2Api35DebugAndroidTest
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: touch-target-test-results
path: |
app/build/reports/androidTests/
app/build/outputs/androidTest-results/
Your Android Gradle Plugin determines the exact managed-device task name. List it with ./gradlew :app:tasks --all | grep AndroidTest instead of guessing when adapting the example.
Verify CI by opening a draft pull request with the 32 dp fixture. The job should fail and retain test reports. Fix the target, push again, and confirm the same job passes. A gate that has never demonstrated both states is not yet trustworthy.
Step 9: Expand Coverage Without Creating Flaky Tests
One Activity proves the mechanism, not the product. Add high-value screens through deterministic entry points. Seed data locally, bypass onboarding through test configuration, and scan after Espresso confirms a stable landmark is displayed.
Use parameterized screen cases only when navigation is reliable. A small set of explicit tests often produces clearer ownership:
@Test
fun checkoutTargetsAreCompliant() {
onView(withId(R.id.checkout_title)).check(matches(isDisplayed()))
activityRule.scenario.onActivity { activity ->
val issues = TouchTargetScanner.scan(
activity.findViewById(android.R.id.content)
)
assertTrue("Checkout issues: $issues", issues.isEmpty())
}
}
Cover screens with dense toolbars, cards containing nested actions, dialogs, bottom sheets, menus, and custom controls first. Also test large font and display scaling, because reflow can change rendered bounds or reveal alternate controls. The sibling tutorial on testing mobile dynamic type layouts shows how to make those layout variants repeatable.
Verify each new screen independently with the test-class or test-method instrumentation argument. Then run the full suite twice on a clean emulator. Investigate inconsistent results instead of adding sleeps. Espresso synchronization and stable local data are better controls than arbitrary delays.
Troubleshooting
ATF reports no issue for a visibly small control -> Confirm an Espresso action occurs after AccessibilityChecks.enable(), enable root-view checks, and verify the element is clickable. ATF evaluates accessibility properties and may interpret hierarchy context differently from a simple bounds scanner, so keep the focused gate for deterministic enforcement.
The custom scanner reports 0 dp dimensions -> The scan ran before layout or against a detached root. Launch with ActivityScenarioRule, execute inside onActivity, and first assert a stable view is displayed with Espresso.
A 48 pixel control passes on one emulator and fails on another -> You are comparing pixels, not dp. Divide rendered pixels by displayMetrics.density, and pin the managed device profile in CI.
A small icon is flagged although its surrounding area is tappable -> Make the 48 dp parent the clickable element and keep the icon non-clickable. If a TouchDelegate is required, document and separately test its effective hit rectangle before adding an exact exception.
The same issue appears multiple times -> Nested parent and child views may both be clickable. Simplify event ownership where possible. Otherwise report each resource ID because each independently clickable node is part of the interaction model.
CI cannot find the JSON report -> External storage paths include the application ID and can differ by device. Print the absolute path, preserve standard Android test reports, and use adb shell find during setup. Remember that a managed device may be destroyed immediately after its Gradle task.
Best Practices
- Make a 48 dp clickable container the default component API, even when the visible glyph is smaller.
- Run ATF for broad accessibility feedback and keep the focused scanner for stable size enforcement.
- Measure after layout in dp and record one decimal place in diagnostic output.
- Give interactive views stable resource IDs so failures identify an owner.
- Keep exceptions exact, justified, reviewed, and close to the test configuration.
- Test both a known failing fixture and a known passing fixture to detect a broken checker.
- Scan representative states, including dialogs, menus, error states, and larger text settings.
- Treat target size as one accessibility dimension, not a substitute for labels, roles, focus order, contrast, or screen-reader behavior.
Where To Go Next
Use the complete mobile accessibility automation guide to place this gate inside a broader test strategy. Target size protects motor access, while semantics and navigation need separate evidence.
Next, test Android TalkBack focus order with Appium to verify that labeled controls are reached in a logical sequence. If your team ships Apple platforms too, validate iOS VoiceOver labels with XCUITest. Combine those checks with the dynamic type tutorial linked above for meaningful cross-platform coverage.
Keep the checks layered. Run quick deterministic hierarchy rules on every pull request, run focused screen-reader flows on stable builds, and retain manual accessibility exploration for behaviors automation cannot judge well.
Interview Questions and Answers
Q: Why is 48 dp used instead of 48 pixels for Android touch targets?
Dp normalizes physical sizing across screen densities. Pixel dimensions vary with device density, so a fixed pixel threshold would evaluate equivalent controls inconsistently.
Q: What is the difference between visible icon size and touch target size?
The icon is the visual content, while the touch target is the entire tappable region. A 24 dp icon can be accessible when centered inside a clickable 48 dp container.
Q: Why combine ATF with a custom scanner?
ATF provides broad, maintained accessibility checks integrated with Espresso. A focused scanner gives the team deterministic 48 dp enforcement, exact exception handling, and a custom report schema.
Q: When is a touch target exception acceptable?
Only when the effective hit area is proven by another mechanism, or the node is not an independent action. The exception should name one resource, explain why, and receive review like production code.
Q: How do you prevent touch target tests from becoming flaky?
Use a controlled emulator, deterministic app data, Espresso synchronization, and scans after layout. Avoid coordinate taps, arbitrary sleeps, and navigation that depends on external services.
Q: Does a passing touch target test prove an app is accessible?
No. It proves only one motor-access requirement for tested states. Labels, roles, focus order, contrast, text scaling, announcements, and actual assistive-technology behavior require additional checks.
Conclusion
To automate Android touch target size checks effectively, evaluate rendered interactive views, convert pixels to dp, enforce a 48 dp by 48 dp baseline, and make every exception specific. ATF supplies broad accessibility coverage, while the focused scanner turns target sizing into an understandable CI contract.
Start with one deliberate failure and one compliant control. Once both verification paths behave correctly, expand screen by screen and pair this gate with screen-reader and text-scaling tests.
Interview Questions and Answers
Why does Android express touch target guidance in dp rather than pixels?
Dp accounts for different screen densities, so equivalent controls receive consistent evaluation. A pixel-only threshold becomes physically smaller or larger depending on the device. Tests should divide rendered pixels by display density before applying the 48 dp rule.
How would you automate Android touch target size checks?
I would enable ATF in the Espresso suite for broad accessibility feedback, then add a focused scanner over visible clickable views after layout. The scanner converts bounds to dp, filters only approved exact exceptions, and fails CI with resource IDs and measured dimensions.
What is the difference between a control's visual bounds and its touch target?
Visual bounds describe what the user sees, such as a 24 dp icon. The touch target is the full region that accepts the gesture. A compliant component can keep the icon small while exposing a clickable 48 dp container.
How would you handle TouchDelegate in an automated size gate?
A simple child-view bounds scanner cannot prove the delegate rectangle. I prefer making the larger parent explicitly clickable. If TouchDelegate is required, I test its effective hit coordinates separately and allowlist only that exact resource with a documented reason.
How do you verify that an accessibility gate itself works?
I maintain a known undersized fixture and a known compliant control. The characterization test must detect only the failing fixture, and the production gate must turn green after the target is fixed. CI should demonstrate both failure and success states.
How do you keep Android accessibility instrumentation stable in CI?
I pin the managed device, API level, density, locale, and app data. I use Espresso synchronization and deterministic navigation rather than sleeps or raw coordinates. I also retain instrumentation reports so failures can be diagnosed.
Does ATF coverage make a custom touch target scanner unnecessary?
Not always. ATF is valuable because it provides maintained checks across several accessibility properties. A custom scanner is justified when the team needs a stable 48 dp contract, precise exceptions, or a machine-readable report, but it should complement ATF rather than replace it.
Frequently Asked Questions
What is the minimum Android touch target size?
Use at least 48 dp by 48 dp for an interactive Android touch target. The visible icon can be smaller when a clickable container or verified effective hit area provides the full target.
Can Espresso check Android touch target size automatically?
Yes. Enable Android Accessibility Test Framework checks through Espresso and perform an action on the screen. Root-view checks can inspect the surrounding hierarchy, while a focused scanner can enforce an exact team rule.
Should an Android touch target test measure pixels or dp?
Measure dp because it normalizes dimensions across screen densities. Divide the rendered pixel width and height by the device density before comparing them with 48.
How do I test a small icon with a large tappable area?
Prefer placing the icon inside a clickable container that is at least 48 dp by 48 dp. If the app uses TouchDelegate, verify the effective rectangle separately and document any scanner exception.
Why does my touch target test report zero width or height?
The view was probably scanned before layout or while detached. Launch it with ActivityScenario, wait for a stable displayed landmark through Espresso, and scan inside onActivity.
Can automated touch target checks replace manual accessibility testing?
No. Automation catches repeatable size regressions, but it cannot judge every interaction or assistive-technology experience. Pair it with screen-reader flows, text scaling, and manual exploration.
How should touch target exceptions be managed?
Allowlist only an exact resource ID and include a concrete reason. Review exceptions as code, verify the effective hit area another way, and never suppress a whole widget class.