GradientDrawable on Android

Drawable represents something that can be drawn on screen on Android. There’re many Drawable types. This article covers GradientDrawable, which can also be defined in xml with <shape> element.

Confusingly there’s also ShapeDrawable, which the Android documentation and training guide claims to be defined in xml with <shape> element. This is not right and one can look at ShapeDrawable source code to confirm that.

The following screenshot captures what can be done with GradientDrawable. The source can be found here.

Figure 1. Drawings Generated by Android GradientDrawable

Shape

And the table below shows some elements are not available for certain shapes.

Element\Shape Line Rectangle Oval Ring
corners NA NA NA
Linear gradient NA
Radial gradient NA
Sweep gradient NA
padding
size
solid NA
stroke

We’ll first introduce the four shapes, and then go through some of the important xml elements.

Line

There’s not much use when shape is line, because lots of elements are not supported and all the effects we want to draw with line shape can be done with rectangle.

One thing to note that is when specifying a dashed stroke, it won’t work properly due to bug here. Suppose we’re drawing the line on ImageView, the following code is needed.

 imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

Rectangle

Rectangle is used to draw a rectangle-like shape. It’s the only shape can have corners elements, which makes it very powerful. The corners element allows one to specify the radius at four corners of the rectangle.

One can use rectangle shape to draw a line by specifying a small height, or draw a circle by specifying proper corner radius.

Oval

Oval can be used to draw a circle or oval.

Ring

Ring is used to draw a ring with specified thickness. innerRadius, innerRadiusRatio, thickness, thicknessRatio, useLevel attributes only applies when the shape attribute is ring.

innerRadius is used to specify the width of the inner hole of the ring. innerRadiusRatio is to specify the innerRadius as the ratio of the ring’s width. For example, innerRadiusRatio=”5” means innerRadius is equal to the ring’s width divide by 5.

Thickness defines the thickness of the ring. thicknessRatio is similar to innerRadiusRatio.

Gradient

There are three gradient types supported, namely linear, radial and sweep. One can refer to figure 1 for the drawing can be created by them.

All gradients must specify startColor and endColor. centerColor is optional and its relative position is also configurable via centerX and centerY (both in range of (0, 1.0)).

Linear

A linear gradient, which specifies a starting gradually transforms into ending color. It is also the default gradient. Linear gradient has a special attribute named angle. It must be a multiple of 45 and in the range of [0, 315].

Figure 2. Angle of Linear Gradient

Radial

Radial defines a gradient in a fan out fashion. It has a special attribute gradientRadius, which must be defined.

Padding

Padding is applied to the containing View element (this pads the position of the View content, not the shape).

For example, if this oval is set to the background of an ImageView, and the paddings will be applied to position the image resource (view content) of the image view.

Size

Defines the shape’s width and height. The shape scales to the size of the container View proportionate to the dimensions defined here by default. When you use the shape in an ImageView, you can restrict scaling by setting the android:scaleType to “center”.

References

  1. GradientDrawable Android doc
  2. Android drawable resource shape

Android LayerDrawable and Drawable.Callback

LayerDrawable is a Drawable that manages an array of Drawables, where each Drawable is a layer of it. It can be used to compose fancy visual effects. However, used incorrectly, it can also introduce difficult to catch bugs. This post discusses a bug that can be caused by the Callback mechanism of Drawable.

Chain of Callbacks

In LayerDrawable, every layer/drawable registers the LayerDrawable as its Drawable.callback. This allows the layer to inform the LayerDrawable when it needs to redraw. As shown in the invalidateSelf method from Drawable.java, the DrawableCallback.invalidateDrawable is called to inform the client (the LayerDrawable, in this case) the drawable needs to redraw.

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

 

Also, a View registers itself as Drawable.callback, so when the drawable needs to redraw, the View can be informed and invalidated. So if we set background of a View to be LayeredDrawable, we have a chain of DrawableCallbacks. This is illustrated in the figure below.

1.jpg

Figure 1. Chain of Callbacks when Setting LayerDrawable as View’s Background

When the Drawable needs to redraw, it calls its callback.invalidateDrawable, which in turns calls LayerDrawable’s callback.invalidateDrawable, which causes the View to redraw the corresponding region if necessary.

View Breaks Callback of Old Background Drawable

So far all work well, but the setBackgroundDrawable of View.java has the following code snippet.

public void setBackgroundDrawable(Drawable background) {
    ...

    /*
     * Regardless of whether we're setting a new background or not, we want
     * to clear the previous drawable.
     */
    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    …
    if (background != null) {
        background.setCallback(this);
    }
    ...
}

This means setting a new background will break the old background drawable’s callback by setting it to null, regardless whether the callback is still set to the View.

The Bug

With all the knowledge above, we can “fabricate” a bug by follow the steps below.

  1. Set a Drawable A as a background of a View V. The A.callback is set to V.

  2. Create a LayerDrawable L with A as one layer. Now A.callback is set to L.

  3. Now set another Drawable (or just null) as background of V. V will set callback of its old background (A in our example) to null, which breaks the link between A and L.

  4. Updates A won’t trigger L to update.

The solution to this issue is to update the background of V before creating L. This is illustrated in the code example here.

In the example, we tried animating the actionbar background. The background consists of two layers, the first one is a green ColorDrawable, and second a bitmap drawable. We want to animate the alpha value of the bitmap drawable, so it appears the image is faded in.

The code contains two methods, one works and the other doesn’t due to the bug we described. Each method can be triggered by a button. Below we extract the key part of the code, one can refer to the link for more details.


Button btn1 = (Button) findViewById(R.id.button1);
btn1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarWorking();
  }
});
Button btn2 = (Button) findViewById(R.id.button2);
btn2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarNotWorking();
  }
});

private void animateActionBarNotWorking() {
	Drawable[] layers = new Drawable[] { colorLayer, launcherIconDrawable };
	// 2. This sets launcherIconDrawable.callback to layerDrawable
	LayerDrawable layerDrawable = new LayerDrawable(layers);
	// 3. This sets launcherIconDrawable.callback to null
	actionBar.setBackgroundDrawable(layerDrawable);
	ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 255);
	valueAnimator.setDuration(1000);
	valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
	  @Override
	  public void onAnimationUpdate(ValueAnimator animation) {
		// 4. Updates launcherIconDrawable will not trigger action bar background to update
		// as launcherIconDrawable.callback is null
		launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue());
	  }
	});
	valueAnimator.start();
}

private void animateActionBarWorking() {
	actionBar.setBackgroundDrawable(null);
	animateActionBarNotWorking();
}

 

References:

  1. LayerDrawable: http://developer.android.com/reference/android/graphics/drawable/LayerDrawable.html