Android从源码分析ScrollView自动滑动的焦点问题以及解决方案

大家做项目开放应该都碰到过类似于这种界面
《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同样也不会自动滑动了。

总结

  1. View的setVisibility()很可能会改变焦点的归属,对于某些控件需要考虑清楚使用。
  2. 很多情况下不需要考虑焦点直接上层拦截 android:descendantFocusability=”blocksDescendants”,父布局设置 android:focusable=”true”并不能保证拦截焦点。
  3. hasFocus()和isFocused()区别需要分清楚,一个是有焦点,但是可能焦点在子View,另一个就是当前的View是否有焦点了。
  4. 设置GONE的时候可能会clearFocus(),这时候焦点会从DecorView重新搜索获取。
    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/a568478312/article/details/80771453
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞