Is there an example of how to use a TouchDelegate in Android to increase the size of a view's click target?
Asked Answered
I

13

90

My understanding is that when you have a view that's too small to easily touch, you're supposed to use a TouchDelegate to increase the clickable region for that view.

However, searching for usage examples on Google turn up multiple people asking the question, but few answers.

Does anyone know the proper way to set up a touch delegate for a view to, say, increase its clickable region by 4 pixels in every direction?

Iatry answered 27/8, 2009 at 19:13 Comment(2)
Here is an additional good post on how to use TouchDelegate: groups.google.com/group/android-developers/browse_thread/thread/…Magnetic
Good tutorial: Enlarged touchable areasSusumu
I
107

I asked a friend at Google and they were able to help me figure out how to use TouchDelegate. Here's what we came up with:

final View parent = (View) delegate.getParent();
parent.post( new Runnable() {
    // Post in the parent's message queue to make sure the parent
    // lays out its children before we call getHitRect()
    public void run() {
        final Rect r = new Rect();
        delegate.getHitRect(r);
        r.top -= 4;
        r.bottom += 4;
        parent.setTouchDelegate( new TouchDelegate( r , delegate));
    }
});
Iatry answered 27/8, 2009 at 21:21 Comment(10)
What if there are two buttons on one screen that you would like to do this to? If you setTouchDelegate again then it erases the previous touch delegate.Anemochore
This one didn't work for me... I assume the delegate is the view I'm trying to add the TouchDelegate to.Euton
Does anyone have a solution for @littleFluffyKitty's issue? I am running into the same problem!Hunnicutt
@AmokraneChentir I have had the same issue. Instead of using TouchDelegates, I have a class that extends button. In this class I override getHitRect. You can extend the hit rectangle to the size of the parent view. public void getHitRect(Rect outRect) { outRect.set(getLeft() - mPadding, getTop() - mPadding, getRight() + mPadding, getTop() + mPadding); }Are
@BrendanWeinstein Thanks for sharing this! Will definitely check it out!Hunnicutt
@littleFluffyKitty this suggestion still works well for multiple buttons (or checkboxes in my case), you just have to set a separate parent for each buttonPredispose
Is this solution safe? What if the posted runnable gets executed before the layout happens? Anyway, the dependency here is too much on an expected behavior - which is not future proof.Melgar
Per @ZsoltSafrany, a safer way to do this is via a OnGlobalLayoutListener and assigning the delegate onGlobalLayout().Ohm
@ZsoltSafrany I thought the runnable is guaranteed to run after layout? The documentation should mention this, but doesn't.Tuberosity
This approach does not work if the view parent area is not large enough, please check the #39471399Willson
P
22

I was able to accomplish this with multiple views (checkboxes) on one screen drawing largely from this blog post. Basically you take emmby's solution and apply it to each button and its parent individually.

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));
        }
   });
}

In my case I had a gridview of imageviews with checkboxes overlaid on top, and called the method as follows:

CheckBox mCheckBox = (CheckBox) convertView.findViewById(R.id.checkBox1);
final ImageView imageView = (ImageView) convertView.findViewById(R.id.imageView1);

// Increase checkbox clickable area
expandTouchArea(imageView, mCheckBox, 100);

Working great for me.

Predispose answered 4/3, 2013 at 3:1 Comment(0)
R
12

This solution was posted by @BrendanWeinstein in comments.

Instead of sending a TouchDelegate you can override getHitRect(Rect) method of your View (in case that you are extending one).

public class MyView extends View {  //NOTE: any other View can be used here

    /* a lot of required methods */

    @Override
    public void getHitRect(Rect r) {
        super.getHitRect(r); //get hit Rect of current View

        if(r == null) {
            return;
        }

        /* Manipulate with rect as you wish */
        r.top -= 10;
    }
}
Rohrer answered 7/11, 2012 at 13:33 Comment(2)
Interesting idea. But would than not mean that the view can not be created from a layout? No, wait — if I design a new view class and use that in the layout. That could work. Worth a try.Onstad
@Onstad that's the right idea. Unfortunately, getHitRect is no longer called as of ICS by dispatchTouchEvent grepcode.com/file/repository.grepcode.com/java/ext/… Overriding getHitRect will only work for gingerbread and below.Are
J
6

emmby's approch didn't work for me but after a little changes it did:

private void initApplyButtonOnClick() {
    mApplyButton.setOnClickListener(onApplyClickListener);
    final View parent = (View)mApplyButton.getParent();
    parent.post(new Runnable() {

        @Override
        public void run() {
            final Rect hitRect = new Rect();
            parent.getHitRect(hitRect);
            hitRect.right = hitRect.right - hitRect.left;
            hitRect.bottom = hitRect.bottom - hitRect.top;
            hitRect.top = 0;
            hitRect.left = 0;
            parent.setTouchDelegate(new TouchDelegate(hitRect , mApplyButton));         
        }
    });
}

Maybe it can save someone's time

Jourdan answered 13/6, 2013 at 13:16 Comment(2)
this is what worked for me. it actually makes the entire parent to send click events to the child. it doesn't even have to be the direct parent (can be a parent of a parent).Fairly
That probably works if you have one view to delegate. what if you have, say, 50 views to enhance the touch area? — Like in this Screenshot: plus.google.com/u/0/b/112127498414857833804/… — Is a TouchDelegate then still an appropriate solution?Onstad
A
4

According to @Mason Lee comment, this solved my problem. My project had relative layout and one button. So parent is -> layout and child is -> a button.

Here is a google link example google code

In case of deleting his very valuable answer I put here his answer.

I was recently asked about how to use a TouchDelegate. I was a bit rusty myself on this and I couldn't find any good documentation on it. Here's the code I wrote after a little trial and error. touch_delegate_view is a simple RelativeLayout with the id touch_delegate_root. I defined with a single, child of the layout, the button delegated_button. In this example I expand the clickable area of the button to 200 pixels above the top of my button.

public class TouchDelegateSample extends Activity {

  Button mButton;   
  @Override   
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.touch_delegate_view);
    mButton = (Button)findViewById(R.id.delegated_button);
    View parent = findViewById(R.id.touch_delegate_root);

    // post a runnable to the parent view's message queue so its run after
    // the view is drawn
    parent.post(new Runnable() {
      @Override
      public void run() {
        Rect delegateArea = new Rect();
        Button delegate = TouchDelegateSample.this.mButton;
        delegate.getHitRect(delegateArea);
        delegateArea.top -= 200;
        TouchDelegate expandedArea = new TouchDelegate(delegateArea, delegate);
        // give the delegate to an ancestor of the view we're delegating the
        // area to
        if (View.class.isInstance(delegate.getParent())) {
          ((View)delegate.getParent()).setTouchDelegate(expandedArea);
        }
      }
    });   
  } 
}

Cheers, Justin Android Team @ Google

Few words from me: if you want expand left side you give value with minus, and if you want expand right side of object, you give value with plus. This works the same with top and bottom.

Auricular answered 5/4, 2013 at 10:29 Comment(1)
The magic hint here seems to be one button — Somehow I have the feeling that TouchDelegate are unusable for multiple buttons. Can you confirm that?Onstad
D
2

A bit late to the party, but after much research, I'm now using:

/**
 * Expand the given child View's touchable area by the given padding, by
 * setting a TouchDelegate on the given ancestor View whenever its layout
 * changes.
 */*emphasized text*
public static void expandTouchArea(final View ancestorView,
    final View childView, final Rect padding) {

    ancestorView.getViewTreeObserver().addOnGlobalLayoutListener(
        new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                TouchDelegate delegate = null;

                if (childView.isShown()) {
                    // Get hitRect in parent's coordinates
                    Rect hitRect = new Rect();
                    childView.getHitRect(hitRect);

                    // Translate to ancestor's coordinates
                    int ancestorLoc[] = new int[2];
                    ancestorView.getLocationInWindow(ancestorLoc);

                    int parentLoc[] = new int[2];
                    ((View)childView.getParent()).getLocationInWindow(
                        parentLoc);

                    int xOffset = parentLoc[0] - ancestorLoc[0];
                    hitRect.left += xOffset;
                    hitRect.right += xOffset;

                    int yOffset = parentLoc[1] - ancestorLoc[1];
                    hitRect.top += yOffset;
                    hitRect.bottom += yOffset;

                    // Add padding
                    hitRect.top -= padding.top;
                    hitRect.bottom += padding.bottom;
                    hitRect.left -= padding.left;
                    hitRect.right += padding.right;

                    delegate = new TouchDelegate(hitRect, childView);
                }

                ancestorView.setTouchDelegate(delegate);
            }
        });
}

This is better than the accepted solution because it also allows a TouchDelegate to be set on any ancestor View, not just the parent View.

Unlike the accepted solution, it also updates the TouchDelegate whenever there is a change in the ancestor View's layout.

Driftage answered 8/9, 2014 at 15:45 Comment(0)
S
1

Isn't it the better Idea of giving Padding to that particular component(Button).

Staggers answered 5/7, 2013 at 8:19 Comment(1)
That would make to view bigger. What if you have some non clickabe text text above a button and you want the area of that text be used to improve click accuracy for the button. — Like in this Screenshot: plus.google.com/u/0/b/112127498414857833804/…Onstad
N
1

Because I didn't like the idea of waiting for the layout pass just to get the new size of the TouchDelegate's rectangle, I went for a different solution:

public class TouchSizeIncreaser extends FrameLayout {
    public TouchSizeIncreaser(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final View child = getChildAt(0);
        if(child != null) {
            child.onTouchEvent(event);
        }
        return true;
    }
}

And then, in a layout:

<ch.tutti.ui.util.TouchSizeIncreaser
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <Spinner
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>
</ch.tutti.ui.util.TouchSizeIncreaser>

The idea is that TouchSizeIncreaser FrameLayout will wrap the Spinner (could be any child View) and forward all the touch events captured in it's hit rect to the child View. It works for clicks, the spinner opens even if clicked outside its bounds, not sure what are the implications for other more complex cases.

Nigrosine answered 15/6, 2017 at 13:43 Comment(0)
R
1

One solution that I found in a google repo for using TouchDelegate is implemented like this:

/**
 * Ensures that the touchable area of [view] equal [minTouchTarget] by expanding the touch area
 * of a view beyond its actual view bounds. This adapter can be used expand the touchable area of a
 * view when other options (adding padding, for example) may not be available.
 *
 * Usage:
 * <ImageView
 *     ...
 *     app:ensureMinTouchArea="@{@dimen/min_touch_target}"
 *
 * @param view The view whose touch area may be expanded
 * @param minTouchTarget The minimum touch area expressed dimen resource
 */
@BindingAdapter("ensureMinTouchArea")
fun addTouchDelegate(view: View, minTouchTarget: Float) {
    val parent = view.parent as View
    parent.post {
        val delegate = Rect()
        view.getHitRect(delegate)

        val metrics = view.context.resources.displayMetrics
        val height = ceil(delegate.height() / metrics.density)
        val width = ceil(delegate.width() / metrics.density)
        val minTarget = minTouchTarget / metrics.density
        var extraSpace = 0
        if (height < minTarget) {
            extraSpace = (minTarget.toInt() - height.toInt()) / 2
            delegate.apply {
                top -= extraSpace
                bottom += extraSpace
            }
        }

        if (width < minTarget) {
            extraSpace = (minTarget.toInt() - width.toInt()) / 2
            delegate.apply {
                left -= extraSpace
                right += extraSpace
            }
        }

        parent.touchDelegate = TouchDelegate(delegate, view)
    }
}
Ronnironnica answered 29/9, 2022 at 12:29 Comment(0)
T
0

If don't want to do it programatically then simply create transparent area around the image, If you are using image as background for the button (view).

The grey area can be transparent to increase the touch area.

enter image description here

Tranquil answered 7/11, 2012 at 13:25 Comment(2)
Would that not create empty space around then view which can not be used for anything else? What if you want to display informational text close a button and use the text area to enhance the button click area?Onstad
I don't say its the best and the final approach. But it will fit in many scenarios where you want to show a tiny button image but the touch area to be extended. In your case it is difficult to give precedence to the button when you click other views (textview) it is all together is different challenge.Tranquil
G
0

In most cases, you can wrap the view that requires a larger touch area in another headless view (artificial transparent view) and add padding/margin to the wrapper view and attach the click/touch even to the wrapper view instead of the original view that has to have a larger touch area.

Gallous answered 26/2, 2013 at 3:52 Comment(2)
Would that not create empty space around then view which can not be used for anything else? What if you want to display informational text close a button and use the text area to enhance the button click area?Onstad
@Martin, It depends how you do it. And should be easy to do whatever you want – just wrap whatever you want (however many views you want) in a transparent view on top of all of these views and assign a click even to that area.Gallous
A
0

To expand the touch area generically with pretty few restrictions use the following code.

It lets you expand the touch area of the given view within the given ancestor view by the given expansion in pixels. You can choose any ancestor as long as the given view is in the ancestors layout tree.

    public static void expandTouchArea(final View view, final ViewGroup ancestor, final int expansion) {
    ancestor.post(new Runnable() {
        public void run() {
            Rect bounds = getRelativeBounds(view, ancestor);
            Rect expandedBounds = expand(bounds, expansion);
            // LOG.debug("Expanding touch area of {} within {} from {} by {}px to {}", view, ancestor, bounds, expansion, expandedBounds);
            ancestor.setTouchDelegate(new TouchDelegate(expandedBounds, view));
        }

        private Rect getRelativeBounds(View view, ViewGroup ancestor) {
            Point relativeLocation = getRelativeLocation(view, ancestor);
            return new Rect(relativeLocation.x, relativeLocation.y,
                            relativeLocation.x + view.getWidth(),
                            relativeLocation.y + view.getHeight());
        }

        private Point getRelativeLocation(View view, ViewGroup ancestor) {
            Point absoluteAncestorLocation = getAbsoluteLocation(ancestor);
            Point absoluteViewLocation = getAbsoluteLocation(view);
            return new Point(absoluteViewLocation.x - absoluteAncestorLocation.x,
                             absoluteViewLocation.y - absoluteAncestorLocation.y);
        }

        private Point getAbsoluteLocation(View view) {
            int[] absoluteLocation = new int[2];
            view.getLocationOnScreen(absoluteLocation);
            return new Point(absoluteLocation[0], absoluteLocation[1]);
        }

        private Rect expand(Rect rect, int by) {
            Rect expandedRect = new Rect(rect);
            expandedRect.left -= by;
            expandedRect.top -= by;
            expandedRect.right += by;
            expandedRect.bottom += by;
            return expandedRect;
        }
    });
}

Restrictions that apply:

  • The touch area can not exceed the bounds of the view's ancestor since the ancestor must be able to catch the touch event in order to forward it to the view.
  • Only one TouchDelegate can be set to a ViewGroup. If you want to work with multiple touch delegates, choose different ancestors or use a composing touch delegate like explained in How To Use Multiple TouchDelegate.
Aeolipile answered 18/5, 2017 at 15:25 Comment(0)
H
0

Write a common function like this:

fun increaseHitArea(view: View, increaseBy: Int) {
            val parent = view.parent as View
            parent.post {
                val rect = Rect()
                view.getHitRect(rect)
                rect.top -= increaseBy
                rect.left -= increaseBy
                rect.bottom += increaseBy
                rect.right += increaseBy
                parent.touchDelegate = TouchDelegate(rect, view)
            }
        }

And call it as below:

increaseHitArea(btn, 40)
Hilltop answered 27/6, 2023 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.