前言
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