focusableInTouchMode导致Click事件失效

2,587 阅读4分钟

前言

focusableInTouchMode="ture"是Android开发常用的一个属性,SDK源码说的很清楚,表示触摸事件可以让View获取焦点。

<!-- Boolean that controls whether a view can take focus while in touch mode.
     If this is true for a view, that view can gain focus when clicked on, and can keep
     focus if another view is clicked on that doesn't have this attribute set to true. -->
<attr name="focusableInTouchMode" format="boolean" />

日常开发中通常配合clickable="true"和focusable="true"一起使用。但是这里有一个有意思的点,当一个View设置了clickable="true"、focusable="true"、focusableInTouchMode="ture"后,这个View的onClickListener并不会第一时间触发,而是要点击两次才会触发事件,实际开发中要注意这一点。

探究原因

那么出现这样情况的原因是什么呢?Android开发的一个好处就是,所有的问题源码里都有解释,看看源码里是怎么写的,这里以Android 10.0的源码为例,找到onTouchEvent()方法

  public boolean onTouchEvent(MotionEvent event) {
       ...
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            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();
                                }
                            }
                        }
                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                ... 
            }

            return true;
        }

        return false;
    }

我们可以清楚的看到,MotionEvent.ACTION_UP事件中,设置focusableInTouchMode="ture"之后,未获取焦点时,会先去获取焦点,并跳过此次Click事件,当获取了焦点之后,才会去触发Click事件。

if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
	focusTaken = requestFocus();
}


// Only perform take click actions if we were in the pressed state
                            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();
                                }
                            }

这样就解释为什么会需要点击两次才能出发Click事件。

解决问题

既然找到问题原因了,那么怎么解决这个问题呢?其实很简单,看我们的实际需求,可以有两种处理方式:

  • 第一种

    设置focusableInTouchMode="false"。

  • 第二种

    那如果我们必须要用到focusableInTouchMode="true",那怎么办呢?其实也简单,只用多加一步处理就行:将目标View 设置setOnFocusChangeListener(),在焦点事件里处理你的业务逻辑,这样就可以了

    view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    // TODO: 20/4/26  
                }
            });
    view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // TODO: 20/4/26  
                }
            });
    

focusableInTouchMode使用小技巧

说了前面那么多?日常开发中focusableInTouchMode="true",哪些地方会用到呢?这里给一个常见的场景,两个View,选中的时候,切换不同的状态

常见的实现方式是使用RadioGroup搭配RadioButton。这里提供另外一种思路,就使用TextView配合focusableInTouchMode来实现:

  • 第一步

    首先还是一样,自定义TextView背景

    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item android:state_focused="false">
            <color android:color="@color/white" />
        </item>
    
        <item android:state_focused="true">
            <layer-list>
                <item >
                    <color android:color="@color/white" />
                </item>
                <item android:height="2dp" android:gravity="bottom">
                    <color android:color="@color/colorAccent" />
                </item>
            </layer-list>
            <color android:color="@color/white" />
        </item>
    
    </selector>
    

    然后自定义TextView字体颜色

    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item android:state_focused="false" android:color="@color/gray"/>
        <item android:state_focused="true" android:color="@color/colorAccent"/>
    
    </selector>
    
  • 第二步

    布局文件中配置,主要是clickable、focusable、focusableInTouchMode这个三个属性

        <TextView
            android:id="@+id/tv_left"
            android:layout_width="100dp"
            android:layout_height="40dp"
            android:layout_marginTop="80dp"
            android:background="@drawable/selector_text_bg"
            android:clickable="true"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:gravity="center"
            android:text="LEFT"
            android:textColor="@drawable/selector_text_color_gray_blue" />
    
  • 第三步

    设置监听事件,处理自己的业务逻辑

    tvLeft.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    Toast.makeText(FocusTestActivity.this, "获取焦点", Toast.LENGTH_SHORT).show();
                }
            });
    tvLeft.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(FocusTestActivity.this, "点击事件", Toast.LENGTH_SHORT).show();
                }
            });
    

    很简单吧,但是实现方式又不一样,是不是很有意思 :)本文所有代码均上传到Github