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?
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" />
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)
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;
}
});
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With