29

Sometimes I have a button in my UI that it is so small that it is difficult to click. My solution so far has been to add a transparent border around the button in photoshop. Just increasing the padding on the button does not work, since this will also stretch the image. Since it is kind of a fuss to open photoshop each time I want to change the clickable surface, is there any way to do this programmatically? I have tried placing a framelayout behind the button and make it clickable, but then the button wont change appearance on touch as it should. Ofcourse I could also add a ontouchlistener on the framelayout which changes the buttons appearance, but then it quite some code if I have several of those buttons.

Cheers,

pgsandstrom
  • 14,361
  • 13
  • 70
  • 104

8 Answers8

35

Me personally, I'd use a TouchDelegate. This lets you deal with the touch target, and the visual view bounds as two different things. Very handy...

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

GeorgeLawrence
  • 351
  • 3
  • 3
  • Thanks GeorgeLawrence, you've made one post on stackoverflow and its the one I'm looking for! – Kevin Parker Apr 28 '11 at 15:03
  • This is generally the best solution to the touch area problem. – Mason Lee Aug 20 '11 at 01:27
  • 3
    An explanation about `TouchDelegate` and a snippet code: http://developer.android.com/training/gestures/viewgroup.html#delegate – Brais Gabin Jul 08 '14 at 10:46
  • 1
    Does the TouchDelegate only work well with the immediate parent? Consider this: I put the button in a LinearLayout that's smaller than the desired touchable area, therefore I use the parent of the LinearLayout as the "parent" for the TouchDelegate. So the Touchable area is bigger than the LinearLayout, but it fits in the top-most parent. -> Then the touchable are is still clipped and only the part inside the LinearLayout is really touchable. – Stan Feb 15 '17 at 15:31
29

I have just found a neat way to solve this problem.

  1. Surround the button with a say a LinearLayout that has the padding round the button.
  2. Add the same onclick to the LinearLayout as the Button.
  3. In the Button set the duplicateParentState to true which make the button highlight when you click outside the button but inside the LinearLayout.

    <LinearLayout
        android:layout_height="fill_parent"
        android:onClick="searchButtonClicked" 
        android:layout_centerVertical="true" 
        android:orientation="horizontal" 
        android:layout_width="wrap_content" 
        android:paddingRight="10dp" 
        android:paddingLeft="30dp">
        <Button 
            android:id="@+id/search_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/toggle_button_selector"
            android:textColor="#fff"
            android:text="Search"
            android:focusable="true"
            android:textStyle="bold"
            android:onClick="searchButtonClicked" 
            android:layout_gravity="center_vertical" 
            android:duplicateParentState="true"/>
    </LinearLayout>
    
Louis Barman
  • 314
  • 3
  • 4
  • 6
    This only works sortof. To make it fully work you need to make the button setClickable(false) otherwise clicking on the botton itself will not highlight as it gets its state from the parent which is not clicked at this point. Once you make the button non-clickable, no need to set an onClick listener on it. – Tom anMoney Jan 16 '13 at 03:50
12

This is a very late "me too," but after coming to this and other questions looking for a solution, I found a simple, elegant solution of my own.

Another question complained that the transparent background of their image was not clickable. If that is an issue, this seems to get around that as well.

Here's the button:

<ImageButton
    android:id="@+id/arrowUp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/arrow_up"
    android:background="@drawable/clear_button_background" />

The relevant lines are the last two. "@drawable/arrow_up" is a few button states in *.png files defined in a drawable *.xml file like this:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
 <item android:state_pressed="true"
       android:drawable="@drawable/tri_up_blue" /> <!-- pressed -->
 <item android:state_selected="true"
       android:drawable="@drawable/tri_up_blue" /> <!-- selected -->
 <item android:state_focused="true"
       android:drawable="@drawable/tri_up_blue" /> <!-- focused -->
 <item android:drawable="@drawable/tri_up_green" /> <!-- default -->
</selector>

Just your basic button. Nothing special. And "@drawable/clear_button_background" is just this:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"  
    android:shape="rectangle">
   <solid android:color="@android:color/transparent"/>
   <size android:width="20dp" android:height="30dp" />
</shape>

The height and width here are the clickable area, resize as needed. You can reuse this for as many buttons as you need in a single view, unlike the absurdly detailed TouchDelegate. No additional listeners. It doesn't add any views or groups to your hierarchy and you won't be messing around with padding and margins all day.

Simple. Elegant.

Community
  • 1
  • 1
anthropomo
  • 4,100
  • 1
  • 24
  • 39
  • Good idea if you can use ImageButton, however many button are text-based which makes this more difficult to apply. – greg7gkb Apr 30 '15 at 20:26
  • @greg7gkb for a text-based button, you simply add `padding`. (Adding padding to an image-based button would stretch the image, which is why people are looking for some solution other than padding.) – ToolmakerSteve Oct 07 '15 at 01:36
3

I think your solution is the best one available at the moment, if you don't want to go deep into some android stuff and intercept all the motionEvent and TouchEvents yourself and then you also would need to trigger the pressed view of the button yourself etc.

Just create a nine patch image with a stretchable transparent border. In that way you can change the size of the image without the need to change the image itself and your button will grow or shrink without the actual displayed background changing.

Janusz
  • 187,060
  • 113
  • 301
  • 369
1

Simply provide padding to the layout in place of Margin

    <ImageView
    android:id="@+id/back_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="25dip"
    android:src="@drawable/back_btn" />
sidhanshu
  • 356
  • 2
  • 9
  • 1
    This will expand the background of the view, which might not be desired. – pgsandstrom Oct 03 '12 at 11:23
  • yes that is correct but it can be helpful when there is nothing much in the layout and few button. – sidhanshu Oct 04 '12 at 05:45
  • Padding is not enough if you don't want the image to be stretched, you must use `android:scaleType="centerInside"` along with the padding to have the desired effect. – ForceMagic Feb 06 '14 at 18:49
1

Anothe idea is to make the new transparent image and put icon on it so the touch area will be more and design look perfect. Check out the image

Thank you.

Sameer Z.
  • 3,265
  • 9
  • 48
  • 72
0

What I did is not the recommended way and very few will find it useful.

For me it was the best solution, because TouchDelegate did not worked in all View hierarchy scenarios.

The solution is based on overriding the hidden

View class
public boolean pointInView(float localX, float localY, float slop)

Here is the code:

   public boolean pointInView(float localX, float localY, float slop) {
    boolean res = ae_pointInView(localX, localY, slop, slop);
    if(!res) {
        if(viewIsClickableButToSmall(this)) {
            //our view is clickable itself
            float newSlopX = Math.max(slop, slop + minTouchableViewWidth - this.getWidth());
            float newSlopY = Math.max(slop, slop + minTouchableViewHeight - this.getHeight());
            if(ae_pointInView(localX, localY, newSlopX, newSlopY)) {
                return true;
            }
        }

        //the point is outside our view, now check for views with increased tap area that may extent beyond it
        int childCount = getChildCount();
        for(int i = 0; i < childCount;i++) {
            View child = getChildAt(i);
            if(child instanceof MyViewGroup) {
                //this is our container that may also contain views with increased tap area

                float[] newPoint = ae_transformPointToViewLocal(localX, localY, child);
                if(((MyViewGroup) child).pointInView(newPoint[0], newPoint[1], slop)) {
                    return true;
                }
            }
            else {
                if(viewIsClickableButToSmall(child)) {
                    float[] newPoint = ae_transformPointToViewLocal(localX, localY, child);
                    float newSlopX = Math.max(slop, slop + minTouchableViewWidth - this.getWidth());
                    float newSlopY = Math.max(slop, slop + minTouchableViewHeight - this.getHeight());
                    if(ae_pointInView(newPoint[0], newPoint[1], newSlopX, newSlopY)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    else
        return true;
}

private int minTouchableViewHeight, minTouchableViewWidth;
private boolean viewIsClickableButToSmall(View view)
{
    minTouchableViewHeight = GlobalHelper.dipToDevicePixels(40);
    minTouchableViewWidth = GlobalHelper.dipToDevicePixels(40);

    return (view.getHeight() < minTouchableViewHeight || view.getWidth() < minTouchableViewWidth) && view.hasOnClickListeners();
}

public boolean ae_pointInView(float localX, float localY, float slopX, float slopY) {
    boolean res = localX >= -slopX && localY >= -slopY && localX < ((getRight() - getLeft()) + slopX) &&
            localY < ((getBottom() - getTop()) + slopY);
    return res;
}

private float[] tempPoint;
public float[] ae_transformPointToViewLocal(float x, float y, View child) {
    if(tempPoint == null) {
        tempPoint = new float[2];
    }
    tempPoint[0] = x + getScrollX() - child.getLeft();
    tempPoint[1] = y + getScrollY() - child.getTop();

    return tempPoint;
}
Kamen Dobrev
  • 1,321
  • 15
  • 20
-1

Maybe you could do so by looking at the X and Y coordinates of the MotionEvent passed into onTouchEvent ?

JRL
  • 76,767
  • 18
  • 98
  • 146