带有 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"

你在搞乱 touchfocus 事件。让我们从理解具有相同颜色的行为开始。默认情况下,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(在[中返回truefalse =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_UPonTouchEvent 方法。我们看到那里发生了执行点击操作。因此,在 OnTouchListeneronTouch 中返回 true 并因此不调用 onTouchEvent,导致不调用 OnClickListeneronClick

还有一个不调用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;
}

UnsetPressedState:

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 事件,您可以通过应用主题进行自定义。