Tags

, , , , ,

Android’s user interface changed constantly from Froyo to JellyBean. One important widget that changed in almost every single version of the OS is TextView. From the one that had shadow in Froyo to just a underline in ICS, the TextView had various incarnations. How do we ensure proper styling of TextView across all platforms? To add to the complexity, different vendors have their own color schemes. When we started with Firefox for Android, we wanted similar shapes on all devices, however the highlight on it should come from the device’s highlight color. A normal 9.png cannot solve this issue, as it would have just one constant highlight color. Howe do we solve this?

Android provides the ability to use LightingColorFilter to add a color mask on an image. We used the same to hack the 9.png images, to change the color based on the device. It’s assumed that the TextView has different states. The following is drawable used for the pressed state.

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- A masking layer on top of the background - Usually in black -->
    <item android:id="@+id/outline"
          android:drawable="@drawable/outline"/>

    <!-- The background used in normal state -->
    <item android:id="@+id/background"
          android:drawable="@drawable/background"/>
</layer-list>

When we used this as a StateListDrawable for the TextView, Android will show a black outline (as our outline is black). This is not what we wanted. We wanted a color to be applied on top. Let’s extend StateListDrawable for our convenience.

public class MyStateListDrawable extends StateListDrawable {
    private LightingColorFilter mFilter;
    public void initializeFilter(int color) {
        // Use complimentary color of the color we used for LayerDrawable's outline layer.
        mFilter = new LightingColorFilter(Color.WHITE, color);
    }

    @Override
    protected boolean onStateChange(int[] stateSet) {
        for (int state: stateSet) {
            if (state == android.R.attr.state_pressed || state == android.R.attr.state_focused) {
                // Ask StateListDrawable to choose the proper state.
                super.onStateChange(stateSet);

                // We made it a LayerDrawable. Now take the topmost layer and apply the filter.
                ((LayerDrawable) getCurrent()).getDrawable(0).setColorFilter(mFilter);
                return true;
            }
        }

        return super.onStateChange(stateSet);
    }
}

As you can see, we ask Android to select the appropriate state list drawable with a call to the super(). This will have the black outline. Ask we know it’s a LayerDrawable, we select the outline layer, and apply the filter to it. This will change the color of the outline. One important thing to note here is that the outline and background should be two separate layers. If not, the filter will be applied to the background too, resulting in artifacts. How do we apply this to our view? Simple!

MyStateListDrawable states = new MyStateListDrawable();

// Initialize the filter with the color we want -- in this case, the highlight color of the system.
states.initializeFilter(filter_color);
states.addState(new int[] { android.R.attr.state_focused }, resources.getDrawable(R.drawable.drawable_for_pressed));
states.addState(new int[] { android.R.attr.state_pressed }, resources.getDrawable(R.drawable.drawable_for_pressed));
states.addState(new int[] { }, resources.getDrawable(R.drawable.drawable_for_default));

// Set it as background to the text view.
mText.setBackgroundDrawable(states);

And how do we get the highlight color from the device?

TypedArray typedArray;
if (Build.VERSION.SDK_INT >= 11) {            
    typedArray = mContext.obtainStyledAttributes(new int[] { android.R.attr.textColorHighlight });
} else {
    ContextThemeWrapper wrapper  = new ContextThemeWrapper(mContext, android.R.style.TextAppearance);
    typedArray = wrapper.getTheme().obtainStyledAttributes(new int[] { android.R.attr.textColorHighlight });
}

highlight_color = typedArray.getColor(typedArray.getIndex(0), 0);
typedArray.recycle();
Advertisements