(dev) Spinners and accessibility (SuperSU)
Posted on 2013-06-15, 6 comments, 60 +1's, imported from Google+/Chainfire

NOTICE: This content was originally posted to Google+, then imported here. Some formatting may be lost, links may be dead, and images may be missing.

I've never been a big fan of Android's Spinner widget (aka dropdown button or combobox) as it is pretty annoying to work with, and on now ancient Android versions they somehow just never came out the size you expected them to be (sucks for alignment) or would cut off (oversized) content in less than ideal places.

But, nowadays they do look nice and often they are (visually) the right tool for the job.

The original problem

A Spinner will fire the OnItemSelectedListener callbacks when you set the selection manually through one of the setSelection() methods (often done in initialization code), and if you did not set any selection in initialization it will fire the callback as if the first item was selected.

All in all, that is not that weird by itself. It is however often impractical if the callbacks expect to only be triggered by user interaction, as is often the case. Of course, the callback itself would ideally be written in such a way it is able to handle repeated callbacks with the same values, or unexpected callbacks triggered by Android itself rather than user interaction - but often this will not be the case.

The suggested solutions

As these are common issues, there are common solutions. A common work-around to the callback being automatically fired for the first item in the adapter is to count the number of times the callback has fired. A common work-around to the callback being fired after the selected item has been set by code is by not attaching the callback immediately, but rather calling setOnItemSelectedListener() in a Runnable posted to the main thread's handler.

If you've implemented either of these solutions in your code, you've probably not been happy about it due to both being inelegant and weird, and for good reason.

If you look around, you will see both solutions suggested often on forums, StackOverflow, etc. But both solutions make assumptions that ends up being false: namely that the callback will only be called with the "unwanted" values once, or before the next layout has been completed.

When they break - enter accessibility

While the proposed solutions will often work as advertised, when accessibility services are enabled, the previously mentioned assumptions do not hold.

Those who use accessibility apps (quite a few people, as apps may use it for other reasons like spying on notifications) will no doubt be aware sometimes weird things happen. This is one of these cases.

When accessibility services are enabled, the OnItemSelectedListener callbacks may fire multiple times (with the same parameter values), and after the next layout has completed. This can lead to unexpected behavior in apps using either of the suggested solutions.

And this is indeed what happened with SuperSU. I don't normally elaborate in detail on SuperSU bugs, but I thought this one deserved comment, as it caused some very weird behavior and I was unable to find the problem until somebody figured the problem only occurred with accessibility services enabled.

My solution

I wanted a quick and easy solution that would require minimal code changes and be more or less drop-in.

At first I went for writing a new Spinner subclass that would take care of the issue. This ended up being hard to do in little code, without resorting to walking a stack trace to figure out where calls came from and what to do with them (ugh!).

As such I decided to go for a helper class instead, which neatly got rid of that problem - as the helper class would be called by you, and the real Spinner would be called by Android.

The helper class proxies the (my) most used calls on the Spinner class, so you can just replace the Spinner variables in your code:

Spinner sp = (Spinner)findViewById(...)

becomes:

SpinnerHelper sp = new SpinnerHelper(findViewById(...))

The helper class will keep track of which item you select from your code, and proxy the OnItemSelectedListener callbacks so they're only called when a different value is passed.

For my own usage of Spinner, this neatly works around the mentioned issues and works fine with accessibility as well.

The full code and further comment can be found in the linked StackOverflow answer. I don't normally use StackOverflow but as I got the original work-arounds from there I decided to post my answer there as well. If you have an account feel free to vote up my solution if you like it :)


Android: How to keep onItemSelected from firing off on a newly instantiated Spinner

+160
Kiran Rao commented on 2013-06-16 at 03:06:

I had a somewhat related problem - with Switches and Checkboxes. These widgets represented a setting on my server. When the app starts I GET the value from the server and programmatically set the on/off states of these widgets. When the user changes the state, I POST to my server.

I found a solution on stack overflow that I'm using in my apps. It basically adds a setCheckedProgrammatic method to the appropriate CompoundButton class. All this method does is temporarily disable the onCheckedChangeListener before setting the checked state.

An example implementation is here: https://github.com/curioustechizen/android-hybridchoice/blob/master/src/com/github/curioustechizen/hybridchoice/EnhancedCheckBox.java

A similar approach could be used in case of Spinners as well.

陈英 commented on 2013-06-16 at 03:46:

Chainfire commented on 2013-06-16 at 08:54:

+Kiran Rao if you used that method for Spinners you'd still get rogue event fires

عبدالله العنزي commented on 2013-06-20 at 08:42:

ى

Kiran Rao commented on 2013-06-20 at 13:50:

+Chainfire  You're right. I vaguely remember facing the same problem with my Checkbox example (or I think it was a Switch). The problem was that the initial state of the CompoundButton (whether it is on or off) is set at some point and that always triggered an event.

IIRC I solved the problem by initializing the listener in onStart rather than in onCreate/onCreateView. I'll have to dig into my code and confirm that though.

Cristan Meijer commented on 2017-05-23 at 13:15:

You are a hero! If it weren't for you, an app with 1M+ downloads wouldn't have functional filters when accessibility is enabled.

I do like my annotations, so I annotated onItemSelected and onNothingSelected with @Override and annotated the parameters of the constructor, setOnItemSelectedListener and setAdapter with @Nullable / @NonNull. (after which I've realized I like Kotlin better and converted the whole thing to Kotlin :P)

This post is over a month old, commenting has been disabled.