Android从源码分析ScrollView自动滑动的焦点问题以及解决方案
大家做项目开放应该都碰到过类似于这种界面
这时候我们做Fragment切换的时候,如果Fragment带有像ListView和RecyclerView之类的列表,在切换的时候ScrollView会自动滑动到列表的顶部。虽然做手机app开发的时候,焦点处理比较少,但是我们可以从源码来分析一下焦点是如何传递的。
源码分析
我们两个Fragment切换的时候,最终其实还是一个View隐藏,一个View显示而已。在两个页面都加载进去了之后,我们做切换的时候一个View会GONE,一个View会VISIBLE。GONE我们后面分析,其实就是会在有焦点的时候会清空焦点。设置VISIBLE之后 。
void setFlags(int flags, int mask) {
final boolean accessibilityEnabled =
AccessibilityManager.getInstance(mContext).isEnabled();
final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();
int old = mViewFlags;
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;
if (changed == 0) {
return;
}
int privateFlags = mPrivateFlags;
.....
final int newVisibility = flags & VISIBILITY_MASK;
if (newVisibility == VISIBLE) {
if ((changed & VISIBILITY_MASK) != 0) {
/*
* If this view is becoming visible, invalidate it in case it changed while
* it was not visible. Marking it drawn ensures that the invalidation will
* go through.
*/
mPrivateFlags |= PFLAG_DRAWN;
invalidate(true);
needGlobalAttributesUpdate(true);
// a view becoming visible is worth notifying the parent
// about in case nothing has focus. even if this specific view
// isn't focusable, it may contain something that is, so let
// the root view try to give this focus if nothing else does.
if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) {
mParent.focusableViewAvailable(this);
}
}
}
.....
}
从上面可以看到,我们会执行mParent.focusableViewAvailable(this);我们看看这个方法做了什么
public void focusableViewAvailable(View v) {
if (mParent != null
// shortcut: don't report a new focusable view if we block our descendants from
// getting focus
&& (getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS)
&& (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())
// shortcut: don't report a new focusable view if we already are focused
// (and we don't prefer our descendants)
//
// note: knowing that mFocused is non-null is not a good enough reason
// to break the traversal since in that case we'd actually have to find
// the focused view and make sure it wasn't FOCUS_AFTER_DESCENDANTS and
// an ancestor of v; this will get checked for at ViewAncestor
&& !(isFocused() && getDescendantFocusability() != FOCUS_AFTER_DESCENDANTS)) {
mParent.focusableViewAvailable(v);
}
}
这里会循环往父布局调用,我们的ViewGroup的默认都是FOCUS_BEFORE_DESCENDANTS,所以会循环调到ViewRootImpl。
@Override
public void focusableViewAvailable(View v) {
checkThread();
if (mView != null) {
if (!mView.hasFocus()) {
v.requestFocus();
} else {
// the one case where will transfer focus away from the current one
// is if the current view is a view group that prefers to give focus
// to its children first AND the view is a descendant of it.
View focused = mView.findFocus();
if (focused instanceof ViewGroup) {
ViewGroup group = (ViewGroup) focused;
if (group.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
&& isViewDescendantOf(v, focused)) {
v.requestFocus();
}
}
}
}
}
这个方法的参数就是我们Fragment的根布局View了。这里会调用v.requestFocus();经过几层嵌套会来到ViewGroup的这个方法。
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
}
和明显,会走 case FOCUS_BEFORE_DESCENDANTS。先分析 final boolean took = super.requestFocus(direction, previouslyFocusedRect)。这个是View的方法。
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if ((mViewFlags & FOCUSABLE) != FOCUSABLE
|| (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
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;
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
因为一般的父布局focusable一般都是false,所以第一个判断直接过不了,返回false。然后就到了
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
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;
}
上面这个方法就是循环调用子view的requestFocus了。这里需要注意因为RecyclerView是focusable=true的。所以对于RecyclerView的requestFocus会走这个方法。 handleFocusGainInternal(direction, previouslyFocusedRect);参考刚刚分析的。
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
mParent.requestChildFocus(this, this);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
上面这个方法做的事情也不是特别多,我们也只需要关注 这里mParent.requestChildFocus(this, this);ViewGroup中的代码。
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
看起来没有做什么,但是应该可以猜到ScrollView应该是重写过这个方法的,我们找到ScrollView的requestChildFocus方法。
@Override
public void requestChildFocus(View child, View focused) {
if (!mIsLayoutDirty) {
scrollToChild(focused);
} else {
// The child may not be laid out yet, we can't compute the scroll yet
mChildToScrollTo = focused;
}
super.requestChildFocus(child, focused);
}
很简单,如果我们请求了requestLayout,那么第一个if判断就不会过,走到else。setVisibility改变了我们肯定会重新绘制,我们直接看ScrollView的onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
...
}
这里我们终于找到了结果,是因为View的显示状态变化,导致焦点View会传到ScrollView中,重新测量的时候导致会自动滑动到焦点的View的位置。
解决方案
方案一
既然我们知道了原因,想要找到解决方案肯定也不难了。之前我google的时候搜索类似问题,有人提出在根布局加上
android:focusable="true"
android:focusableInTouchMode="true"
这个东西有没有用呢,有用,然而第一次的切换的时候会出问题,之后就没有问题了。这个到底是为什么呢?源码里也能找到答案。
当我们第一次从第二个Fragment切换会第一个时,走的逻辑和上面分析的类似,所以第一次还是会跳。然后再切换到第二个Fragment,这时候第一个Fragment的根View会设置GONE,这里会执行一个关键的方法clearFocus
void setFlags(int flags, int mask) {
...
/* Check if the GONE bit has changed */
if ((changed & GONE) != 0) {
needGlobalAttributesUpdate(false);
requestLayout();
if (((mViewFlags & VISIBILITY_MASK) == GONE)) {
if (hasFocus()) clearFocus();
clearAccessibilityFocus();
destroyDrawingCache();
if (mParent instanceof View) {
// GONE views noop invalidation, so invalidate the parent
((View) mParent).invalidate(true);
}
// Mark the view drawn to ensure that it gets invalidated properly the next
// time it is visible and gets invalidated
mPrivateFlags |= PFLAG_DRAWN;
}
if (mAttachInfo != null) {
mAttachInfo.mViewVisibilityChanged = true;
}
}
...
}
注意hasFocus并不代表当前的View有焦点,也可能是子View有焦点。ClearFocus会找到子View带有焦点的View,这里肯定是我们RecyclerView了。
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
}
onFocusChanged(false, 0, null);
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
清空了focuse的标志位后,会执行rootViewRequestFocus()。
boolean rootViewRequestFocus() {
final View root = getRootView();
return root != null && root.requestFocus();
}
那么这里就很明显了,调用DecorView的requestFocus(),而我们在根布局上设置的属性会导致焦点会设置到那个父View上,
这个时候focusableViewAvailable(View v) 同样无法向上传递,因为isFocused()为true。这样RecyclerView就找不到无法获取焦点,ScrollView也就不会自己滑动了。可是第一次切换的时候依然无效,基本上不考虑。
方案二
第二种解决方案就比较简单了,直接在根布局设置android:descendantFocusability=”blocksDescendants”,这样的话同样导致focusableViewAvailable(View v) 这个方法无法向上传递,这样RecyclerView就无法获取焦点,ScrollView同样也不会自动滑动了。
总结
- View的setVisibility()很可能会改变焦点的归属,对于某些控件需要考虑清楚使用。
- 很多情况下不需要考虑焦点直接上层拦截 android:descendantFocusability=”blocksDescendants”,父布局设置 android:focusable=”true”并不能保证拦截焦点。
- hasFocus()和isFocused()区别需要分清楚,一个是有焦点,但是可能焦点在子View,另一个就是当前的View是否有焦点了。
- 设置GONE的时候可能会clearFocus(),这时候焦点会从DecorView重新搜索获取。
上一篇: 老板一直在为我打工
下一篇: 胡皇后:历史上唯一当妓女的皇后