10

I'm having a problem with my Button staying in a highlighted state, after doing the following:

public class MainActivity extends AppCompatActivity {

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AppCompatButton button = (AppCompatButton) findViewById(R.id.mybutton);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("Test", "calling onClick");
            }
        });
        button.setOnTouchListener(new View.OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN: {
                        v.getBackground().setColorFilter(0xe0f47521,PorterDuff.Mode.SRC_ATOP);
                        v.invalidate();
                        break;
                    }
                    case MotionEvent.ACTION_UP: {
                        v.getBackground().clearColorFilter();
                        v.invalidate();
                        v.performClick();
                        Log.d("Test", "Performing click");
                        return true;
                    }
                }
                return false;
            }
        });

    }
}

Concerning the code above, when using it, I'm expecting the button click to be handled by the touch, and by returning "true" the handling should stop at the touchListener.

But this is not the case. The button stays in a highlighted state, even though the click is being called.

What I get is:

Test - calling onClick
Test - Performing click

on the other hand, if I'm using the following code, the button is clicked, same prints, but the button doesn't end up stuck in a highlighted state:

public class MainActivity extends AppCompatActivity {

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AppCompatButton button = (AppCompatButton) findViewById(R.id.mybutton);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("Test", "calling onClick");
            }
        });
        button.setOnTouchListener(new View.OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN: {
                        v.getBackground().setColorFilter(0xe0f47521,PorterDuff.Mode.SRC_ATOP);
                        v.invalidate();
                        break;
                    }
                    case MotionEvent.ACTION_UP: {
                        v.getBackground().clearColorFilter();
                        v.invalidate();
                        // v.performClick();
                        Log.d("Test", "Performing click");
                        return false;
                    }
                }
                return false;
            }
        });

    }
}

I'm a bit confused as to what's the responder chain to the touch event. My guess is that it's:

1) TouchListener

2) ClickListener

3) ParentViews

Can someone confirm this as well?

Whitebear
  • 1,761
  • 2
  • 12
  • 22
  • What you actually want to do, is it handling by touch or change color on just press? – Haider Saleem Nov 12 '19 at 16:17
  • I want to run some logic in touch and then call performClick and for it not to change the color of the button. – Whitebear Nov 12 '19 at 16:35
  • @Whitebear Please check the answer below. Maybe I can add more information. – GensaGames Nov 12 '19 at 17:17
  • [This](https://stackoverflow.com/questions/7449799/how-are-android-touch-events-delivered/46862320#46862320) might help you with understanding the flow of touch events. It's not clear what you want to occur. Do you want a click handler and to do the perform click? Do you want the button to go from its initial color to the state set by the color filter then back to its initial color? – Cheticamp Nov 13 '19 at 02:46
  • Let me explain what I mean. I have a touchListener and a clickListener on the button. The touch precedes the click by priority and returns true if it handled the event, which means that no one else should handle it. This is exactly what I'm doing through the touch, handling the click and returning true, yet the button still stays highlighted even though it's onclick listener is called and the flow is done correctly. – Whitebear Nov 14 '19 at 09:09

4 Answers4

9

Such customizations need no programmatically modifications. You can do it simply in xml files. First of all, delete the setOnTouchListener method that you provide in the onCreate entirely. Next, define a selector color in the res/color directory like the following. (if the directory doesn't exist, create it)

res/color/button_tint_color.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#e0f47521" android:state_pressed="true" />
    <item android:color="?attr/colorButtonNormal" android:state_pressed="false" />
</selector>

Now, set it to the button's app:backgroundTint attribute:

<androidx.appcompat.widget.AppCompatButton
    android:id="@+id/mybutton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    app:backgroundTint="@color/button_tint_color" />


Visual Result:

enter image description here



EDITED: (to address touch event issue)

From an overall point of view, the flow of the touch event starts from the Activity, then flows down to the layout (from the parent to the child layouts), and then to the views. (LTR flow in the following picture)

enter image description here

When the touch event reaches the target view, the view can handle the event then decide to pass it to the prior layouts/activity or not (returning false of true in onTouch method). (RTL flow in the above picture)

Now let's take a look at the View's source code to gain a deeper insight into the touch event flows. By taking a look at the implementation of the dispatchTouchEvent, we'd see that if you set an OnTouchListener to the view and then return true in its onTouch method, the onTouchEvent of the view won't be called.

public boolean dispatchTouchEvent(MotionEvent event) {
    // removed lines for conciseness...
    boolean result = false;    
    // removed lines for conciseness...
    if (onFilterTouchEventForSecurity(event)) {
        // removed lines for conciseness...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { // <== right here!
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    // removed lines for conciseness...
    return result;
}

Now, look at the onTouchEvent method where the event action is MotionEvent.ACTION_UP. We see that perform-click action happens there. So, returning true in the OnTouchListener's onTouch and consequently not calling the onTouchEvent, causes not calling the OnClickListener's onClick.

There is another issue with not calling the onTouchEvent, which is related to the pressed-state and you mentioned in the question. As we can see in the below code block, there is an instance of UnsetPressedState that calls setPressed(false) when it runs. The result of not calling setPressed(false) is that the view gets stuck in the pressed state and its drawable state doesn't change.

public boolean onTouchEvent(MotionEvent event) {
    // removed lines for conciseness...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // removed lines for conciseness...
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // removed lines for conciseness...
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // removed lines for conciseness...
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }
                    // removed lines for conciseness...
                }
                // removed lines for conciseness...
                break;
            // removed lines for conciseness...
        }
        return true;
    }
    return false;
}

UnsetPressedState:

private final class UnsetPressedState implements Runnable {
    @Override
    public void run() {
        setPressed(false);
    }
}


Regarding the above descriptions, you can change the code by calling setPressed(false) yourself to change the drawable state where the event action is MotionEvent.ACTION_UP:

button.setOnTouchListener(new View.OnTouchListener() {

    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                v.getBackground().setColorFilter(0xe0f47521,PorterDuff.Mode.SRC_ATOP);
                v.invalidate();
                break;
            }
            case MotionEvent.ACTION_UP: {
                v.getBackground().clearColorFilter();
                // v.invalidate();
                v.setPressed(false);
                v.performClick();
                Log.d("Test", "Performing click");
                return true;
            }
        }
        return false;
    }
});
aminography
  • 21,986
  • 13
  • 70
  • 74
  • This is not what I'm looking for my friend. I'm looking to understand the change in behavior in both situations I described above. If there's something I can elaborate about, please let me know. I don't want to delete the touch handler nor the click handler. Please check out my comment to the answer above. – Whitebear Nov 14 '19 at 09:10
  • @Whitebear: I've updated the answer. Please check it out, dude. – aminography Nov 14 '19 at 19:11
  • That's a good detailed answer, and I'd accept it. A few pointers to change: ```Now, look at the onTouchEvent method where the event action is MotionEvent.ACTION_UP. We see that perform-click action happens there. So, returning true in the OnTouchListener's onTouch and consequently not calling the onTouchEvent, causes not calling the OnClickListener's onClick.``` In my case the onClick is called. mUnsetPressedState checks if it's null before setting to false and also the runnable is not sure to run if we're prepressed. I don't quite understand how you deduct that it should be set to false – Whitebear Nov 17 '19 at 10:12
  • The `onClick` is called because you are calling `v.performClick();`. Please check the above code in `MotionEvent.ACTION_UP` section again, `setPressed(false)` is called anyway, whether `mUnsetPressedState` is null or not, whether `prepressed` is true or not. The difference is in the way of calling `setPressed(false)` that could be through `post`/`postDelayed` or directly. – aminography Nov 19 '19 at 10:06
2

You are messing around touch and focus events. Let start with understanding behavior with same color. By default, there is Selector assigned as background to the Button in Android. So simply changing background color, make is static (color will not change). But it's not a native behavior.

Selector might look like this one.

<?xml version="1.0" encoding="utf-8"?> 
  <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_focused="true"
        android:state_pressed="true"
        android:drawable="@drawable/bgalt" />

    <item
        android:state_focused="false"
        android:state_pressed="true"
        android:drawable="@drawable/bgalt" />

    <item android:drawable="@drawable/bgnorm" />
</selector>

As you can see above, there is state focused and state pressed. By setting onTouchListener you will handle touch events, which have nothing to do with focus.

Selector of the Button should replace focus event with touch during click event on the button. But in first part of your code you intercepted events for the touch (returning true from callback). Color change cannot proceed further and is freezing with same color . And that is why second variant (without interception) are working fine and that is your confusion.

UPDATE

All you need to do it's to change behavior and color for the Selector. For ex. by using next background for the Button. AND remove onTouchListener from your implementation at all.

<?xml version="1.0" encoding="utf-8"?> 
  <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:drawable="@color/color_pressed" />

    <item android:drawable="@color/color_normal" />
</selector>
GensaGames
  • 5,538
  • 4
  • 24
  • 53
  • How would you change the first example to work well then? – Whitebear Nov 12 '19 at 18:15
  • Im not looking to remove the touch listener from my implementation. as I want to catch clicks on a specific view using the touch handler, and then handle them myself (using performClick) and return true from the touch listener in order to tell the other handlers that no further handling is done. The logs are printed fine in my example, yet the button still stays highlighted. – Whitebear Nov 14 '19 at 09:11
  • @Whitebear You didn't mentioned that in you questions. In any way, you can use as many `onTouchListener`s as you want. You just don't need to consume event, by `return true`. – GensaGames Nov 14 '19 at 17:13
  • @Whitebear **OR** Remove selector and set raw color to the button via `backgroundColor`. – GensaGames Nov 14 '19 at 17:13
  • guys, you still aren't addressing what I wrote in the original post.. – Whitebear Nov 17 '19 at 09:53
0

if you assign a background to the button, it wont change the color on click.

 <color name="myColor">#000000</color>

and set it as backgrount to your button

android:background="@color/myColor"
Haider Saleem
  • 773
  • 1
  • 9
  • 17
0

you can just use material Chips for instead of Button view. refer : https://material.io/develop/android/components/chip there they handle those hililghted events and you can customize with applying the themes.