Showing posts with label design. Show all posts
Showing posts with label design. Show all posts

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.



Wednesday, November 30, 2011

Irregular ActionBar

So you've been working with the Honeycomb API's for a while and are comfortable with the new ActionBar.  Then your designer sends you this visual design...


Can you spot what is going on in that screenshot?  ActionBar is the rectangular area at the top of your Honeycomb app where you can put things like tabs, menus, drop down lists, etc.  But for all the styling you can apply to your ActionBar, it's still a rectangle.  If you want to add a bit of flare to your ActionBar and break out of that rectangular box.  Here's a neat trick.

Your visual designer just sent you the background resource for your apps ActionBar, and it's got some irregularities to it's shape, with some transparency and they expect your frame content to seamlessly scroll behind your ActionBar.  Here's what you do.

Step 1: Send it back and tell them the ActionBar is a rectangle! This rarely works.

Step 2: Bring the image into your favorite image editing software (I use Gimp).  Now you want to cut the image horizontally and save the top (rectangular) portion in one image file, and the bottom irregular portion in a second image file.

ActionBar header top (header_top.png)

ActionBar header bottom (header_bottom.png)
Step 3: Create a custom style that uses Widget.Holo.ActionBar as it's parent and set the header_top.png as the background drawable for your ActionBar.  Then make sure you add your style as a theme to your manifest file under application (android:theme="@style/MyTheme")  So far nothing too special, you've set a background image for your ActionBar.

(res/values/styles.xml)


    

    
    

Step 4: Modify your content layout file to include the bottom part of the ActionBar as an ImageView and use android:layout_alignParentTop="true" to make sure it is "attached" to the top of your parent view and looks like it's part of your ActionBar.  Note that your parent layout will need to be a RelativeLayout in order for this to work.

Now, if you run your app and you can't see the bottom part of the ActionBar that you just added, you probably put your ImageView before your content in your layout.  Even though you've told your ImageView to alignParentTop, if you create it before your content view, it will be covered up and you actually want it to be drawn last in your view.  So move it to be the last view in your layout.  That should do it!  Check out your awesome irregular ActionBar!

(res/layout/main.xml)


 
    
    
     
     
    

 

Credit to my designer Stefan for the awesome idea!