Tags

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

Lightweight themes are basically images aligned to the top-right corner of the desktop window. When we applied the same to the mobile, the top-right corner is actually holding two buttons. Applying the image directly would remove the affordance of the buttons. Hence we wanted to bring the texture of the button with the image applied as a translucent layer on top of them. The way to achieve the translucent effect is described here. This is combined with a texture using a LayerDrawable. This approached worked fine until the JellyBean MR1 release (Android 4.2.1 or version 17). The LayerDrawable broke in the latest release removing the affordance from the buttons.

Mutations

Going through the changesets in the Android codebase, the only new change to the Drawables in three years was related to mutate(). However this couldn’t all of a sudden affect how a drawable is drawn. What could be the problem? As it turns out, LayerDrawable tries to get a ConstantState of the drawable in its constructor. Our implementation of a LightweightThemeDrawable relied on a BitmapDrawable for the same. And since BitmapDrawable doesn’t know that it’s draw() is going to be overriden, it is going to give it’s own ConstantState, which wouldn’t add translucency.

Now the visible solution in hand is to implement the ConstantState for the LightweightThemeDrawable. This drawable is going to be used for just one button. The bitmap is not going to be shared between the buttons as it is not a texture. Do we really need to maintain a constant state? What other option could save us here? Let’s look at how this drawable would be, if defined in XML.

<layer-list>

    <!-- The translucent drawable, with transparency applied by overriding draw() -->
    <item android:drawable="@drawable/_the_theme_image_"/>

    <!-- The texture beneath -->
    <item android:drawable="@drawable/_repeating_texture_bitmap_"/>

</layer-list>

Is there a way to combine these layers into one? When given a Canvas, we need to paint the texture once, and then a bitmap. Since we are creating a custom drawable for translucency, we can reuse the same to draw the texture too. The good news is that the translucency can be achieved in a even better way if we use Shaders. Shaders are more like Photoshop brushes. We could use a pattern and paint over the canvas in Photoshop using a brush. The same holds for Canvas in Android too.

A repeating shader for Bitmap is easy. We need to create a BitmapShader that holds the repeating bitmap to be drawn. The snippet below checks to see if the textureId specified is a texture in drawable-nodpi/ or a BitmapDrawable that already defines how to repeat the texture. In the first case, the texture is repeated in both directions. In the second case, the texture is repeated as specified in the drawable.

    public void setTexture(int textureId) {
        Shader.TileMode modeX = Shader.TileMode.REPEAT;
        Shader.TileMode modeY = Shader.TileMode.REPEAT;

        // The texture to be repeated.
        Bitmap texture = BitmapFactory.decodeResource(mResources, textureId);

        if (texture == null) {
            // Texture may be used inside a BitmapDrawable.
            Drawable drawable = mResources.getDrawable(textureId);
            if (drawable != null && drawable instanceof BitmapDrawable) {
                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
                texture = bitmapDrawable.getBitmap();
                modeX = bitmapDrawable.getTileModeX();
                modeY = bitmapDrawable.getTileModeY();
            }
        }

        // Set the shader for the texture paint.
        if (texture != null) {
            mTexturePaint = new Paint();
            mTexturePaint.setAntiAlias(true);
            mTexturePaint.setShader(new BitmapShader(texture, modeX, modeY));
        }
    }

As mentioned before, the translucency can be achieved in one go with Shaders. A ComposeShader takes two shaders and combines them using a PorterDuff-Mode. With the bitmap’s shader as the DST and the LinearGradient as the SRC, we can use a DST_IN mode to get the translucency effect.

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);

        // A bitmap-shader to draw the bitmap.
        // mBitmap is usually passed as an argument for the constructor.
        // Clamp mode will repeat the last row of pixels.
        // Hence its better to have an endAlpha of 0 for the linear-gradient.
        BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        // A linear-gradient to specify the opacity of the bitmap.
        // mStartColor and mEndColor have just the alpha bits set to required value. E.g.: 0xAA000000.
        LinearGradient gradient = new LinearGradient(0, 0, 0, mBitmap.getHeight(), 
                                                     mStartColor, mEndColor, Shader.TileMode.CLAMP);

        // Make a combined shader -- a performance win.
        // The linear-gradient is the 'SRC' and the bitmap-shader is the 'DST'.
        // Drawing the DST in the SRC will provide the opacity.
        mPaint.setShader(new ComposeShader(bitmapShader, gradient, PorterDuff.Mode.DST_IN));
    }

We have a shader for the texture and a shader for the bitmap. All we need to do.. is paint them!

    @Override
    public void draw(Canvas canvas) {
        // Draw the texture.
        canvas.drawPaint(mTexturePaint);

        // Draw the bitmap.
        canvas.drawPaint(mPaint);
    }

And we have the solution! In fact, the base layer needn’t be a texture. We could just paint a color and paint the bitmap over it. Shaders can be used to do many other effects. What would setting up an alpha based shader, like RadialGradient, from transparency to black, and drawing it on a bitmap look like? A vignette effect! What would adding a red LinearGradient for a shader and using a MULTIPLY look like? A saturated sun-set effect! Have you started dreaming of implementing Instagram filters already? The greatest advantage over my previous post is that this is all drawn by Paint in one go. We don’t need offscreen bitmap processing to worry about handling opaque areas.

Note: Painting the Canvas twice could result in overdraw. This complex effect of painting over a texture wouldn’t be needed in most applications. In that case, a ComposeShader will result in a single time painting. In case you would want to combine the two shaders above, you could do that. It might work.

About these ads