带有 touchListener 和 clickListener 的按钮处于高亮状态
Button left in highlighted state with touchListener and clickListener
在执行以下操作后,我的 Button 一直处于突出显示状态时遇到问题:
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;
}
});
}
}
关于上面的代码,在使用它时,我期望按钮点击由触摸处理,并且通过返回 "true" 处理应该在 touchListener 处停止。
但事实并非如此。按钮保持突出显示状态,即使正在调用点击。
我得到的是:
Test - calling onClick
Test - Performing click
另一方面,如果我使用下面的代码,按钮被点击,打印相同,但按钮最终不会停留在突出显示状态:
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;
}
});
}
}
对于触摸事件的响应链是什么,我有点困惑。我的猜测是:
1) TouchListener
2) 点击监听器
3) 父视图
有人也可以证实这一点吗?
如果您为按钮指定背景,它不会在点击时改变颜色。
<color name="myColor">#000000</color>
并将其设置为按钮的背景
android:background="@color/myColor"
你在搞乱 touch
和 focus
事件。让我们从理解具有相同颜色的行为开始。默认情况下,Selector
被指定为 Android 中的 Button
的背景。所以简单地改变背景颜色,make是静态的(颜色不会改变)。但这不是本机行为。
Selector
可能看起来像这个。
<?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>
正如您在上面看到的,有状态 focused
和状态 pressed
。通过设置 onTouchListener
你将处理触摸事件,这与 focus
无关。
按钮的 Selector
应该在按钮的点击事件期间用 touch
替换 focus
事件。但是在代码的第一部分中,您拦截了 touch
的事件(从回调中返回 true)。颜色变化无法进一步进行,并以相同的颜色冻结。这就是为什么第二种变体(没有拦截)工作正常,这就是你的困惑。
更新
您只需更改 Selector
的行为和颜色即可。对于前。通过为 Button
使用下一个背景。 AND 从您的实施中完全删除 onTouchListener
。
<?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>
此类自定义不需要以编程方式进行修改。您可以简单地在 xml
个文件中完成。首先,完全删除您在 onCreate
中提供的 setOnTouchListener
方法。接下来,在 res/color
目录中定义一个选择器颜色,如下所示。 (如果该目录不存在,请创建它)
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>
现在,将其设置为按钮的 app:backgroundTint
属性:
<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" />
视觉结果:
已编辑:(解决触摸事件问题)
从整体来看,触摸事件的流程从Activity
开始,然后向下流到布局(从父布局到子布局),再到视图。 (下图LTR流程)
当触摸事件到达目标视图时,视图可以处理该事件,然后决定是否将其传递给先前的layouts/activity(在[中返回true
的false
=24=] 方法)。 (上图RTL流程)
现在我们来看一下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
不会调用的视图
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;
}
现在,查看事件操作为 MotionEvent.ACTION_UP
的 onTouchEvent
方法。我们看到那里发生了执行点击操作。因此,在 OnTouchListener
的 onTouch
中返回 true
并因此不调用 onTouchEvent
,导致不调用 OnClickListener
的 onClick
。
还有一个不调用onTouchEvent
的问题,跟问题中提到的pressed-state有关。正如我们在下面的代码块中看到的,运行时有一个 UnsetPressedState
that calls setPressed
(false)
的实例。不调用 setPressed(false)
的结果是视图卡在按下状态并且其可绘制状态不会改变。
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;
}
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
关于上面的描述,可以通过自己调用setPressed(false)
来更改代码,改变事件动作所在的drawable状态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;
}
});
您可以只使用 material 条块代替按钮视图。
参考:https://material.io/develop/android/components/chip
他们在那里处理那些 hililghted 事件,您可以通过应用主题进行自定义。
在执行以下操作后,我的 Button 一直处于突出显示状态时遇到问题:
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;
}
});
}
}
关于上面的代码,在使用它时,我期望按钮点击由触摸处理,并且通过返回 "true" 处理应该在 touchListener 处停止。
但事实并非如此。按钮保持突出显示状态,即使正在调用点击。
我得到的是:
Test - calling onClick
Test - Performing click
另一方面,如果我使用下面的代码,按钮被点击,打印相同,但按钮最终不会停留在突出显示状态:
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;
}
});
}
}
对于触摸事件的响应链是什么,我有点困惑。我的猜测是:
1) TouchListener
2) 点击监听器
3) 父视图
有人也可以证实这一点吗?
如果您为按钮指定背景,它不会在点击时改变颜色。
<color name="myColor">#000000</color>
并将其设置为按钮的背景
android:background="@color/myColor"
你在搞乱 touch
和 focus
事件。让我们从理解具有相同颜色的行为开始。默认情况下,Selector
被指定为 Android 中的 Button
的背景。所以简单地改变背景颜色,make是静态的(颜色不会改变)。但这不是本机行为。
Selector
可能看起来像这个。
<?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>
正如您在上面看到的,有状态 focused
和状态 pressed
。通过设置 onTouchListener
你将处理触摸事件,这与 focus
无关。
Selector
应该在按钮的点击事件期间用 touch
替换 focus
事件。但是在代码的第一部分中,您拦截了 touch
的事件(从回调中返回 true)。颜色变化无法进一步进行,并以相同的颜色冻结。这就是为什么第二种变体(没有拦截)工作正常,这就是你的困惑。
更新
您只需更改 Selector
的行为和颜色即可。对于前。通过为 Button
使用下一个背景。 AND 从您的实施中完全删除 onTouchListener
。
<?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>
此类自定义不需要以编程方式进行修改。您可以简单地在 xml
个文件中完成。首先,完全删除您在 onCreate
中提供的 setOnTouchListener
方法。接下来,在 res/color
目录中定义一个选择器颜色,如下所示。 (如果该目录不存在,请创建它)
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>
现在,将其设置为按钮的 app:backgroundTint
属性:
<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" />
视觉结果:
已编辑:(解决触摸事件问题)
从整体来看,触摸事件的流程从Activity
开始,然后向下流到布局(从父布局到子布局),再到视图。 (下图LTR流程)
当触摸事件到达目标视图时,视图可以处理该事件,然后决定是否将其传递给先前的layouts/activity(在[中返回true
的false
=24=] 方法)。 (上图RTL流程)
现在我们来看一下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
不会调用的视图
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;
}
现在,查看事件操作为 MotionEvent.ACTION_UP
的 onTouchEvent
方法。我们看到那里发生了执行点击操作。因此,在 OnTouchListener
的 onTouch
中返回 true
并因此不调用 onTouchEvent
,导致不调用 OnClickListener
的 onClick
。
还有一个不调用onTouchEvent
的问题,跟问题中提到的pressed-state有关。正如我们在下面的代码块中看到的,运行时有一个 UnsetPressedState
that calls setPressed
(false)
的实例。不调用 setPressed(false)
的结果是视图卡在按下状态并且其可绘制状态不会改变。
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;
}
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
关于上面的描述,可以通过自己调用setPressed(false)
来更改代码,改变事件动作所在的drawable状态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;
}
});
您可以只使用 material 条块代替按钮视图。 参考:https://material.io/develop/android/components/chip 他们在那里处理那些 hililghted 事件,您可以通过应用主题进行自定义。