Monday, January 30, 2012

Android Design Tip: Drop shadows on ListViews

Today the designer on my project showed me a comp with a nice drop shadow between two views that gives the illusion that one is sort of behind the other after an animation is run to reveal it.  He gave me a nice 9 patched image file to use, nothing to it right?  Well, not exactly.  So the the view that the drop shadow is meant to be applied to contains a ListView.  And the drop shadow is a narrow vertical line running down the right hand side of the ListView.

Failed attempt #1: Add an ImageView in my layout with the ListView such that it's aligned with the parents top, right and bottom.  And thinking I was being clever, I even added it last in my layout, so it would be "on top".  Well for most views this is probably good enough, but when I ran it, I couldn't see my drop shadow.  You may have already guessed why.  The list item views in my ListView are drawn last, over top of my drop shadow.

So after a bit of reading up on ListView and well AdapterView in general.  I found the method I needed to override.  dispatchDraw(Canvas).  This allows us to draw whatever we want over top of our AdapterView after all it's child views have been drawn!

So long story short, just sub class ListView and override dispatchDraw(Canvas) to get the desired effect.  First, here is a screen cap of my result.

What I ended up doing is creating some custom attributes for left, right, top, bottom as booleans to indicate if a drop shadow should be drawn, then also which drawable to use for each side.  Here is my attrs.xml


 
     
     
     
     
     
     
     
     
 


And here is my custom ListView subclass.
public class DropShadowListView extends ListView {

    private static final int LOC_LEFT = 0;
    private static final int LOC_TOP = 1;
    private static final int LOC_RIGHT = 2;
    private static final int LOC_BOTTOM = 3;

    private int mDropShadowRightId;
    private int mDropShadowLeftId;
    private int mDropShadowTopId;
    private int mDropShadowBottomId;

    private boolean mDropShadowRight;
    private boolean mDropShadowLeft;
    private boolean mDropShadowTop;
    private boolean mDropShadowBottom;

    public DropShadowListView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DropShadowListView);

        mDropShadowRightId = a.getResourceId(R.styleable.DropShadowListView_dropShadowRightSrc, -1);
        mDropShadowLeftId = a.getResourceId(R.styleable.DropShadowListView_dropShadowLeftSrc, -1);
        mDropShadowTopId = a.getResourceId(R.styleable.DropShadowListView_dropShadowTopSrc, -1);
        mDropShadowBottomId = a.getResourceId(R.styleable.DropShadowListView_dropShadowBottomSrc, -1);

        mDropShadowRight = a.getBoolean(R.styleable.DropShadowListView_dropShadowRight, false);
        mDropShadowLeft = a.getBoolean(R.styleable.DropShadowListView_dropShadowLeft, false);
        mDropShadowTop = a.getBoolean(R.styleable.DropShadowListView_dropShadowTop, false);
        mDropShadowBottom = a.getBoolean(R.styleable.DropShadowListView_dropShadowBottom, false);
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.widget.ListView#dispatchDraw(android.graphics.Canvas)
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        Bitmap bm;
        if (mDropShadowRight) {
            bm = getBitmap(mDropShadowRightId);
            canvas.drawBitmap(bm, null, getDropShadowArea(LOC_RIGHT, canvas, bm), null);
        }

        if (mDropShadowLeft) {
            bm = getBitmap(mDropShadowLeftId);
            canvas.drawBitmap(bm, null, getDropShadowArea(LOC_LEFT, canvas, bm), null);
        }

        if (mDropShadowTop) {
            bm = getBitmap(mDropShadowTopId);
            canvas.drawBitmap(bm, null, getDropShadowArea(LOC_TOP, canvas, bm), null);
        }

        if (mDropShadowBottom) {
            bm = getBitmap(mDropShadowBottomId);
            canvas.drawBitmap(bm, null, getDropShadowArea(LOC_BOTTOM, canvas, bm), null);
        }
    }

    /**
     * Get the correct bitmap from context resources
     * 
     * @param id
     *            Resource id
     * @return Bitmap to draw on the view
     */
    private Bitmap getBitmap(int id) {
        return BitmapFactory.decodeResource(getContext().getResources(), id);
    }

    /**
     * Get the Rect to draw the Bitmap in
     * 
     * @param loc
     *            Left, Top, Right or Bottom (0,1,2,3)
     * @param canvas
     *            The canvas to we're drawing on
     * @param bm
     *            The bitmap we're drawing
     * @return Rect for the area we are drawing the Bitmap onto the canvas
     */
    private Rect getDropShadowArea(int loc, Canvas canvas, Bitmap bm) {
        switch (loc) {
            case LOC_LEFT :
                return new Rect(0, 0, bm.getWidth(), canvas.getHeight());
            case LOC_TOP :
                return new Rect(0, 0, canvas.getWidth(), bm.getHeight());
            case LOC_RIGHT :
                return new Rect(canvas.getWidth() - bm.getWidth(), 0, canvas.getWidth(), canvas.getHeight());
            case LOC_BOTTOM :
                return new Rect(0, canvas.getHeight() - bm.getHeight(), canvas.getWidth(), canvas.getHeight());
            default :
                return null;
        }
    }
}


And finally you can create this custom ListView in xml like this.