Creating irregular shaped ImageButton with different click states
Asked Answered
H

2

8

I created an ImageButton with a selector for pressed and non-pressed states, and this works fine.

But the button has an irregular shape and I only want it clickable where the underlying rectangular image is non-transparent.

So I implemented an OnTouchListener that checks the touch event's co-ordinates against the Bitmap's pixel values (as described in the first answer here: link). This works, in terms of the logic deciding if the button was pressed, but now the image of the button doesn't change to the pressed image anymore.

Here is what I have:

Selector xml file:

<?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/button_start_call_pressed" />
    <item android:drawable="@drawable/button_start_call_normal" />
</selector>

Partly transparent ImageButton in the layout:

<ImageButton
    android:id="@+id/dashboardStartCallButton"
    android:background="@null"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/start_call_button_selector"
     />

In the Activity:

public void onCreate(Bundle savedInstance) {
   super.onCreate(savedInstance);
   ...
   ImageButton startCallButton = (ImageButton) this.findViewById(R.id.dashboardStartCallButton);
   startCallButton.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return OnStartCallButtonTouch(v,event);
        }           
    });
}


public boolean OnStartCallButtonTouch(View v, MotionEvent event)
{
    Bitmap TheBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.button_start_call_normal);
    int eventPadTouch = event.getAction();
    int iX = (int) event.getX();
    int iY = (int) event.getY();
    switch (eventPadTouch) {
        case MotionEvent.ACTION_DOWN:
            if (iX>=0 & iY>=0 & iX<TheBitmap.getWidth() & iY<TheBitmap.getHeight()) {                 
                if (TheBitmap.getPixel(iX,iY)!=0) {
                    onStartCallButtonClicked(v); 
                    return false; 
                } 
            }
    }           
    return true;
}
Heronry answered 18/6, 2012 at 10:20 Comment(1)
why not return OnStartCallButtonTouch(v,event) || startCallButton.onTouch()?Antibiotic
N
10

I think you're actually looking for this:

View.dispatchTouchEvent(...)

You need to override this method in your custom button to return false if point is not in the desired area. I suggest you go about it like this:

public static class MyButton extends ImageButton {
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int iX = (int) event.getX();
        int iY = (int) event.getY();
        // TODO Or use a more sophisticated, pixel value-based condition
        if (!(iX>=0 & iY>=0 & iX<TheBitmap.getWidth() & iY<TheBitmap.getHeight())) {
            return false;
        }
        return super.dispatchTouchEvent(event)
    }
}

Why not use OnTouchListener: Because you need to call through to View.onTouchEvent() (which is done in super.dispatchTouchEvent(event)) to let View handle drawable states (it's done in onTouchEvent() using View.refreshDrawableState() method, and there's also some more rather complex logic there)

Why not override onTouchEvent(): Because in View.dispatchTouchEvent() there is a condition that if there is an OnTouchListener, then let the listener handle everything and return true. So if later on you set an OnTouchListener to the button for some reason, your coordinate-based condition in onTouchEvent() would not get checked, because onTouchEvent() would never get called. By overriding dispatchTouchEvent() you filter touches at the very top and leave all the other logic in place - you can add any listeners to the button and they will work as they would for normal button, but only when your condition is true.

Nothing answered 21/6, 2012 at 12:19 Comment(1)
Hey, how's it going? If the answer did it for you, please, consider accepting it.Nothing
M
1

I think you need to modify your collision detection for whether touch is inside your button or not. Use folowing approach.

public boolean OnStartCallButtonTouch(View v, MotionEvent event)
{
    Bitmap TheBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.button_start_call_normal);
    int eventPadTouch = event.getAction();
    int iX = (int) event.getX();
    int iY = (int) event.getY();

    int[] location = new int[2];
    v.getLocationOnScreen(location);

    int viewX = location[0];
    int viewY = location[1];


    switch (eventPadTouch) {
        case MotionEvent.ACTION_DOWN:
            if (iX>=viewX & iY>=viewY & iX<=(viewX+TheBitmap.getWidth()) & iY<=(viewY+TheBitmap.getHeight())) {                 
                if (TheBitmap.getPixel(iX,iY)!=0) {
                    onStartCallButtonClicked(v);
                    showPressedState();
                    return false; 
                } 
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            showNormalState();
            break;
    }           
    return true;
}

private void showNormalState() {
    // set your button background drawable to show normal state
}

private void showPressedState() {
    // set your button background drawable to show pressed state
}
Monorail answered 22/6, 2012 at 16:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.