Thursday, June 12, 2014

Extending ListPreference to use custom RadioButton drawables

I did some pretty cool stuff today.  Actually, I only did one cool thing, since it took me basically the entire day to get everything working.  I wanted to add color previews to the theme chooser in my settings menu, but I didn't just want to add a colored square next to the name.  I wanted to replace the default RadioButton with a custom icon that would represent the color of the theme.  So I had to learn all about custom Drawables... although in retrospect it probably would have been easier to just make an image.  Oh well, I guess I still learned a lot.

So this was pretty much the first thing that I had trouble researching online.  Turns out, not many people want to do something like this.  I will post the code and explain the problems I ran into, and hopefully they may end up helping someone.  The end result can be seen right over there...  -->

Let me start off with the drawable.  It uses two layer-lists combined in a selector, so that the icon can have "checked" and "unchecked" states.

theme_radio_checked.xml
 @drawable/check is an image containing the gray check mark.  The file for the unchecked drawable is similar, but uses a blank image of the same size.  I went with a white background so that later when I use a color filter to change the icon color, it produces the exact color I want, so I don't have do mess around with all the filter types.  These two drawables are then combined with a selector:

theme_radio_selector.xml
theme_list_row.xml
 Most of that is pretty simple, but I'd like to point out the last 4 attributes of the RadioButton:

android:focusable="false"
This is needed because if you have something in the row that can take focus, you will not be able to select the row.  This makes sense for things like buttons, but I want them to be able to click anywhere on the row to select it.
 
android:button="@null"
android:background="@null"
android:drawableLeft="@drawable/theme_radio_selector"
At first glance, this probably seems like a silly way to add the drawable to the button.  I ended up doing it this way to address two problems that I ran into:

1.) If I assign the drawable to the button, there is no way (that I could find) to access the drawable in the code.  I needed to access it to dynamically change the color, so that wouldn't work.
2.) If I assign the drawable as the background, it sometimes becomes distorted to fill its spot in the layout.  I couldn't figure out any way to force a square layout aside from creating a custom View, which I didn't want to do.

So by turning the button into a compound drawable, I am able to both keep it square and dynamically change the color.  Awesome.  Finally, I created a custom Preference extending from ListPreference to handle the color changing.  The only thing I had to override was the onPrepareDialogBuilder function:

ThemeListPreference.java
(Sorry about the formatting, one of these days I'll get around to editing the CSS of this blog. EDIT: Told you!)

 The array adapter handles mapping the entry array to the ListView, but I also had to overload its getView function to change the view as I needed.  I discovered that I had to manually check the selected radio button.  I then got the drawable with getCompoundDrawables()[0], and gave it a color filter based on which element it was.

Before I implemented the selector, I was able to use ((LayerDrawable) button.getBackground()).getDrawable(0) to change the color of only the background layer, (leaving the check mark pure gray instead of tinting it along with the background,) but I couldn't find a way to get the drawable for a single state of the StateListDrawable.  Oh well, I actually like it better this way.

So that's really all there is to it.  Seems a lot simpler now that I have it all written out, haha.  Hopefully someone finds that helpful. 

EDIT: I discovered a bug.  With the above code, if you click on the drawable itself, it will appear to switch to the "checked" state, but it won't actually do anything.  This is easily fixed by adding the android:clickable="false" attribute to the RadioButton.

3 comments:

  1. Thanks for this very clear explanation! Most of the sample code I found is gibberish... I need a custom list preference, each list item contains a thumbnail graphic and 3 lines of text. I followed your code, modified to my data, and it worked first time! Well, almost - charsequence above needs to be CharSequence but Eclipse suggested the fix.

    One thing I notice is I'm not getting a separator line between list items. Is that defined in your style somewhere? I've tried a few things and will keep banging on it.

    ReplyDelete
  2. Thank for the explanation. Please can you share the sample app for this. I am looking for displaying multiple lines of text in the ListPreference and do you any info on how to do this?

    ReplyDelete
  3. I got a problem trying this: android.view.InflateException: Binary XML file line #14: Binary XML file line #14: Error inflating class RadioButton
    Caused by: android.view.InflateException: Binary XML file line #14: Error inflating class RadioButton
    Caused by: android.content.res.Resources$NotFoundException: Drawable com.ex.sf:drawable/theme_radio_selector with resource ID #0x7f02009a
    Caused by: android.content.res.Resources$NotFoundException: File res/drawable/theme_radio_selector.xml from drawable resource ID #0x7f02009a

    ReplyDelete