21

I've got a button graphic which needs to have "press and hold" functionality, so instead of using onClickListener, I'm using onTouchListener so that the app can react to

 MotionEvent.ACTION_DOWN,

and

 MotionEvent.ACTION_UP

Depending on how quickly those two events are triggered, I can run a "pressAndHoldHandler" in the time between the two.

Anyway, long story short: I have numerous "basic" buttons in the same app that don't require the press and hold functionality, so they are using the onClickListener.

Every single one of these buttons have been customized graphically with their own XML selector file:

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

    <item
        android:state_enabled="false"
        android:drawable="@drawable/btn_chicken_off" />

    <item
        android:state_enabled="true"
        android:state_pressed="true"
        android:drawable="@drawable/btn_chicken_s3" />

    <item
        android:state_enabled="true"
        android:state_focused="true"
        android:drawable="@drawable/btn_chicken_s2" />

    <item
        android:state_enabled="true"
        android:drawable="@drawable/btn_chicken_off" />

</selector>

So, the problem here is: The above selector doesn't get accessed with the onTouchListener. Only the onClickListener will pull in the state changes with the onClick() section of its own method, so these "press and hold" buttons never change state. Pretty terrible feedback for the user.

I'm currently forcing the above inside the switch case of ACTION_DOWN and ACTION_UP by doing the following:

if (action == MotionEvent.ACTION_DOWN) {
    btn_chicken.setBackgroundResource(R.drawable.btn_chicken_s3);
}
else
    if (action == MotionEvent.ACTION_UP) {
        btn_chicken.setBackgroundResource(R.drawable.btn_chicken_off);
    }

But it seems like a hack, and it's missing the "focused but not pressed" stage.

Anybody tripped across this before?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Octoth0rpe
  • 2,267
  • 4
  • 19
  • 21

5 Answers5

46

Use the view.setPressed() function to simulate the pressed behavior by yourself.

You would probably want to enable the Pressed state when you get ACTION_DOWN event and disable it when you get the ACTION_UP event.

Also, it will be a good idea to disable it in case the user slide out of the button. Catching the ACTION_OUTSIDE event, as shown in the example below:

@Override
public boolean onTouch(View v, MotionEvent event) {

    switch (event.getAction() & MotionEvent.ACTION_MASK) {

    case MotionEvent.ACTION_DOWN:
        v.setPressed(true);
        // Start action ...
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_OUTSIDE:
    case MotionEvent.ACTION_CANCEL:
        v.setPressed(false);
        // Stop action ...
        break;
    case MotionEvent.ACTION_POINTER_DOWN:
        break;
    case MotionEvent.ACTION_POINTER_UP:
        break;
    case MotionEvent.ACTION_MOVE:
        break;
    }

    return true;
}
Eyal Biran
  • 5,656
  • 1
  • 28
  • 29
  • 2
    I don't think ACTION_OUTSIDE actually gets triggered. That appears to be specifically for touching outside of dialogs. – yincrash Jun 06 '14 at 20:45
  • `ACTION_CANCEL` would be a good place to call `view.setPressed(false);` – Leo Landau Aug 13 '14 at 16:25
  • Putting `view.setPressed(false)` in `MotionEvent.ACTION_UP:` worked for me. I couldn't get it to not work, even if I slid off of the view rather than just lifting up my finger. – Suragch Jan 01 '15 at 11:03
5

Make sure to return false at the end of the onTouchListener() function. :)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Lei
  • 690
  • 1
  • 7
  • 13
3

You can just set the button onClickListener and leave its onClick method empty. Your logic implement inside onTouch. That way you'll have the press effect.

P.S You don't need all those state in the selector you can simply use:

<?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/IMAGE_PRESSED" />

    <item android:drawable="@drawable/IMAGE" />

</selector>
Givi
  • 3,283
  • 4
  • 20
  • 23
  • well, I tried that, and it won't work: onTouch isn't a method of onClickListener. and I do still need to have the for those using a trackball. any other ideas folks? – Octoth0rpe Sep 26 '12 at 16:30
  • i meant for you to leave your ontouch method as is, but remove the piece of code that changes the background from it. IN ADDITION, set the view on click listener (i.e yourView.setOnClickListener(new OnClick....) and leave it empty. That way you'll have the press effect. – Givi Sep 26 '12 at 17:03
  • I can confirm the above doesn't work :( @Octoth0rpe: Did you find a workaround? – Andrés Pachon Nov 19 '12 at 18:29
  • Adding an onClickListener, even if it's empty, will cause the onClickListener to steal events from the onTouchListener - so you'll get your touch feedback, but your onTouchListener is now useless, which begs the question 'why are you using a useless onTouchListener'. – Tim Malseed Mar 04 '15 at 01:57
3

I figured it out! Just use view.setSelected() as follows:

@Override
public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        yourView.setSelected(true);
        ...
        return true;
    }
    else if(event.getAction() == MotionEvent.ACTION_UP){
        yourView.setSelected(false);
        ...
        return true;
    }
    else 
        return false;
}

That'll make your selector work even if your view has an onTouch listener :)

Andrés Pachon
  • 838
  • 1
  • 7
  • 15
3

Better solution:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_activated="true">
        <bitmap android:src="@drawable/image_selected"/>
    </item>
    <item>
        <bitmap android:src="@drawable/image_not_selected"/>
    </item>
</selector>

@Override
public void onClick(View v) {
    if (v.isActivated())
       v.setActivated(false);
    else
       v.setActivated(true);
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
M. Usman Khan
  • 3,689
  • 1
  • 59
  • 69