102

I have a button with a custom drawable.

<Button
    android:layout_width="22dip"
    android:layout_height="23dip"
    android:background="@drawable/triangle" />

The drawable is a triangle with transparent background.

|\
| \
|__\

I find this button hard to tap. First, it's relatively small. Second, the transparent pixels are not tappable. I would like to keep the drawable the same size, but make the hit area a square shape twice the size of the triangle.

_____________
|            |
|    |\      |
|    | \     |
|    |__\    |
|____________|
JoJo
  • 19,587
  • 34
  • 106
  • 162

24 Answers24

77

you can use TouchDelegate API.

final View parent = (View) button.getParent();  // button: the view you want to enlarge hit area
parent.post( new Runnable() {
    public void run() { 
        final Rect rect = new Rect(); 
        button.getHitRect(rect); 
        rect.top -= 100;    // increase top hit area
        rect.left -= 100;   // increase left hit area
        rect.bottom += 100; // increase bottom hit area           
        rect.right += 100;  // increase right hit area
        parent.setTouchDelegate( new TouchDelegate( rect , button)); 
    } 
}); 
JNI_OnLoad
  • 5,472
  • 4
  • 35
  • 60
Morris Lin
  • 771
  • 5
  • 2
48

You want "padding" It will put the space inside the view. Margin will put the space outside, which will not increase the hit area.

 <Button
    android:layout_width="22dip"
    android:layout_height="23dip"
    android:background="@drawable/triangle"
    android:padding="10dp" />
kwahn
  • 2,118
  • 2
  • 21
  • 17
  • i tested here on device and emulator, it does not work. have you tested this? – LiangWang Oct 13 '16 at 02:20
  • 21
    Padding is added inside of layout_width/layout_height, not outside. Thus this Button is still 22x23 large. So this one does not help. – Simon Warta Oct 20 '16 at 12:53
  • 1
    This works great. The padding expanded the tappable area as desired and left my image the same size, which just as JoJo (the original author) asked. – Alyoshak Sep 18 '18 at 22:14
  • Why this answer got so many upvotes, it's clear that padding pads content inside borders not outside?! – Farid Mar 03 '19 at 09:03
  • I guess you should also enlarge the size of the view to make it work. like: layout_width="42dp" layout_height="43dp" padding="10dp" – Henry May 27 '19 at 08:29
  • 1
    Following up my previous comment, I think this will also enlarge the background. If so, maybe you can put the background inside an "inset" drawable. PS. I prefer this one than TouchDelegate, because TouchDelegate needs to mess up with the ancestor and it gets more complicated if the child view is moving... – Henry May 27 '19 at 08:36
  • @Alyoshak that is not possible according to what padding is supposed to do. Padding is applied inside given `height/width` not to the outer side. You've probably changed `height/width` to `wrap_content` – Farid Jul 12 '19 at 04:44
25

You need to use a TouchDelegate, which is defined in the API docs as "Helper class to handle situations where you want a view to have a larger touch area than its actual view bounds"

http://developer.android.com/reference/android/view/TouchDelegate.html

LuxuryMode
  • 33,401
  • 34
  • 117
  • 188
  • @mxcl, I don't see anything in your comment that would render this answer invalid or "wrong". – dbm Mar 28 '14 at 12:40
  • 10
    An explanation about `TouchDelegate` and a snippet code: http://developer.android.com/training/gestures/viewgroup.html#delegate – Brais Gabin Jul 08 '14 at 10:47
17

I prefer this solution:

Simply add a positive padding and a negative marging inside the layout resource file:

<some.View
  ..
  android:padding="12dp"
  android:margin="-12dp"
  .. />

This is a very small change in comparison to the usage of TouchDelegates. I also don't want to run a Runnable for adding the clickable area inside my Java code.

One thing you might consider if the view should be pixel-perfect: padding cannot always equal to margin. The padding should always be there exactly as specified. The margin depends on the surrounding views and will be offset with the margin values of other views.

Timo Bähr
  • 1,826
  • 2
  • 22
  • 26
  • loved this one :D – Ayman Salah Aug 30 '16 at 18:08
  • Couldn't get this to work. I have two texts next to each other in a LinearLayout and want the right hand one to have a touchable area a little bigger than the text itself...but using these 2 extra lines makes my text be offset which is not the look I am going for – Kibi Sep 01 '16 at 08:35
  • @Kibi Inserting a single view (here `TextView`) into a `LinearLayout` should not be needed. Nevertheless, have you tried to add these two lines into your `LinearLayout`? As stated before margin depends on the surrounding views. – Timo Bähr Sep 05 '16 at 07:21
  • Thanks @TimoBähr but (initially) I did not want the whole LinearLayout to be clickable, since the text on the left was not supposed to be clickablr...in the end I jsut jave up and mad the whole LinearLayout Clickable and of course that works. – Kibi Sep 07 '16 at 08:23
17

Based on answer I created a kotlin extension function to help with it.

/**
 * Increase the click area of this view
 */
fun View.increaseHitArea(dp: Float) {
    // increase the hit area
    val increasedArea = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics).toInt()
    val parent = parent as View
    parent.post {
        val rect = Rect()
        getHitRect(rect)
        rect.top -= increasedArea
        rect.left -= increasedArea
        rect.bottom += increasedArea
        rect.right += increasedArea
        parent.touchDelegate = TouchDelegate(rect, this)
    }
}

William Reed
  • 1,717
  • 1
  • 17
  • 30
  • 4
    `Resources.getSystem()` could contain metric values different than the current app's so I would change that part to extract `Resources` from `this` i.e. the `View` in question. Also a `doOnLayout` on the parent would serve better than `post` as there is know guerentee when that post is recieved. Note that the paren's layout is ready _after_ its child so that's why it's a better place. Otherwise... nice extension, I'll be stealing it! – Cyrus Bakhtiari-Haftlang Feb 25 '21 at 09:53
10

In case you are using Data Binding. You can use binding adapter.

@BindingAdapter("increaseTouch")
fun increaseTouch(view: View, value: Float) {
    val parent = view.parent
    (parent as View).post({
        val rect = Rect()
        view.getHitRect(rect)
        val intValue = value.toInt()
        rect.top -= intValue    // increase top hit area
        rect.left -= intValue   // increase left hit area
        rect.bottom += intValue // increase bottom hit area
        rect.right += intValue  // increase right hit area
        parent.setTouchDelegate(TouchDelegate(rect, view));
    });
}

and then you can use attribute on views like this

increaseTouch="@{8dp}"
logcat
  • 3,435
  • 1
  • 29
  • 44
  • 1
    Awesome solution though I don't think you can pass dp from the resource it just takes a float in my case. Then you can convert from dp to px before using it in the function. – Slion Feb 11 '23 at 09:56
6

simply by adding padding to the button

 <Button
android:layout_width="wrap_contant"
android:layout_height="wrap_contant"
android:background="@drawable/triangle"
android:padding="20dp" />

So by doing this.. your button will take the height and width of the triangle image and will add the 20dp padding to the button without stretching the image itself . and if you want some minimum width or some hinimum height you can use the minWidth and minHeight tags

Nitesh
  • 3,868
  • 1
  • 20
  • 26
2

try with this

  <Button
        android:id="@+id/btn_profile"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentLeft="true"
        android:background="@android:color/transparent"
        android:drawableBottom="@drawable/moreoptionicon"
        android:onClick="click"
        android:paddingBottom="15dp"
        android:visibility="visible" />
shailesh
  • 1,783
  • 2
  • 17
  • 26
2

I don't know for sure what you mean by "without also increasing the background drawable." If you mean that you just don't want your drawable to get stretched then one option you have is take your background png and add an extra transparent pixel border on to it. That would increase your hit zone but wouldn't stretch your image.

If however you mean that you don't want to change that drawable at all then I think your only option is use larger hard coded values for height and width.

FoamyGuy
  • 46,603
  • 18
  • 125
  • 156
  • What I mean is I need the button graphic to appear X by Y but have the hit area be X+10 by Y+10. I don't think transparent pixels add to the hit area. My graphic is a triangular shape. When I tap the transparent areas, the onClick does not trigger. – JoJo Nov 18 '11 at 07:56
  • I know for sure that transparent pixels in the background img will add to the hit zone. I've used it before to make an invisible button. Perhaps it is not adding size because you've hard coded the height and width? Maybe try wrap_content for those. – FoamyGuy Nov 18 '11 at 14:10
  • It's strange. I blew up the button to fill up my entire screen just for testing. When I tap the opaque pixels, LogCat prints out my onClick log. When I tap the transparent areas, it doesn't register. – JoJo Nov 18 '11 at 20:18
  • Also, if you put a lot of pressure on the screen such that your finger surface area is greater than that of the button, the onClick does not trigger. I have to gingerly tap the button to trigger onClick. Is there any property that adjusts this "fat finger syndrome"? – JoJo Nov 18 '11 at 20:44
1

I just needed the same: clickable area larger than button's image. I wrapped it into FrameLayout sized larger than background drawable and changed the inner Button tag to TextView. Than I directed a click handler to work with this FrameLayout, not with the inner TextView. All works well!

Dmitri Novikov
  • 123
  • 1
  • 12
1

You can use transparent button like this...

<Button
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:background="@android:color/transparent" /> 

you can increase or decrease the button size as your requirement and use aenter code here ImageView up the button....

<ImageView
    android:layout_width="22dip"
    android:layout_height="23dip"
    android:background="@drawable/triangle" />
1

I created the top-level function for this in kotlin:

fun increaseTouchArea(view: View, increaseBy: Int) {
    val rect = Rect()
    view.getHitRect(rect)
    rect.top -= increaseBy    // increase top hit area
    rect.left -= increaseBy   // increase left hit area
    rect.bottom += increaseBy // increase bottom hit area
    rect.right += increaseBy  // increase right hit area
    view.touchDelegate = TouchDelegate(rect, view)
}
Igor Konyukhov
  • 394
  • 4
  • 7
1

I honestly think the easiest way is this:-

Say your image size is 26dp*26dp and you want a hit area of 30dp*30dp, just add a padding of 2dp to your ImageButton/Button AND set the dimension to 30dp*30dp


Original

<ImageButton
    android:layout_width="26dp"
    android:layout_height="26dp"
    android:src="@drawable/icon"/>

Bigger hit area

<ImageButton
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:padding="2dp"
    android:src="@drawable/icon_transcript"/>
daisura99
  • 1,030
  • 1
  • 12
  • 22
1

Simplest solution would be increase the width and height of your item (e.g Image, Button etc) and add padding; So the clickable area is larger but looks in the original size.

Example:

<Button
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:padding="8dp"
    android:background="@drawable/triangle" />

Congratulations, you achieved same button size but double the clickable size!

_____________
|            |
|    |\      |
|    | \     |
|    |__\    |
|____________|
unobatbayar
  • 1,223
  • 1
  • 10
  • 24
1

I added a fairly simple solution to SO; https://stackoverflow.com/a/67904934/13215135

This design can be used for multiple Views on one layout - it also allows you to explicitly design the touch area on each View, instead of guestimating the pixels on a users device;

tommytucker7182
  • 213
  • 1
  • 5
  • 11
1

See Can I increase a buttons onclick-area programmatically?. This looks like the easiest way to me for sure. Even includes a bit on animating only the actual clickable object while allowing the background to receive clicks. I'll be applying this to my own code soon.

Community
  • 1
  • 1
Thomson Comer
  • 3,919
  • 3
  • 30
  • 32
1

This worked for me:

public void getHitRect(Rect outRect) {
    outRect.set(getLeft(), getTop(), getRight(), getBottom() + 30);
}

Though really you should convert the 30 to dip or do a percentage of the button height.

mxcl
  • 26,392
  • 12
  • 99
  • 98
  • Needs explanation. OP posted a snippet of layout xml, for a Button (which would be part of some view's layout xml). But I presume that the above method needs to be associated with the Button, not with the view class that contains the button? How do you associate the above method with that button? – ToolmakerSteve Oct 07 '15 at 01:08
0

For this purposes I was using ImageButton. In xml defenition you will see these two attributes:

  • android:background - A drawable to use as the background.
  • android:scr - Sets a drawable as the content of this ImageView.

So we have two drawables: background one and source. In you example source will be triagle(drawable/trianlge):

|\
| \
|__\

And background is square(drawable/square):

_____________
|            |
|            |
|            |
|            |
|____________|

Here is example of ImageButton xml:

<ImageButton
   ...
   android:src="@drawable/triangle"
   android:background="@drawable/square">

Result:

_____________
|            |
|    |\      |
|    | \     |
|    |__\    |
|____________|

Also square drawable, could have several different states(pressed, focused). And you could expand backgroud drawable size using paddings.

Just in case, here is example for background drawable from Android Compat Actionbar:

<?xml version="1.0" encoding="utf-8"?>    
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Even though these two point to the same resource, have two states so the drawable will invalidate itself when coming out of pressed state. -->
    <item android:state_focused="true" android:state_enabled="false" android:state_pressed="true" android:drawable="@drawable/abc_list_selector_disabled_holo_dark" />
    <item android:state_focused="true" android:state_enabled="false" android:drawable="@drawable/abc_list_selector_disabled_holo_dark" />
    <item android:state_focused="true" android:state_pressed="true" android:drawable="@drawable/abc_list_selector_background_transition_holo_dark" />
    <item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/abc_list_selector_background_transition_holo_dark" />
    <item android:state_focused="true" android:drawable="@drawable/abc_list_focused_holo" />
    <item android:drawable="@android:color/transparent" />
 </selector>
molokoka
  • 1,290
  • 12
  • 16
0

You can set the padding to the view i.e. your button.

<Button
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="@android:color/transparent"
        android:padding="15dp" />

Hope this works for you.

Mayank Bhatnagar
  • 2,120
  • 1
  • 12
  • 20
0

So the right answer, add padding to the touchable area get bigger AND change your picture from background to srcCompact.

<Button
    android:layout_width="22dip"
    android:layout_height="23dip"
    android:padding="6dp"
    app:srcCompat="@drawable/triangle"/>

To use app:scrCompat you need add xmlns:app="http://schemas.android.com/apk/res-auto" to your main/parent element in the xml.

example complete:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/height_btn_bottom_touch_area"
android:background="@color/bg_white"
android:clipToPadding="false"
android:elevation="7dp">

<android.support.constraint.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:stateListAnimator="@animator/selected_raise"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" >

    <ImageView
        android:id="@+id/bottom_button_bg"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:contentDescription=
            "@string/service_request_background_contentDescription"
        android:elevation="1dp"
        android:padding="6dp"
        app:srcCompat="@drawable/btn_round_blue"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" >
    </ImageView>

    <TextView
        android:id="@+id/bottom_button_text"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:elevation="1dp"
        android:gravity="center"
        android:text="@string/str_continue_save"
        android:textColor="@color/text_white"
        android:textSize="@dimen/size_text_title"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
Canato
  • 3,598
  • 5
  • 33
  • 57
0

Just adding padding was causing my checkbox to not be centered. The following worked for me, however I specified a fixed size for the checkbox (as per my UI spec). The idea was to add padding to the left and not to the right. I added padding to the top and bottom too making the touch target larger and still keeping the checkbox centered.

<CheckBox
        android:id="@+id/checkbox"
        android:layout_width="30dp"
        android:layout_height="45dp"
        android:paddingLeft="15dp"
        android:paddingTop="15dp"
        android:paddingBottom="15dp"
        android:button="@drawable/checkbox"
        android:checked="false"
        android:gravity="center"
       />
RPM
  • 3,426
  • 2
  • 27
  • 35
0

I think the correct answer should be using ImageButton instead of Button. Set the triangle drawable as the "src" of the ImageButton, and set the size (layout_width & layout_height) as you want for easier touch and set the ScaleType to center to retain the triangle size.

Henry
  • 942
  • 3
  • 11
  • 21
0

Well, I used custom Framelayout and it works perfectly for me as I assigned the touch for the first child. I have a view like this :

<CustomFrameLayout>
<View>
</CustomFrameLayout>

for customFrameLayout :

class CustomFrameLayout(context: Context, attrs: AttributeSet?) :
    FrameLayout(context, attrs) {
    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        return true
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val child = getChildAt(0)
        child.onTouchEvent(event)
        return true
    }

}

and in XML I put my view like this :

 <com.CustomFrameLayout
        android:layout_width="wrap_content"
        android:layout_height="@dimen/buttonsize"
        android:layout_weight="1">

        <com.views.Digit
            android:id="@+id/Digit4"
            android:background="@drawable/four_normal"
            style="@style/numKeypadStyle"
            android:text="4"
            />
 </com.CustomFrameLayout>
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Aslm Monir
  • 51
  • 3
-2
<Button
    android:layout_width="32dp"
    android:layout_height="36dp"
    android:layout_margin="5dp"
    android:background="@drawable/foo" 
/>

The size of the background is still 22x26dp. Just add margin.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Terence Lui
  • 2,778
  • 1
  • 18
  • 17