Tags

, , , , , , , , , , , , , , , , ,

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. 🙂

Advertisement