Edit 4/2013: keiner has the right idea in the comments below – GradientDrawable seems like the way to go to draw shapes, not ShapeDrawable.

Sometimes Android is so crazy. It’s really easy to define a shape via XML (for example, a rounded rectangle with a red background and black border) – see XML code after the jump.

However, if you want to make the same shape with a blue background, that’s another XML file. If you want the blue color to appear only when you press a button, that’s another one to two XML files. If you want to let the user pick the color? You’re kind of stuck. It would be nice to be able to create shapes programatically, right? It’s a lot harder than you’d think. In fact, we spent a couple hours this past Friday figuring this very problem out.

<shape xmlns:android="http://schemas.android.com/apk/res/android" 
    android:shape="rectangle">
	<stroke android:width="2dip"
	    android:color="@android:color/black"></stroke>
	<solid android:color="@android:color/red"></solid>
	<corners android:topLeftRadius="5dip"
	    android:topRightRadius="5dip"
	    android:bottomLeftRadius="5dip"
	    android:bottomRightRadius="5dip"></corners>
</shape>

Just looking at the docs, it would seem that you could write some Java code like this:

RoundRectShape rs = new RoundRectShape(/* radii */);
ShapeDrawable sd = new ShapeDrawable(rs);
button.setBackgroundDrawable(sd);

And, what do you know, we have a shape.

That was the easy part. It’s what you’d want to do from here (i.e. add a stroke) that gets into territory that the Android API is not great at. We started by overriding ShapeDrawable’s onDraw method for custom drawing:

public class CustomShapeDrawable extends ShapeDrawable {
    private final Paint fillpaint, strokepaint;
 
    public CustomShapeDrawable(Shape s, int fill, int stroke, int strokeWidth) {
        super(s);
        fillpaint = new Paint(this.getPaint());
        fillpaint.setColor(fill);
        strokepaint = new Paint(fillpaint);
        strokepaint.setStyle(Paint.Style.STROKE);
        strokepaint.setStrokeWidth(strokeWidth);
        strokepaint.setColor(stroke);
    }
 
    @Override
    protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
        shape.draw(canvas, fillpaint);
        shape.draw(canvas, strokepaint);
    }
}
 
// ... snip ...
 
RoundRectShape rs = new RoundRectShape(new float[] { 10, 10, 10, 10, 10, 10, 10, 10 }, null, null);
ShapeDrawable sd = new CustomShapeDrawable(rs, Color.RED, Color.WHITE, 20);
button.setBackgroundDrawable(sd);

It makes plenty of sense – and also looks nasty.

What happened?

1. The fill (red) is cut off – no bottom rounded corners

It turns out that the size of the canvas is your whole phone – Android applies a clip boundary to indicate how big your actual view is. In order to make your shape fit, you have to resize it:

shape.resize(canvas.getClipBounds().right, canvas.getClipBounds().bottom);

2. The stroke (white) is cut off

In Android, when you draw with a stroke, it draws the center of the stroke at the boundaries of the shape you are drawing. As you can see, the stroke is getting cropped by the boundaries of the image. Luckily, you can perform transformations on a canvas. What we want to do is transform our stroke shape to be slightly smaller than the boundary – and there’s a Matrix operation for that!

matrix.setRectToRect(new RectF(0, 0, canvas.getClipBounds().right, canvas.getClipBounds().bottom),
    new RectF(strokeWidth/2, strokeWidth/2, canvas.getClipBounds().right - strokeWidth/2,
        canvas.getClipBounds().bottom - strokeWidth/2),
    Matrix.ScaleToFit.FILL);

Also, note that you can’t just resize the stroke – because when you shrink the stroke, it becomes the correct stroke for a smaller-sized shape:

Notice the red corners peeking out. Turns out you have to resize both the stroke, and the shape to stay centered within the stroke.

Our new custom drawing logic:

protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
    shape.resize(canvas.getClipBounds().right,
            canvas.getClipBounds().bottom);
    shape.draw(canvas, fillpaint);
 
    Matrix matrix = new Matrix();
    matrix.setRectToRect(new RectF(0, 0, canvas.getClipBounds().right,
                canvas.getClipBounds().bottom),
            new RectF(strokeWidth/2, strokeWidth/2, canvas.getClipBounds().right - strokeWidth/2,
                    canvas.getClipBounds().bottom - strokeWidth/2),
            Matrix.ScaleToFit.FILL);
    canvas.concat(matrix);
 
    shape.draw(canvas, strokepaint);
}

How does it look? Glad you asked:

Finally, we achieve what was originally a 10-line XML file. Android is defintiely making us work for it. Now, if you want your button to change color when you press it, StateListDrawable is your friend.

RoundRectShape rs = new RoundRectShape(new float[] { 10, 10, 10, 10, 10, 10, 10, 10 }, null, null);
ShapeDrawable sdOff = new CustomShapeDrawable(rs, Color.RED, Color.WHITE, 20);
ShapeDrawable sdOn = new CustomShapeDrawable(rs, Color.BLUE, Color.WHITE, 20);
 
StateListDrawable stld = new StateListDrawable();
stld.addState(new int[] { android.R.attr.state_enabled }, sdOff);
stld.addState(new int[] { android.R.attr.state_pressed }, sdOn);

Putting it all together, you could make yourself a custom view like the one at the beginning. Pretty neat, wouldn’t you say? I wonder how easy it is for iOS to do custom shape drawing. (alternatively, “why would you want to draw custom shapes??!!” :) )