每一个迭代中,总会有那么几个bug,让人微笑、默叹、以为绝妙.
从一个Bug说起
故事要从几天前遇到的一个Bug说起,先简单介绍一下这个bug的现象:有那么一个ViewGroup parent,有若干个可获焦的child view。
正常情况下,焦点转移到这个View体系里面的时候,某一个child view会获得焦点,并切换为获焦形态。在用来测试的设备上,ViewGroup似乎抢夺了焦点,导致这个View体系 疑似失去了焦点。
接到Bug单,首先肯定是要先在本地的设备看看能不能复现,要是能复现就完全不是问题。
哐哧哐哧几下进入页面,操作一波,发现焦点都是正常的,没有Bug单描述里提到的整体丢焦的问题。
这个时候看到Bug单提供的设备搭载的Android 9.0系统,感觉应该是高版本Android的问题。
Usb接了另一台Android 9.0的设备看看。哇,随便进入一个页面,页面丢焦的问题就出来了,打开Layout Inspector一看,好家伙,这个ViewGroup怎么就突然可获焦了,相同的代码,在5.0的设备上跑的好好的,怎么在你这个9.0的设备上就不行了。那这个肯定是兼容性问题了。 既然这个ViewGroup现在不需要获焦,那么我就手动设置一下不可获焦吧。
手动加上这两句配置之后,跑了一下,诶,果然表现正常了,看来就是这里的问题,高版本的Android系统上应该有什么变动,导致这个ViewGroup可获焦了,明确指定不可获焦属性就可以完美解决了。 好,搞定收工~~
android:focusable="false"
android:focusableInTouchMode="false"
提交代码~~ 转向下一个Bug。
本文结束。
探究一下这个Focus兼容性问题
好吧,虽说是找到了Bug的修复方案,但是糊里糊涂地解掉一个Bug真是让人心里不踏实,还是去追究一下问题究竟出在那里吧。
上文可知,在高版本的Android系统中,View的Focusable的属性的处理似乎出现了变化,在低版本中默认View未设置focusable时不可获焦的 惯例 会有出不同的表现,那么这个高版本的分界线到底在哪里呢?
查询了资料之后,上面这个问题的原因,来源于Android 8.0(API 26)在View中引入
的FOCUSABLE_AUTO Flag。
从Android 官网 中的相关介绍,FOCUSABLE_AUTO这个Flag在API 26时添加到Android
SDK中:
This view determines focusability automatically. This is the default. Use with setFocusable(int) and android:focusable.
从官方的注释来看,View的Focusable的默认值被更改成了View#FOCUSABLE_AUTO,并且View可以自由决定自己是否可获焦。
那这个新引入的Flag究竟是怎么影响了View的获焦行为的呢?让我们跳进源码一探究竟。
- View初始化时,默认会设置当前View的focusable Flag为
FOCUSABLE_AUTO,在解析xml配置的focus属性时会在getFocusableAttribute中。 - 如果View的xml中没有明确指定focusable=true/false,那么View的focusable初始Flag为
FOCUSABLE_AUTO。这里目前还没有看到新增的Flag的影响,我们继续往下走。
...
// Set default values.
viewFlagValues |= FOCUSABLE_AUTO;
viewFlagMasks |= FOCUSABLE_AUTO;
...
//解析xml属性
case com.android.internal.R.styleable.View_focusable:
viewFlagValues = (viewFlagValues & ~FOCUSABLE_MASK) | getFocusableAttribute(a);
if ((viewFlagValues & FOCUSABLE_AUTO) == 0) {
viewFlagMasks |= FOCUSABLE_MASK;
}
break;
...
//返回xml配置的focusable flag,
//如果未配置,返回`FOCUSABLE_AUTO`
private int getFocusableAttribute(TypedArray attributes) {
TypedValue val = new TypedValue();
if (attributes.getValue(com.android.internal.R.styleable.View_focusable, val)) {
if (val.type == TypedValue.TYPE_INT_BOOLEAN) {
return (val.data == 0 ? NOT_FOCUSABLE : FOCUSABLE);
} else {
return val.data;
}
} else {
return FOCUSABLE_AUTO;
}
}
- 外界调用
View.setFocusable时,如果是不可获焦,那么会顺便将focusableInTouchMode设置为false,整体影响不大,继续往下走。
public void setFocusable(@Focusable int focusable) {
//设置为不可获焦
if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
setFlags(0, FOCUSABLE_IN_TOUCH_MODE);
}
setFlags(focusable, FOCUSABLE_MASK);
}
- 外界调用
View.setFocusableInTouchMode时,如果指定了focusableInTouchMode为true,那么需要同时将View的Focusable Flag置为true,清除FOCUSABLE_AUTO. - 需要注意的是,如果是在代码中设置
setFocusableInTouchMode和setFocusable,最好偏后调用setFocusable,使得设置为focusable后能正常触发可能存在的切换焦点逻辑View.focusableViewAvailable。
public void setFocusableInTouchMode(boolean focusableInTouchMode) {
// Focusable in touch mode should always be set before the focusable flag
// otherwise, setting the focusable flag will trigger a focusableViewAvailable()
// which, in touch mode, will not successfully request focus on this view
// because the focusable in touch mode flag is not set
setFlags(focusableInTouchMode ? FOCUSABLE_IN_TOUCH_MODE : 0, FOCUSABLE_IN_TOUCH_MODE);
// Clear FOCUSABLE_AUTO if set.
if (focusableInTouchMode) {
// Clears FOCUSABLE_AUTO if set.
setFlags(FOCUSABLE, FOCUSABLE_MASK);
}
}
- 继续往下走,在
View.setFlag中,我们似乎发现了问题所在。 - 在设置View的
CLICKABLEflag过程中如果当前flag为FOCUSABLE_AUTO,那么认定可点击的View应该是需要获焦的,然后将View的Focusable Flag置为了true,否则设置为false,那这里应该就是FOCUSABLE_AUTO的变动之处了。
...
//变化的flag
int changed = mViewFlags ^ old;
if (changed == 0) {
return;
}
int privateFlags = mPrivateFlags;
boolean shouldNotifyFocusableAvailable = false;
// If focusable is auto, update the FOCUSABLE bit.
int focusableChangedByAuto = 0;
//当前是FOCUSABLE_AUTO
//改动了FOSUSABLE_FLAG or CLICKABLE flag
if (((mViewFlags & FOCUSABLE_AUTO) != 0)
&& (changed & (FOCUSABLE_MASK | CLICKABLE)) != 0) {
// Heuristic only takes into account whether view is clickable.
final int newFocus;
if ((mViewFlags & CLICKABLE) != 0) {
newFocus = FOCUSABLE;
} else {
newFocus = NOT_FOCUSABLE;
}
mViewFlags = (mViewFlags & ~FOCUSABLE) | newFocus;
focusableChangedByAuto = (old & FOCUSABLE) ^ (newFocus & FOCUSABLE);
changed = (changed & ~FOCUSABLE) | focusableChangedByAuto;
}
...
- 翻看代码,在设置View的Listener时,会更新View的CLICKABLE Flag,如果View的Focusable Flag为
FOCUSABLE_AUTO的话,那么,View会被设置成可获焦的元素,导致抢夺了焦点. - 这一点可以确定,这个ViewGroup确实是有设置了监听器,最终导致整个View可获焦了。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
...
public void setClickable(boolean clickable) {
setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}
- 跟了一阵子之后,最终发现在高版本的Android系统中,默认的Focusable Flag处理逻辑不一样,导致整体的效果不可预期。
- 对比了下API 25的View源码,View的默认Focusable为false,也没有设置为clickable后会更新View的Focusable Flag的逻辑。Android源码相关提交Diff如下,不过不太清楚这个新引入的Flag的目的是什么~~
结论
总的来说,为了处理对应的兼容性问题,在布局过程中,无需获焦的View需要显式地设置focusable为false,避免命中了高版本Android系统中不可知的坑。 另外就是,有时候偶尔潜入Android的源码里面瞅一瞅也是挺好玩滴~~~
完
参考资料: