Last active
August 29, 2015 14:10
-
-
Save patrickhammond/c74fe7984fe3c9ef7650 to your computer and use it in GitHub Desktop.
Creating checkboxes with bigger touch targets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Before (white space around the checkbox is not touchable): | |
| ____________________ | |
| | | | |
| | [.] Lorem ipsum | | |
| |____________________| | |
| After (dots around the checkbox are touchable): | |
| ____________________ | |
| |....... | | |
| |..[.]..Lorem ipsum | | |
| |......._____________| |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| View convertView = // for example, something from an adapter | |
| CheckBox checkBox = // a checkbox that exists inside of convertview | |
| CheckBoxHelper.enlargeCheckBoxHitTarget(convertView, checkBox); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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