Android - Set TalkBack accessibility focus to a specific view

62,615

Solution 1

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

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

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!

Share:
62,615
Skywalker10
Author by

Skywalker10

Android developer

Updated on July 09, 2022

Comments

  • Skywalker10
    Skywalker10 almost 2 years

    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.

  • Skywalker10
    Skywalker10 over 9 years
    Thank you. Would it be better practice to add a preference inside my app asking the user whether to put focus on this button? So the default TalkBack behaviour will be used unless the user explicitly sets this preference.
  • ChrisCM
    ChrisCM over 9 years
    What I like to do in apps, is to make sure my buttons are reasonably sized, specifically the width. If a button is wide enough, non-sighted users should be able to find it using touch exploration, instead of having to flick through # times. Reasonably wide buttons on the center of the screen are easy to find using finger drag motions! This is the best combination of not violating WCag Consistent Navigation/Context Change guidelines, and maximum non-sighted user experience.
  • alanv
    alanv over 9 years
    While this approach may work for the current version of TalkBack, the recommendation we give is to never attempt to manually place accessibility focus from an app. You will end up fighting with TalkBack or the user, neither of which is going to provide a good experience.
  • ChrisCM
    ChrisCM over 9 years
    Hence, the Disclaimer ;). Something I've wondered, why isn't that event in protected space, for AccessibilityServices only. Things that you should "never" do, and I agree this is one of them per WCag (and common sense TalkBack usability) guidelines, seem to fit well into this category... Just curious, while I've got a Google Rep's attention!
  • Skywalker10
    Skywalker10 about 9 years
    Yes, the solution presented by ChrisCM works for me.
  • RuiMochila
    RuiMochila about 9 years
    @Skywalker10 Thank you for your reply. I was trying to use this to focus a View but it isn't doing nothing. My app is controlled by taps and double taps (i'm listening to hover events to detect taps and touchEvents to detect double taps, because of the Talkback/Explore by Touch). In order for this to work I need that the view be already focused. If the view is not already focused the user needs to touch the view the first time (i need to ignore this hover) and then he can do the double tap (touchEvent) or the tap (hover enter/exit). Did you understand? That code should do the trick right?
  • Skywalker10
    Skywalker10 about 9 years
    You should open a new question with some code in it. It's kind of hard to guess what could be going wrong without the code.
  • RuiMochila
    RuiMochila about 9 years
    @Skywalker10 Thanks for everything. Today I tried this approach with another view (a textView) and it worked. I believe it was not working on the other because it was a custom view and it may be lacking something regarding accessibility. I'll do some tests, if I can't manage to do it I'll open a question. Thank you.
  • VIN
    VIN almost 8 years
    @RuiMochila would you be able to elaborate on how to fix this? I've tried to do mTextView.requestFocus(); and I've also tried mTextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIE‌​W_FOCUSED) in the onResume() method, but nothing occurs in either case.
  • Gokhan Arik
    Gokhan Arik over 5 years
    @ChrisCM You should write a blog post about Accessibility in Android. I would love to read about the best practices. Things like, should we announce toolbar title automatically or let the system handle it, or what you mentioned above about the focus. In my case, I have an EditText and it takes the focus. I prefer Toolbar Title to consume it
  • user3701188
    user3701188 over 4 years
    why do we need a postdelayed
  • ChrisCM
    ChrisCM over 4 years
    It makes sense to tackle this in the Activity lifecycle vs the View lifecycle. However, Activity doesn't have the proper callback to facilitate this without a delay, as any attempt to focus elements immediately after Activity lifecycle events will fail, because the Accessibility Tree isn't properly attached yet. Note: As of Feb 12, 2015 this was certainly true. That was a long time ago in Android years.
  • Someone Somewhere
    Someone Somewhere about 4 years
    Great topic! I was wondering myself, how do users with visual impairments react to timed things on the screen? For example, the Snackbar! If the UI is a looong list of stuff and suddenly the snackbar shows up - I'm betting that everyone using accessibility is going to miss that info because by the time they swipe right enough to focus on the snackbar, it's long gone into the sunset. Is snackbar terrible for accessibility ?
  • ChrisCM
    ChrisCM about 4 years
    Snackbar is an excellent patter to replace Toast messages, but it is not a pattern I would use abundantly in my own designs. Hope that made sense!
  • dev2505
    dev2505 over 3 years
    For anyone else having the same problem ... I found doing the regular sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) wasn't working with Samsung Voice Assistant on Android 10 (strangely 9 and below fine ....) However this other solution was the only thing that would work on that device. Kudos Gilberto, many thanks!
  • Martin Marconcini
    Martin Marconcini about 3 years
    I would like to add that the "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." is not actually true. In some cases (and more recent books on the subject) the recommended approach is "it depends". Context is the king, and focusing a screen in a "back arrow" that says "Back" is not better than providing appropriate "talkback" context and proper content descriptions.
  • Roman Truba
    Roman Truba about 3 years
    Please, don't use coroutines in this way. There's View#postDelayed, which works fine.