Android - Set TalkBack accessibility focus to a specific view

AndroidAccessibilityTalkback

Android Problem Overview


When TalkBack is enabled, is there any way to set the accessibility focus manual to a specific view? For instance, when my Activity is started I want TalkBack to automatically focus on a certain Button (yellow frame around the view) and read its content description.

What I've tried so far:

    myButton.setFocusable(true);
    myButton.setFocusableInTouchMode(true);
    myButton.requestFocus();

requestFocus(), it seems, is only requesting input focus and has nothing to do with accessibility focus. I've also tried:

    myButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    myButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    myButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
    myButton.announceForAccessibility("accessibility test");
    myButton.performAccessibilityAction(64, null); // Equivalent to ACTION_ACCESSIBILITY_FOCUS

    AccessibilityManager manager = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
    if (manager.isEnabled()) {
        AccessibilityEvent e = AccessibilityEvent.obtain();
        e.setSource(myButton);
        e.setEventType(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
        e.setClassName(getClass().getName());
        e.setPackageName(getPackageName());
        e.getText().add("another accessibility test");
        manager.sendAccessibilityEvent(e);
    }

None of this seems to work.

Android Solutions


Solution 1 - Android

DISCLAIMER: Forcing focus on Activity load to be anywhere but at the top bar is always (okay, always should almost never be said), but really, just don't do it. It is a violation of all sorts of WCAG 2.0 regulations, including 3.2.1 and 3.2.3, regarding predictable navigation and context changes respectively. You are, likely, actually making your app MORE inaccessible by doing this.

http://www.w3.org/TR/WCAG20/#consistent-behavior

END DISCLAIMER.

You are using the correct function calls. All you should need to do is this:

myButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);

The problem is more likely the point at which your attempting to do this. Talkback attaches itself to your activity slightly later in the activity cycle. The following solution illustrates this problem, I'm not sure there is a better way to do this, to be honest. I tried onPostResume, which is the last callback the Android OS calls, regarding the loading of activities, and still I had to add in a delay.

@Override
protected void onPostResume() {
	super.onPostResume();

	Log.wtf(this.getClass().getSimpleName(), "onPostResume");

	Runnable task = new Runnable() {

		@Override
		public void run() {
			Button theButton = (Button)WelcomeScreen.this.findViewById(R.id.idButton);
			theButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
		}
	};

	final ScheduledExecutorService worker = Executors.newSingleThreadScheduledExecutor();

	worker.schedule(task, 5, TimeUnit.SECONDS);

}

You might be able to create a custom view. The callbacks within view, may provide the logic you need to do this, without the race condition! I might look into it more later if I get time.

Solution 2 - Android

Recently I had the same problem. I created an Android extension function to focus a view that was not focused with a postDelayed like the other solutions proposed;

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)

But I had another scenario where it didn't work. However, I got it to work with this:

fun View.accessibilityFocus(): View {
    this.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
    this.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
    return this
}

Solution 3 - Android

I had the same problem because for consistent navigation we wanted the newly opened page's title to be selected. The problem was that the screen reader was selecting the first header button at the top left of my pages and not the title.

I had a myRootView variable for the whole view and a myTitleView variable for the title text view.

The solution that ChrisCM proposed for getting accessibility focused on the right view definitely helped me:

myTitleView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);

However I still had the problem that calling this code on app start had no effect because the screen reader was not ready yet, and the solution proposed for "waiting for talkback to be available" of waiting 5 full seconds was not something I wanted to do because by then, the user might already be using the interface and their selection would be interrupted by the automatic focus.

I noticed that on app open, the top left header button was systematically selected by accessibility, so I wrote a class to listen on that selection and trigger my own selection right after.

Here is the code of the class in question:

import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public abstract class OnFirstAccessibilityFocusRunner extends AccessibilityDelegate implements Runnable {
	@NonNull
	private final View rootView;
	private boolean hasAlreadyRun = false;

	public OnFirstAccessibilityFocusRunner(@NonNull final View _rootView) {
		rootView = _rootView;
		init();
	}

	private void init() {
		rootView.setAccessibilityDelegate(this);
	}

	@Override
	public boolean onRequestSendAccessibilityEvent(@Nullable final ViewGroup host, @Nullable final View child,
		@Nullable final AccessibilityEvent event) {
		if (!hasAlreadyRun
			&& event != null
			&& event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
		) {
			hasAlreadyRun = true;
			rootView.setAccessibilityDelegate(null);
			run();
		}
		return super.onRequestSendAccessibilityEvent(host, child, event);
	}
}

Then I use the code like this (for example in the onPostResume method of the Activity):

@Override
protected void onPostResume() {
	new OnFirstAccessibilityFocusRunner(myRootView) {
		@Override
		public void run() {
			myTitleView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
		}
	};
}

It took me a few hours to figure out how to instantly get the focus to the right view, I hope this helps others too!

Solution 4 - Android

Disclaimer: Seems that some views intercepting the accessibility focus (like RecyclerView, PreferenceFragmentCompat) and my solution is kind of "waiting talkback to be available".

Kotlin KTX provide the way to execute smth when view is ready for showing to user and Coroutine can help with delay. It isn't the best way but fast and simple.

CoroutineScope(Dispatchers.Default).launch {
      delay(300)
      withContext(Dispatchers.Main) { customView.doOnLayout {it.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) }}
}

Solution 5 - Android

lifecycleScope is available in the activity. No need to bind delay with any overridden method.

private fun View.focusAccessibility() = lifecycleScope.launchWhenResumed {
    delay(300)
    this@focusAccessibility.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSkywalker10View Question on Stackoverflow
Solution 1 - AndroidChrisCMView Answer on Stackoverflow
Solution 2 - AndroidGilberto IbarraView Answer on Stackoverflow
Solution 3 - AndroidandroidsebView Answer on Stackoverflow
Solution 4 - Androidkonovalov_kView Answer on Stackoverflow
Solution 5 - AndroidMaksim GolendukhinView Answer on Stackoverflow