在 android 中,焦点的获取和事件差不多,有一个分发机制,一般来说View 树上上层节点的 ViewGroup 比底层节点的 View 有更高的优先级获取焦点,这体现在 ViewGroup 有一个属性 descendantFocusability 可以用来控制焦点获取的优先级。 该属性的值有三种:
- beforeDescendants:ViewGroup 会优先其子类控件而获取到焦点,如果父控件不获取焦点,子控件才可能会获得焦点
- afterDescendants:只有当其子类控件不需要获取焦点时,ViewGroup 才可能获取焦点
- blocksDescendants:viewgroup 会阻塞子类控件获得焦点,无论 ViewGroup 是否获取焦点,子控件都不能够获得焦点
如何判断 View 是否能够获取焦点
因为上面的原因,判断一个 View 是否有可能获得焦点,就与它所有的父节点有关系了,所以 View 的 hasFocusable 方法实现如下:
public boolean hasFocusable() {
if (!isFocusableInTouchMode()) {
for (ViewParent p = mParent; p instanceof ViewGroup; p = p.getParent()) {
final ViewGroup g = (ViewGroup) p;
if (g.shouldBlockFocusForTouchscreen()) {
return false;
}
}
}
return (mViewFlags & VISIBILITY_MASK) == VISIBLE && isFocusable();
}
- 在触摸模式下如果不可获取焦点,先遍历 View 的所有父节点,如果有一个父节点设置了阻塞子 View 获取焦点,那么该 View 就不可能获取焦点
- 在触摸模式下如果不可获取焦点,并且没有父节点设置阻塞子 View 获取焦点,和在触摸模式下如果可以获取焦点,那么才判断 View 自身的 visiable 和 focusable 属性,来决定是否可以获取焦点,不可见的 View 当然也不能获取焦点,所以只有 visiable 和 focusable 同时为 true,该View 才可能获取焦点
如何判断 ViewGroup 是否能够获取焦点
判断一个 ViewGroup 是否可能获取到焦点,也与它的子 View 有关系了,如果子 View 可以获取焦点,辣么,就可以算作这个 ViewGroup 也可能获取(消耗)焦点,所以 ViewGroup 的 hasFocusable 方法实现如下:
public boolean hasFocusable() {
if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
if (isFocusable()) {
return true;
}
final int descendantFocusability = getDescendantFocusability();
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
if (child.hasFocusable()) {
return true;
}
}
}
return false;
}
根据上面的源码可知,判断 ViewGroup 是否能够获取焦点就简单多了
- 如果 ViewGroup visiable 和 focusable 都为 true,就算能够获取焦点
- 如果不满足第 1 点,如果子 View 节点能够获取焦点,该 ViewGroup 也算能获取焦点(通过递归实现)
- 否则,就算作不能够获取到焦点
- ViewGroup 自己能够获取焦点,完全没有考虑是否在触摸模式的情况,不晓得是不是 google 工程师的 bug,快到 google groups 提问 ^-^!
View 是否能够获取焦点的实际应用
View 是否能够获取焦点,通常在 AbsListView 的 OnItemClickListener,EditText,Button 的使用和 key 事件分发中会应用到。比如 ListView 的 OnItemClickListener,只有在 ListView 的 ItemView 不能获取到 focus 的情况下,才会调用 OnItemClickListener 的 onItemClick 方法。具体代码可以参考 AbsListView 的 onTouchUp 方法:
private void onTouchUp(MotionEvent ev) {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
......
该省就省,我是省略代码分割符 ^_^
......
if (inList && !child.hasFocusable()) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
......
该省就省,我是省略代码分割符 ^_^
......
mTouchModeReset = new Runnable() {
@Override
public void run() {
mTouchModeReset = null;
mTouchMode = TOUCH_MODE_REST;
child.setPressed(false);
setPressed(false);
if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
performClick.run();
}
}
};
postDelayed(mTouchModeReset,
ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
updateSelectorState();
}
return;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
performClick.run();
}
}
}
......
该省就省,我是省略代码分割符 ^_^
......
}
从上面代码的第 9 行可知,ListView 的 ItemView 的 hasFocusable 方法必须返回 fasle,才会执行 performClick.run(),才会执行到 OnItemClickListener 的 onItemClick 方法。这也就是为什么通常我们在使用 ListView 的 OnItemClickListener 方法的时候要把 Item 布局文件的根节点的 descendantFocusability 属性设置为 blocksDescendants,并且该节点的 focusable 属性不能为 true。