Skip to content

Instantly share code, notes, and snippets.

@patrickhammond
Last active August 29, 2015 14:10
Show Gist options
  • Save patrickhammond/c74fe7984fe3c9ef7650 to your computer and use it in GitHub Desktop.
Save patrickhammond/c74fe7984fe3c9ef7650 to your computer and use it in GitHub Desktop.
Creating checkboxes with bigger touch targets
import android.graphics.Rect;
import android.view.View;
import android.widget.CheckBox;
/**
* Enlarges the effective hit target of a checkbox to more closely match the dimensions of the
* provided root view. If the provided root isn't actually an ancestor view of the checkbox, bad
* things will happen.
* <p/>
* See: http://developer.android.com/training/gestures/viewgroup.html#delegate
*/
public class CheckBoxHelper {
public static void enlargeCheckBoxHitTarget(final View rootView, final CheckBox checkBox) {
rootView.post(new Runnable() {
@Override
public void run() {
int relativeCheckTop = ViewHelper.getRelativeTop(rootView, checkBox);
int relativeCheckBottom = ViewHelper.getRelativeBottom(rootView, checkBox);
int relativeCheckLeft = ViewHelper.getRelativeLeft(rootView, checkBox);
int additionalTop = relativeCheckTop;
int additionalBottom = rootView.getMeasuredHeight() - relativeCheckBottom;
int additionalLeft = relativeCheckLeft;
// Semi-arbitrary, right is same as left. Makes sense when a checkbox is on the left.
int additionalRight = additionalLeft;
setLargerHitTarget(checkBox, additionalLeft, additionalTop, additionalRight, additionalBottom);
}
});
}
private static void setLargerHitTarget(View view, int additionalLeft, int additionalTop, int additionalRight, int additionalBottom) {
Rect origHitRect = new Rect();
view.getHitRect(origHitRect);
Rect delegateHitRect = new Rect(
origHitRect.left - additionalLeft,
origHitRect.top - additionalTop,
origHitRect.right + additionalRight,
origHitRect.bottom + additionalBottom);
HackedTouchDelegate touchDelegate = new HackedTouchDelegate(delegateHitRect, view);
if (View.class.isInstance(view.getParent())) {
((View) view.getParent()).setTouchDelegate(touchDelegate);
}
}
}
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewConfiguration;
/**
* There is a bug with TouchDelegate where ancestor views can get into an awkward state after
* a delegate view has been actioned upon by the touch delegate.
*
* This class is a direct copy of https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/TouchDelegate.java
* but with the fix.
*
* See: https://code.google.com/p/android/issues/detail?id=36445 for details.
*/
public class HackedTouchDelegate extends TouchDelegate {
/**
* View that should receive forwarded touch events
*/
private View mDelegateView;
/**
* Bounds in local coordinates of the containing view that should be mapped to the delegate
* view. This rect is used for initial hit testing.
*/
private Rect mBounds;
/**
* mBounds inflated to include some slop. This rect is to track whether the motion events
* should be considered to be be within the delegate view.
*/
private Rect mSlopBounds;
/**
* True if the delegate had been targeted on a down event (intersected mBounds).
*/
private boolean mDelegateTargeted;
/**
* The touchable region of the View extends above its actual extent.
*/
public static final int ABOVE = 1;
/**
* The touchable region of the View extends below its actual extent.
*/
public static final int BELOW = 2;
/**
* The touchable region of the View extends to the left of its
* actual extent.
*/
public static final int TO_LEFT = 4;
/**
* The touchable region of the View extends to the right of its
* actual extent.
*/
public static final int TO_RIGHT = 8;
private int mSlop;
/**
* Constructor
*
* @param bounds Bounds in local coordinates of the containing view that should be mapped to
* the delegate view
* @param delegateView The view that should receive motion events
*/
public HackedTouchDelegate(Rect bounds, View delegateView) {
super(bounds, delegateView); // Doesn't actually matter since we are overriding the entire implementation
mBounds = bounds;
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);
mDelegateView = delegateView;
}
/**
* Will forward touch events to the delegate view if the event is within the bounds
* specified in the constructor.
*
* @param event The touch event to forward
* @return True if the event was forwarded to the delegate, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
if (bounds.contains(x, y)) {
mDelegateTargeted = true;
sendToDelegate = true;
} else { // NOTE: This else block reflects the only effective change to this class.
mDelegateTargeted = false;
sendToDelegate = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
final View delegateView = mDelegateView;
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
handled = delegateView.dispatchTouchEvent(event);
}
return handled;
}
}
Before (white space around the checkbox is not touchable):
____________________
| |
| [.] Lorem ipsum |
|____________________|
After (dots around the checkbox are touchable):
____________________
|....... |
|..[.]..Lorem ipsum |
|......._____________|
View convertView = // for example, something from an adapter
CheckBox checkBox = // a checkbox that exists inside of convertview
CheckBoxHelper.enlargeCheckBoxHitTarget(convertView, checkBox);
import android.view.View;
public class ViewHelper {
/**
* Similar to View.getLeft(), but you specify what parent the value is relative to. If you
* provide a root view that isn't actually an ancestor view, bad things will happen.
*/
public static int getRelativeLeft(View relativeRootView, View view) {
int[] viewLocation = new int[2];
view.getLocationInWindow(viewLocation);
int[] rootLocation = new int[2];
relativeRootView.getLocationInWindow(rootLocation);
return viewLocation[0] - rootLocation[0];
}
/**
* Similar to View.getTop(), but you specify what parent the value is relative to. If you
* provide a root view that isn't actually an ancestor view, bad things will happen.
*/
public static int getRelativeTop(View relativeRootView, View view) {
int[] viewLocation = new int[2];
view.getLocationInWindow(viewLocation);
int[] rootLocation = new int[2];
relativeRootView.getLocationInWindow(rootLocation);
return viewLocation[1] - rootLocation[1];
}
/**
* Similar to View.getRight(), but you specify what parent the value is relative to. If you
* provide a root view that isn't actually an ancestor view, bad things will happen.
*/
public static int getRelativeRight(View relativeRootView, View view) {
return getRelativeLeft(relativeRootView, view) + view.getMeasuredWidth();
}
/**
* Similar to View.getBottom(), but you specify what parent the value is relative to. If you
* provide a root view that isn't actually an ancestor view, bad things will happen.
*/
public static int getRelativeBottom(View relativeRootView, View view) {
return getRelativeTop(relativeRootView, view) + view.getMeasuredHeight();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment