Android Buttons with Expanded Touch Areas

Android image and text icons are often too small for a user to easily touch. We want to expand the touch area of the icon to allow for some error. Android allows us to do this by assigning a larger view encapsulating a smaller view to be its Touch Delegate. This larger view will pass on touch events to the view that actually handles the touch, and process that touch if it is in the Rect you pass to setTouchDelegate. You can use this utility method, courtesy of Erik Burke of Square, to do this easily:

  public static void expandTouchArea(final View bigView, final View smallView, final int extraPadding) {
bigView.post(new Runnable() {
    @Override
    public void run() {
        Rect rect = new Rect();
        smallView.getHitRect(rect);
        rect.top -= extraPadding;
        rect.left -= extraPadding;
        rect.right += extraPadding;
        rect.bottom += extraPadding;
        bigView.setTouchDelegate(new TouchDelegate(rect, smallView));
    }
});

The problem is that a parent view can only be a touch delegate for one view. For example, if you had two ImageButton’s in a toolbar, you would have to wrap each ImageButton within a view with the dimensions of an expanded touch area. Although you might want to just redefine the hit rectangle for each view and use the shared parent view as the touch delegate, this is not possible. Touches will only be passed to one view.

This is undesirable, because we want to keep our view hierarchy as simple as possible. It’s also more code.

There used to be an easy way around this: overriding getHitRect. Before Ice Cream Sandwich (ICS/Android 4.0), the following code would work just fine:

    @Override
public void getHitRect(Rect outRect) {
    outRect.set(getLeft() - mHitPadding, getTop() - mHitPadding, getRight() + mHitPadding, getTop() + mHitPadding);
}

You could alternatively define the bounds of what is touchable inside the touchable view and override dispatchTouchEvent in the parent to pass all TouchEvent(s) to the proper child views. This seems like quite a bit of work though.

Unfortunately, overriding getHitRect() does not work in ICS. getHitRect is no longer called in ViewGroup’s dispatchTouchEvent as it is in pre-ICS dispatchTouchEvent. In ICS a new method isTransformedTouchPointInView() is used to determine whether to pass a MotionEvent to a child view. This method determines that by passing coordinates to each child’s pointInView() method. pointInView() is a final method, so we unfortunately cannot override it.

We can redefine isTransformedTouchPointInView() to do the same check that the pre-ICS dispatchTouchEvent method did:

	 protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
		 //Log.d(TAG, "isTranformedTouchPointInView()");
		 final Rect frame = new Rect();
		 child.getHitRect(frame);
		 if(frame.contains((int)x, (int)y)) {
			 return true;
		 }
		 return false;
	 }

It’d be cleaner to override a View method as opposed to a ViewGroup method, but I am not aware of a clean way of doing that.

I’ve put together a little sample project, so feel free to try out both solutions for yourself. The view hierarchy is conveniently the same, but you could get rid of the relativelayouts and change the LinearLayout to a TDRelativeLayout if you wanted.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>