FOCUSABLE_AUTO--一个View Focusable Flag的探究

1,590 阅读5分钟

每一个迭代中,总会有那么几个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.
  • 需要注意的是,如果是在代码中设置setFocusableInTouchModesetFocusable,最好偏后调用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的CLICKABLE flag过程中如果当前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的源码里面瞅一瞅也是挺好玩滴~~~

参考资料: