Tags
android, attributes, background, custom attributes, custom state, custom-ui, dark themes, drawable, drawable state, mozilla, namespace, private browsing, private browsing mode, private mode, selector, state, state list drawable, themes
As a part of the ongoing work on private browsing mode, we had to come up with a way to easily flip the background between light and dark themes. It’s a known fact that though Android supports themes, they cannot be switched on the fly. If we take a step back and look at Android, views can show different backgrounds for selected, pressed, focused, and enabled states. These can be provided as a StateListDrawable
in XML, and Android knows to apply the actual background based on the current state.
But, could we reuse an existing state to convey the private browsing mode? This would not go well with the actual meaning of the predefined Android states. So, could we create a new state for private mode? Oh, yes!
We need a custom attribute that can be used in other XML files that describes the background. This is added in res/values/attrs.xml
.
<resources> <declare-styleable name="PrivateBrowsing"> <!-- name of the attribute that will be used in XML files --> <attr name="state_private_mode" format="boolean"/> </declare-styleable> </resources>
We can now add this attribute to a StateListDrawable
, that defines the background of the view. Let’s have it in some res/drawable/background.xml
.
<!-- Note: The "state_private_mode" attribute is defined in our application's scope. Let's call it "sirius". --> <selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:sirius="http://schemas.android.com/apk/res/com.sriramramani.just.another.app"> <!-- private browsing mode --> <item sirius:state_private_mode="true" android:color="_some_dark_color_" /> <!-- normal mode --> <item android:color="_some_light_color_"/> </selector>
Now this background be specified in the layout XML just like any other drawable. This doesn’t end here. We’ve specified a way to add a custom attribute, and have used it in our background. Though the app will compile fine, Android doesn’t know when to apply this state. It’s the responsibility of the custom view to inform Android, that it’s state has changed and request it to show a new background. Is that hard? Let’s create a new custom button, PrivateModeButton, that’s a Button at heart but supports private browsing mode, by providing a method setPrivateMode()
.
public class PrivateModeButton extends Button { // (Combination of) States are usually specified as an array. // Our custom attribute will be generated as R.attr.state_private_mode. // Note: This is in our app's scope. private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private_mode }; // The view needs a way to know if it's in private mode or not. private boolean mIsPrivate = false; public PrivateModeButton(Context context, AttributeSet attrs) { super(context, attrs); } // Android calls this method to know the current drawable state of the view. // It starts with an "extraSpace" of 0 in View.java, and each inherited view adds its new state. // We add just one more state, hence, we create a new array of size "extraSpace + 1". @Override public int[] onCreateDrawableState(int extraSpace) { // Ask the parent to add its default states. final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); // If we are private, add the state to array of states. // If not added, the value will be treated as false. // mergeDrawableStates() takes care of resolving the duplicates. if (mIsPrivate) mergeDrawableStates(drawableState, STATE_PRIVATE_MODE); // Return the new drawable state. return drawableState; } // We need a way for the Activity (or some other part of the code) // to enable private mode for the view. public void setPrivateMode(boolean isPrivate) { // If we flip the current state of private mode, record the value // and inform Android to refresh the drawable state. // This will in turn invalidate() the view. if (mIsPrivate != isPrivate) { mIsPrivate = isPrivate; refreshDrawableState(); } } }
Voila! We can now call myPinkButton.setPrivateMode(true);
to show a black button instead. Facebook can have a LikeButton
with a custom attribute state_liked
, or Twitter can have a FavoriteButton
that exposes setFavorite()
which can add a small little star at the top right. From Java’s perspective, it’s a property of the view. From XML perspective, it’s just another state!
P.S.: Thanks to Wes (wesj) for pointing me out to try using custom states. 🙂
Pingback: View Reduction | Sriram Ramani
I am trying for a couple of days to have different states for a row in a listview. I do not understand how this works.
For example:
imagine a list with 100 songs, every row shows the name of the song.
if I click a row I want to show a “buffering” state, and then a “playing” state, and then after that song is done playing, a “visited/played” state.
how can I do this?
PS: I tried using a custom layout for each row, (extended RelativeLayout and implemented Checkable interface). but did not work out, and each row had a layout xml which defined some decorations that were supposed to be shown in those diferent states…
Let’s say the buffering, playing, visited/played are denoted by icons. You could have a custom ImageView, say SongStateView, that can support 4 different states. Also, if you make your SongStateView duplicate it’s parent’s state, then you could set the state at the list-view row level instead of the individual ImageView.
Nice article!
Anyway I’m more or less in the same condition of Marius Pena but I have a different problem:
List of item, each item is a LinearLayout with a simple TextView in it. I extended the LinearLayout to implement the setMyCustomState() function and set the duplicateParentState=true” attribute to the TextView. All the rest as described in your article, but when I add the state_custom to the selector (both for the LinearLayout background and for the TextView textColor) I’m able to have it working only for the “false” state! Even if I try to set the property by default in the xml (es. app:state_custom=”true” – another nice thing to mention by the way).
Do you have any clue what the problem could be?!
I am trying apply this technique into my next app so it can support switching light theme and dark theme on fly. So far I added custom state on all view and everything work fine. But one thing I worried is custom-state view may consume memory twice than normal view because additional drawables. Is it true or not ?
I am not really sure. Android uses caching for drawables. It evicts drawables that are not in use with the new one requested. So, even if your base xml has has two different drawables based on the state, I think only one will be in memory, based on the caching.
This seems a solution to my problem, but it seems a little bit hackish to me that to change the value of the attribute (mIsPrivate) we need to add it or not to the current states. Wouldn’t it be better to be able to set the value to the attribute? Is it possible?
Pingback: 自定义按钮状态(Custom States) | 47Log
Pingback: 안드로이드 Selector에서 Custom States 만들기 – 꿈꾸는 개발자의 로그