3
\$\begingroup\$

Accessible apps allow more users to use your app; however, it is usually an afterthought for developers, and it makes your XML layout files messy whether Accessibility was a forethought or afterthought. This hopefully-soon-to-be-a-standalone-library-but-now-part-of-another-app-that-is-not-related-to-accessibility "library" allows for hassle-free accessibility additions in your application — often in 1 line of code! OK, well, you - a sane developer - will normally write it in more than one line, but Java will be none the wiser. Take that Java!

Motivation

I was finishing up a project of mine, and as an afterthought, of course, I decided to add Accessibility of navigation in custom views and Accessibility of spoken text. Since I always disliked accessibility additions "uglifying" my XML Layout Resource files, I decided to do it programmatically; however, that typically would only "uglifying" my Java code, and if there is anything I dislike the most, it is ugly Java code.

So for kicks and giggles, I went the Java code path and object-orientified it and was able to produce what you will see below (unless you are a rebel code reviewer and already viewed my code).

Usage

Here is one of the many similar usages that I was able to use this "library" in. This is how you would use it for custom Views; however, it should be able to be used in many other situations, too.

public class ContributionView extends RelativeLayout implements Mappable<Resume.Contribution> {

    // Called from Constructors
    private void init(AttributeSet attrs, int defStyle) {

        root = (ViewGroup) LayoutInflater.from(getContext()).inflate(
                R.layout.internal_contribution_view, this, true);

        ...

        // Declare Navigation Accessibility
        Accessibility.with(root)

                // Disable certain views in the ViewGroup from ever gaining focus
                .disableFocusableNavigationOn(
                        R.id.contribution_textview_creator,
                        R.id.contribution_textview_year)

                // For all focusable views in the ViewGroup, set the D-pad Navigation
                .setFocusableNavigationOn(txtProjectName)
                    .down(R.id.contribution_textview_description).complete()
                .setFocusableNavigationOn(txtContributionDescription)
                    .up(R.id.contribution_textview_name)
                    .down(R.id.contribution_textview_link).complete()
                .setFocusableNavigationOn(txtProjectLink)
                    .up(R.id.project_textview_description).complete()

                // Set which view in the ViewGroup will have be first to be focused
                .requestFocusOn(R.id.contribution_textview_name);

        invalidateView();

    }

    private void invalidateView() {

        ...

        // Declare Content Description Accessibility
        Accessibility.with(root)

                // Set the content descriptions for each focusable view in the ViewGroup

                // Set the content description for the Contribution Name
                .setAccessibilityTextOn(txtProjectName)
                    .setModifiableContentDescription(getProjectName())
                        .prepend("Contribution occurred on the Project called ")
                        .append(String.format(" by %s in %s",
                                getProjectCreator(),
                                getContributionYear())).complete()

                // Set the content description for the Contribution Description
                .setAccessibilityTextOn(txtContributionDescription)
                    .setModifiableContentDescription(getContributionDescription())
                        .prepend("Description: ").complete()

                // Set the content description for the Contribution URL
                .setAccessibilityTextOn(txtProjectLink)
                    .setModifiableContentDescription(getProjectLink())
                        .prepend("URL is ").complete();

    }

    ...

}

So what does this actually do? Well, if you have Talk-back enabled on your Android device, then this code will make sure the user will navigate to the right UI items on the screen. The code in init(...) will disable going to the creator and year UI controls, set the Up and Down navigation of the UI, and set the name is the first navigation item in the view (via requestFocus). The invalidateViews() code will set the updatable spoken text (content descriptions) of the UI items. In particular, it will append and prepend text to give context to what is going to be spoken. Also, since the creator and year UI controls cannot be navigated to, it will have those spoken to when the 'name' UI control is in focus.

Design

I decided early on to have a fluent API that resembles that of Picasso by Square. I have created some similar fluent API's, but this is the first to implement a Step "Builder" pattern if the developer ventures into certain rabbit-holes. Besides those rabbit holes, the API is fairly open for the developer to do as they wish as I didn't want to hinder or assert anything.

I originally used interface; however, I converted them to abstract class that provided specific modifiable variables to be used.

My implemented classes all have the naming convention of ending in Accessibility (i.e. "TextAccessibility" to handle everything dealing with accessibility to text) and the interface / abstract class will describe just one aspect of accessibility (i.e. "Direction" of navigation, or setting "Content Description")

Code Structure

Accessibility enter image description here ViewGroupAccessibility enter image description here DirectionAccessibility enter image description here TextAccessibility enter image description here

Future Extensions As you can see, my code right now is highly dependent on View Group as can be seen in all my classes! I intend to add an addition method in Accessibility with the signature of with(View): ViewAccessibility. It will be similar to ViewGroupAccessibility, and I will expect my classes such as DirectionAccessibility and TextAccessibility to be able to return me to ViewAccessibility.

I know I cannot do that know, and I will have to refactor my code to all such a thing. I am not quite sure how to do that right now, but I believe it is accomplishable while still keeping my code DRY.

Code

GitHub link to library - here

Accessibility

public class Accessibility {
    public static ViewGroupAccessibility with(ViewGroup viewGroup) {
        return new ViewGroupAccessibility(viewGroup);
    }

    // Future Extension

    // public static ViewAccessibility with(View view) {
    //    return new ViewAccessibility(view);
    //}
}

ViewGroupAccessibility

public class ViewGroupAccessibility {

    private ViewGroup viewGroup;

    ViewGroupAccessibility(ViewGroup viewGroup) {
        this.viewGroup = viewGroup;
    }

    public DirectionAccessibility setFocusableNavigationOn(@IdRes int parentViewId) {
        return new DirectionAccessibility(this, viewGroup, parentViewId);
    }

    public DirectionAccessibility setFocusableNavigationOn(View parentView) {
        return new DirectionAccessibility(this, parentView);
    }

    public ViewGroupAccessibility requestFocusOn(int viewId) {
        viewGroup.findViewById(viewId).requestFocus();
        return this;
    }

    public ViewGroupAccessibility disableFocusableNavigationOn(@IdRes int... ids) {

        for (int id : ids) {
            viewGroup.findViewById(id)
                    .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
        }
        return this;

    }

    public ContentDescription setAccessibilityTextOn(View parentView) {
        return new TextAccessibility(this, parentView);
    }

}

DirectionAccessibility

public class DirectionAccessibility extends Direction {

    private ViewGroupAccessibility accessibility;

    DirectionAccessibility(ViewGroupAccessibility accessibility, ViewGroup viewGroup, @IdRes int parentViewId) {
        this.parentView = viewGroup.findViewById(parentViewId);
        this.accessibility = accessibility;
    }

    DirectionAccessibility(ViewGroupAccessibility accessibility, View view) {
        this.parentView = view;
        this.accessibility = accessibility;
    }

    @Override
    public Direction up(@IdRes int viewId) {
        parentView.setNextFocusUpId(viewId);
        return this;
    }

    @Override
    public Direction down(@IdRes int viewId) {
        parentView.setNextFocusDownId(viewId);
        return this;
    }

    @Override
    public Direction left(@IdRes int viewId) {
        parentView.setNextFocusLeftId(viewId);
        return this;
    }

    @Override
    public Direction right(@IdRes int viewId) {
        parentView.setNextFocusRightId(viewId);
        return this;
    }

    @Override
    public Direction focusForward(@IdRes int viewId) {
        parentView.setNextFocusForwardId(viewId);
        return this;
    }

    @Override
    public ViewGroupAccessibility complete() {
        this.parentView.setFocusable(true);
        return this.accessibility;
    }
}

TextAccessibility

class TextAccessibility extends ContentDescription {

    private final ViewGroupAccessibility accessibility;
    private final View parentView;

    TextAccessibility(ViewGroupAccessibility accessibility, View parentView) {
        this.accessibility = accessibility;
        this.parentView = parentView;

        this.appendText = new StringBuilder();
        this.prependText = new StringBuilder();
    }

    @Override
    public ModifiableContentDescription prepend(String prependedText) {
        this.prependText.insert(0, prependedText);
        return this;
    }

    @Override
    public ModifiableContentDescription appendablePrepend(String prependedText) {
        this.prependText.append(prependedText);
        return this;
    }

    @Override
    public ModifiableContentDescription append(String appendedText) {
        this.appendText.append(appendedText);
        return this;
    }

    @Override
    public ModifiableContentDescription prepend(List<String> prependedTextList) {
        for (String text : prependedTextList) {
            this.prependText.append(text);
        }
        return this;
    }

    @Override
    public ModifiableContentDescription append(List<String> appendedTextList) {
        for (String text : appendedTextList) {
            this.appendText.append(text);
        }
        return this;
    }

    @Override
    public ViewGroupAccessibility setContentDescription(String text) {

        // if text is null, then main text will be a string with the value of "null". This will make the screen-reader ignore the text
        this.mainText = new StringBuilder().append(text);
        return complete();

    }

    @Override
    public ViewGroupAccessibility setContentDescription(CharSequence text) {
        return setContentDescription(text.toString());
    }

    @Override
    public ModifiableContentDescription setModifiableContentDescription(String text) {

        if (text != null) {
            this.isModified = true;
        }

        this.mainText = new StringBuilder().append(text);
        return this;

    }

    @Override
    public ModifiableContentDescription setModifiableContentDescription(CharSequence text) {
        return setModifiableContentDescription(text.toString());
    }

    @Override
    public ViewGroupAccessibility complete() {

        String contentDescription = (isModified)
                ? mainText.insert(0, prependText).append(appendText).toString()
                : mainText.toString();

        this.parentView.setContentDescription(contentDescription);

        return this.accessibility;

    }
}

Direction

public abstract class Direction {

    protected View parentView;

    public abstract Direction up(@IdRes int viewId);
    public abstract Direction down(@IdRes int viewId);
    public abstract Direction left(@IdRes int viewId);
    public abstract Direction right(@IdRes int viewId);
    public abstract Direction focusForward(@IdRes int viewId);
    public abstract ViewGroupAccessibility complete();
}

ContentDescription

public abstract class ContentDescription extends ModifiableContentDescription {

    protected StringBuilder mainText;

    public abstract ViewGroupAccessibility setContentDescription(String text);
    public abstract ViewGroupAccessibility setContentDescription(CharSequence text);
    public abstract ModifiableContentDescription setModifiableContentDescription(String text);
    public abstract ModifiableContentDescription setModifiableContentDescription(CharSequence text);
}

ModifiableContentDescription

public abstract class ModifiableContentDescription {

    protected StringBuilder prependText;
    protected StringBuilder appendText;

    protected boolean isModified;

    public abstract ModifiableContentDescription prepend(String prependedText);
    public abstract ModifiableContentDescription appendablePrepend(String prependedText);
    public abstract ModifiableContentDescription append(String appendedText);
    public abstract ModifiableContentDescription prepend(List<String> prependedTextList);
    public abstract ModifiableContentDescription append(List<String> appendedTextList);
    public abstract ViewGroupAccessibility complete();
}
\$\endgroup\$
  • \$\begingroup\$ Some questions I have. Was the move to abstract class appropriate? Should I go back? Should I move the member variables I assign in the constructors to the abstract class? Is the use of StringBuilder good or is that too heavy of an object? @IdRes will make sure the int I pass in is an R.id, but should I include @LayoutRes annotation to some places to allow for R.layout values? 3rd with(...)? In ModifiableContentDescription I have the method name appendablePrepend, is that a good name? Could it be something else? Do you think the append / prepend String Lists are necessary? \$\endgroup\$ – Christopher Rucinski Jul 9 '16 at 12:03
  • \$\begingroup\$ While extending this library to add additional behavior, is there something you see in the above code that I should stop? That I should be doing? This is the first actual library that I will try to ship when it is complete. If I can finish it \$\endgroup\$ – Christopher Rucinski Jul 9 '16 at 12:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.