Search code examples
androidandroid-orientationtalkbackaccessibilityaccessibility-api

Google talkback API to get current focus item in android


My application is supporting accessibility feature and application is having both portrait and landscape mode.

In my screen i have some views like button, textview, listview with custom row.

The issue what i am facing is when user focus any item in portrait mode and rotate screen, application is not focusing the same element in landscape mode. Can some one suggest how to set the focus to the item which was selected in portrait mode to landscape mode?

i even did some research on existed applications like native settings apps-wifi page and "ES file explore", in these applications also accessibility is not maintained when user change the orientation to landscape mode. System is selecting some random elements in landscape to portrait or vice versa.

Below is the code snippet

accessibility_sample.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
    android:id="@+id/name_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:contentDescription="Name"
    android:focusableInTouchMode="true"
    android:text="Name" />

<TextView
    android:id="@+id/email_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:contentDescription="Email"
    android:focusableInTouchMode="true"
    android:text="Email" />

<Button
    android:id="@+id/sample_button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:contentDescription="Button description"
    android:text="ButtonText" />

<CheckBox
    android:id="@+id/checkbox"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:contentDescription="Checkbox description"
    android:text="Checkbox Text" />

<ListView
    android:id="@+id/sample_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:fadeScrollbars="false" >
</ListView>

</LinearLayout>

sample_list_row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" 
>

<CheckBox
    android:id="@+id/list_row_cb"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >
</CheckBox>

<TextView
    android:id="@+id/list_row_name"
    android:layout_width="match_parent"
    android:focusableInTouchMode="true"
    android:focusable="true"
    android:layout_height="wrap_content" />

  </LinearLayout>

AccessibilitySampleActivity.java

public class AccessibilitySampleActivity extends Activity {

private String TAG = AccessibilitySampleActivity.class.getSimpleName();
ListView sampleList;
Button myButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.accessibility_sample);
    sampleList = (ListView) findViewById(R.id.sample_list);
    myButton = (Button) findViewById(R.id.sample_button);
    ArrayList<String> countriesList = new ArrayList<String>();
    countriesList.add("India");
    countriesList.add("America");
    countriesList.add("China");
    countriesList.add("Swis");
    countriesList.add("Paries");
    countriesList.add("Pak");
    countriesList.add("Aus");
    countriesList.add("Afg");
    countriesList.add("Nedharnalds");
    countriesList.add("Bangladhesh");
    countriesList.add("Srilanka");
    countriesList.add("France");
    countriesList.add("Japan");
    countriesList.add("SouthAfrica");
    countriesList.add("Iran");
    countriesList.add("Malaysia");
    countriesList.add("Nepal");

    sampleList.setAdapter(new CustomArrayAdapter(this, R.layout.sample_list_row, countriesList));

}

class CustomArrayAdapter extends ArrayAdapter<String> {

    Context context;
    int resource;
    ArrayList<String> countriesList;

    class ViewHolder {
        TextView name;
    }

    public CustomArrayAdapter(Context context, int resource, ArrayList<String> countriesList) {
        super(context, resource, countriesList);
        this.context = context;
        this.resource = resource;
        this.countriesList = countriesList;

    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;

        if (convertView == null) {
            LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = vi.inflate(resource, null);

            holder = new ViewHolder();
            holder.name = (TextView) convertView.findViewById(R.id.list_row_name);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.name.setText(getItem(position));

        return convertView;
    }

    @Override
    public int getCount() {
        return super.getCount();
    }

}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static View findAccessibilityFocus(View view) {
    if (view == null)
        return view;

    if (view.isAccessibilityFocused())
        return view;

    if (view instanceof ViewGroup) {
        ViewGroup viewGroup = (ViewGroup) view;

        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View childView = viewGroup.getChildAt(i);

            View result = findAccessibilityFocus(childView);

            if (result != null)
                return result;
        }
    }

    return null;
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    Log.d(TAG, "onConfigurationChanged");
    AccessibilityManager am = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
    boolean isAccessibilityEnabled = am.isEnabled();
    boolean isExploreByTouchEnabled = am.isTouchExplorationEnabled();
    Log.d(TAG, "isAccessibilityEnabled:" + isAccessibilityEnabled + " isExploreByTouchEnabled:"
            + isExploreByTouchEnabled);

    if (isAccessibilityEnabled) {
        View activityView = this.findViewById(android.R.id.content);
        Log.d(TAG, "activityView:" + activityView);
        View selectedView = findAccessibilityFocus(activityView);
        if (selectedView != null) {
            Log.d(TAG, "selectedView:" + selectedView);
            selectedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        }
    }
}

}

For normal views like TextView, Button, Checkbox it is able to maintain state when user rotate screen.

If the user select list view below are the issues i am facing

  • If any view is selected in portrait and rotated screen, then focus is not maintained in landscape. Some times If the selected view is able to display without scrolling(like if user select India and rotate India will be in visible are without scrolling) it maintain the state
  • When user rotate screen from portrait to landscape, apply scrolling and select any view(like Japan) and change orientation form landscape to portrait mode, then we could see that it always select first row checkbox ie checkbox of india.

Solution

  • WCAG currently does not specify what should happen with focus after dynamic (while your app is running) orientation changes. It does discuss orientation changes, but not what to do with focus on such context changes, and for good reason. Focus management is not the core issue here! In fact, if anything, WCAG would discourage you from the solution you are attempting, as it could be seen as interfering with the default operation of the Assistive Technology. Think about it, in your question you pointed out that most apps "don't do" the thing that you're trying to do. If you were to accomplish this thing, AND EVERY OTHER APP IN THE WORLD DIDN'T, who is really inaccessible? Is not "expected behavior" beneficial?

    The inaccessible part of this is the surprising orientation change NOT what happens with focus after a COMPLETELY SCREEN RE-DRAW! It seems to me, that the motivation for this is potentially a misguided interpretation of guidelines under WCAG 2.0 3.2.#. If users are unexpectedly causing orientation changes at times when they care about where focus is... this is indeed an issue! You just have latched onto the wrong part of the problem.

    Focus control is very important for accessibility, but orientation changes are different from other contextual changes. In an orientation change, the screen is potentially completely different. Sure, there are going to be similar elements on screen, but no guarantee that the focused element is even still present. Rather than attempting to track focus and move it to the appropriate (potentially now off screen element) let the AT do it's thing, and be more selective about when orientation changes occur. Best practices here:

    • Never lock a user into a specific orientation. Remember users may have their device mounted in front of them, so requiring an orientation is very bad. Mobile Guidelines 4.1.
    • However, this being said, once your sure the user (when Assistive Technologies that use touch exploration are one) is in the orientation they are comfortable with, lock the orientation, and provide another mechanism for them to manually change orientation. This way the user can freely do things like hold their phone to their ear, without worrying about accidentally triggering an orientation change. WCAG 2.0 - 3.2.5
    • Should an orientation change occur, allow the default behavior. Even if it appears to you to be inaccessible, it's likely not that frustrating AS LONG AS users aren't accidentally triggering orientation changes. Which if you've addressed the issue above properly, will not be a problem.
    • Finally, remember, that when your developing for accessibility, TalkBack is just one Assistive Technology, and blind individuals are just one subset of the users of this technology. While, you may perhaps be improving the user experience for blind TalkBack users, you have to consider what you're doing for TalkBacks users who use talkback to support their physical disability, or BrailleBack users, or SwitchAccess users. In general, supporting multiple ATs means allowing the operating system, AT and user Agent to do the default/supported thing as often as possible. While adding in hack solutions may improve experience for blind TalkBack users, on TalkBack v#.#, it could easily make things worse for current SwitchAccess users, and even break it for blink TalkBack v#.#+1 users. This concept of hack solutions being bad is actually covered in WCAG as well, under WCAG 4.1.

    In conclusion, what you're trying to do is really somewhat misguided from an accessibility perspective, and would actually be making things worse. That being said, the concept of tracking accessibility focus in an Android view is interesting, and I will leave the code for you below. But, I would recommend against this approach, in this instance.

    ORIGINAL ANWER:

    This is pretty simple.

    Telling talkback to focus a view:

    View theView = findViewById(R.id.theViewId);
    theView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    

    As far as finding the currently focused node, you want to search your view heirarchy for the view that is focused.

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static View findAccessibilityFocus(View view) {
        if (view == null) return view;
    
        if (view.isAccessibilityFocused()) return view;
    
        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup)view;
    
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                View childView = viewGroup.getChildAt(i);
    
                View result = findAccessibilityFocus(childView);
    
                if (result != null) return result;
            }
        }
    
        return null;
    }
    

    You have to be careful when using this function! Accessibility focus can be a difficult thing to find when race conditions are involved. A view DOES NOT have to have a view that is accessibility focused. It may be easier to attach an accessibility delegate to your root view and keep track of the last element that had accessibility focus, tracking the corresponding accessibility events.