开发TV应用与开发手机应用的最大不同就是焦点
关键方法梳理
- 触摸模式
- 关键方法
- View.setFocusable
- View.setFocusableInTouchMode
- View.hasFocus
- View.hasFocusable
- View.setOnFocusChangeListener
- View.focusSearch
- View.findFocus
- View.requestFocus
- View.clearFocus
- View.addFocusables
- ViewGroup.findFocus
- ViewGroup.clearFocus
- ViewGroup.requestFocus
- ViewGroup.onRequestFocusInDescendants
- ViewGroup.getFocusedChild
- ViewGroup.focusSearch
触摸模式
Android中分为触摸模式1和选择模式
通常在手机、平板等触摸屏设备上默认都会处于触摸模式。
Android当中提供了View.isInTouchMode方法来判断当前的设备是否处于触摸模式。当我们使用adb命令向设备发送方向键事件时,设备就会进入选择模式
关键方法
View.setFocusable
设置focusable为true时将会给View添加对应的flag
public void setFocusable(boolean focusable) {
setFocusable(focusable ? FOCUSABLE : NOT_FOCUSABLE);
}
public void setFocusable(@Focusable int focusable) {
if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
setFlags(0, FOCUSABLE_IN_TOUCH_MODE);
}
setFlags(focusable, FOCUSABLE_MASK);
}
View.setFocusableInTouchMode
设置触摸模式可聚焦即可在触摸模式下可聚焦,有同学可能会发现recyclerView跟其他控件嵌套使用,有时会抢占焦点,就是因为设置了这个标记的缘故
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.hasFocus
如果此视图本身有焦点,或者它的子视图有焦点,则返回true
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
View.hasFocusable
如果此视图可聚焦,或者它的子视图可聚焦,则返回true,注意只有Visibility为visiable的才算
public boolean hasFocusable() {
return hasFocusable(!sHasFocusableExcludeAutoFocusable, false);
}
View.setOnFocusChangeListener
这个方法会在视图的焦点状态被更改时调用,这个方法有对应的get方法
public void setOnFocusChangeListener(OnFocusChangeListener l) {
getListenerInfo().mOnFocusChangeListener = l;
}
View.focusSearch
这个方法会调用ViewGroup的双参数方法,将自己作为起点,向特定的方向搜索下一个焦点,当然了,前提是这个视图被添加到一个ViewGroup中
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
View.findFocus
此视图本身是否已聚焦,是返回自己,否则返回null
public View findFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}
View.requestFocus
无参方法会调用到requestFocusNoSearch方法
因为View不需要去检查子视图是否需要获得焦点,所以满足条件后会直接获得焦点
public final boolean requestFocus() {
return requestFocus(View.FOCUS_DOWN);
}
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if (!canTakeFocus()) {
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
if (!isLayoutValid()) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
} else {
clearParentsWantFocus();
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
View.clearFocus
此方法会清除视图的焦点,注意此方法清除焦点后,如果系统处于选择模式,清除焦点后会丢失焦点,那么焦点会跑到左上角第一个可聚焦的视图,大屏tv开发时不建议调用此方法
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
}
final boolean refocus = sAlwaysAssignFocus || !isInTouchMode();
clearFocusInternal(null, true, refocus);
}
View.addFocusables
此方法会在FocusFinder查找焦点时调用,用于从ViewGroup中收集可聚焦的视图,如果是View就是收集View本身
public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
}
ViewGroup.findFocus
View的同名重载方法,如果此ViewGroup已经聚焦则返回自己,如果它的子视图已聚焦,则会调用子视图的同名方法
需要注意的是,这里的mFocused指的是当前ViewGroup的直接子视图,而返回值返回的是真正聚焦的视图
@Override
public View findFocus() {
if (DBG) {
System.out.println("Find focus in " + this + ": flags="
+ isFocused() + ", child=" + mFocused);
}
if (isFocused()) {
return this;
}
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}
ViewGroup.clearFocus
此ViewGroup会先清除自己或者子视图的焦点
@Override
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
}
if (mFocused == null) {
super.clearFocus();
} else {
View focused = mFocused;
mFocused = null;
focused.clearFocus();
}
}
ViewGroup.requestFocus
这里涉及到descendantFocusability这个关键方法
我们可以在xml中设置这个属性,值分别时
- blocksDescendants(阻止后代获取焦点,父视图直接获得焦点)
- beforeDescendants(在后代之前获得焦点,如果父视图能获得焦点,父视图会直接获得焦点,否则会调用onRequestFocusInDescendants把焦点分发给子视图)
- afterDescendants(在后代之后获得焦点,先调用onRequestFocusInDescendants把焦点分发给子视图,如果子视图不能获得焦点那么发给自己)
此方法会检查
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
boolean result;
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
result = super.requestFocus(direction, previouslyFocusedRect);
break;
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
result = took ? took : onRequestFocusInDescendants(direction,
previouslyFocusedRect);
break;
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
break;
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
}
return result;
}
ViewGroup.onRequestFocusInDescendants
当你对此ViewGroup进行requestFocus时根据你设置的descendantFocusability会看情况自动调用,
你可以重写此方法来定义你的ViewGroup寻找子视图的规则
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int index;
int increment;
int end;
int count = mChildrenCount;
if ((direction & FOCUS_FORWARD) != 0) {
index = 0;
increment = 1;
end = count;
} else {
index = count - 1;
increment = -1;
end = -1;
}
final View[] children = mChildren;
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
ViewGroup.getFocusedChild
直接返回已经聚焦的子视图,与findFocus不同的是findFocus会按照层级寻找已聚焦的子视图
public View getFocusedChild() {
return mFocused;
}
ViewGroup.focusSearch
这个方法会检查是否是根视图,是根视图就会调用FocusFinder查找焦点,否则就会请求父视图查找焦点,可以重写此方法更新搜索策略,例如recyclerView
@Override
public View focusSearch(View focused, int direction) {
if (isRootNamespace()) {
// root namespace means we should consider ourselves the top of the
// tree for focus searching; otherwise we could be focus searching
// into other tabs. see LocalActivityManager and TabHost for more info.
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
return mParent.focusSearch(focused, direction);
}
return null;
}
https://android-developers.googleblog.com/2008/12/touch-mode.html ↩︎