引言, 有一天我在调试一个界面, xml 布局里面包含 Scroll View, 里面嵌套了 recyclerView 的时候, 界面一进去, 就自动滚动到了 recyclerView 的那部分, 百思不得其解, 上网查了好多资料, 大部分只是提到了解决的办法, 但是对于为什么会这样, 都没有一个很好的解释, 本着对技术的负责的态度, 花费了一点时间将前后理顺了下
1. 首先在包含 ScrollView 的 xml 布局中, 我们在一加载进来, ScrollView 就自动滚动到获取焦点的子 view 的位置, 那我们就需要看下我们 activity 的 onCreate 中执行了什么?
答: 当我们在 activity 的 onCreate 方法中调用 setContentView(int layRes) 的时候, 我们会调用 LayoutInflater 的 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 方法, 这里会找到 xml 的 rootView, 然后对 rootView 进行 rInflateChildren(parser, temp, attrs, true) 加载 xml 的 rootView 下面的子 View, 如果是, 其中会调用 addView 方法, 我们看下 addView 方法:
public void addView(View child, int index, LayoutParams params) {
......
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
addView 的方法内部是调用了 ViewGroup 的 addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) 方法:
android.view.ViewGroup{
......
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
......
if (child.hasFocus()) {
requestChildFocus(child, child.findFocus());
}
......
}
}
}
这里我们看到, 我们在添加一个 hasFocus 的子 view 的时候, 是会调用 requestChildFocus 方法, 在这里我们需要明白 view 的绘制原理, 是 view 树的层级绘制, 是绘制树的最顶端, 也就是子 view, 然后父 view 的机制. 明白这个的话, 我们再继续看 ViewGroup 的 requestChildFocus 方法,
@Override 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);
}
}
在上面会看到 mParent.requestChildFocus(this, focused); 的调用, 这是 Android 中典型的也是 24 种设计模式的一种 (责任链模式), 会一直调用, 就这样, 我们肯定会调用到 ScrollView 的 requestChidlFocus 方法, 然后 Android 的 ScrollView 控件, 重写了 requestChildFocus 方法:
@Override public void requestChildFocus(View child, View focused) {
if (!mIsLayoutDirty) {
scrollToChild(focused);
} else {
mChildToScrollTo = focused;
}
super.requestChildFocus(child, focused);
}
因为在 addViewInner 之前调用了 requestLayout() 方法:
@Override
public void requestLayout() {
mIsLayoutDirty = true;
super.requestLayout();
}
所以我们在执行 requestChildFocus 的时候, 会进入 else 的判断, mChildToScrollTo = focused.
2. 接下来我们继续分析下 mParent.requestChildFocus(this, focused) 方法?
android.view.ViewGroup {@Override 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);
}
}
}
首先, 我们会判断 ViewGroup 的 descendantFocusability 属性, 如果是 FOCUS_BLOCK_DESCENDANTS 值的话, 直接就返回了 (这部分后面会解释, 也是 android:descendantFocusability="blocksDescendants" 属性能解决自动滑动的原因), 我们先来看看 if (mParent != null)mParent.requestChildFocus(this, focused)} 成立的情况, 这里会一直调用, 直到调用到 ViewRootImpl 的 requestChildFocus 方法
@Override public void requestChildFocus(View child, View focused) {
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "Request child focus: focus now" + focused);
}
checkThread();
scheduleTraversals();
}
scheduleTraversals() 会启动一个 runnable, 执行 performTraversals 方法进行 view 树的重绘制.
3. 那么 ScrollView 为什么会滑到获取焦点的子 view 的位置了?
答: 通过上面的分析, 我们可以看到当 Scrollview 中包含有焦点的 view 的时候, 最终会执行 view 树的重绘制, 所以会调用 view 的 onLayout 方法, 我们看下 ScrollView 的 onLayout 方法
android.view.ScrollView{
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
......
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
......
}
}
从第一步我们可以看到, 我们在 requestChildFocus 方法中, 是对 mChildToScrollTo 进行赋值了, 所以这个时候, 我们会进入到 if 判断的执行, 调用 scrollToChild(mChildToScrollTo) 方法:
private void scrollToChild(View child) {
child.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(child, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
if (scrollDelta != 0) {
scrollBy(0, scrollDelta);
}
}
很明显, 当前的方法就是将 ScrollView 移动到获取制定的 view 当中, 在这里我们可以明白了, 为什么 ScrollView 会自动滑到获取焦点的子 view 的位置了.
4. 为什么在 ScrollView 的子 viewGroup 中增加 android:descendantFocusability="blocksDescendants" 属性能阻止 ScrollView 的自动滑动呢?
答: 如第一步所说的, view 的绘制原理: 是 view 树的层级绘制, 是绘制树的最顶端, 也就是子 view, 然后父 view 绘制的机制, 所以我们在 ScrollView 的直接子 view 设置 android:descendantFocusability="blocksDescendants" 属性的时候, 这个时候直接 return 了, 就不会再继续执行父 view 也就是 ScrollView 的 requestChildFocus(View child, View focused) 方法了, 导致下面的自动滑动就不会触发了.
@Override
public void requestChildFocus(View child, View focused) {
......
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
......
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
5. 相信在这里有不少人有疑问了: 如果是按照博主你的解释, 是不是在 ScrollView 上面加 android:descendantFocusability="blocksDescendants" 属性也能阻止自动滑动呢?
答: 按照前面的分析的话, 似乎是可以的, 但是翻看 ScrollView 的源码, 我们可以看到
private void initScrollView() {
mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mOverscrollDistance = configuration.getScaledOverscrollDistance();
mOverflingDistance = configuration.getScaledOverflingDistance();
}
当你开心的设置 android:descendantFocusability="blocksDescendants" 属性以为解决问题了, 但是殊不知人家 ScrollView 的代码里面将这个 descendantFocusability 属性又设置成了 FOCUS_AFTER_DESCENDANTS, 所以你在 xml 中增加是没有任何作用的.
6. 从上面我们分析了, ScrollView 一加载就会滑动到获取焦点的子 view 的位置了, 也明白了增加 android:descendantFocusability="blocksDescendants" 属性能阻止 ScrollView 会自动滚动到获取焦点的子 view 的原因, 但是为什么在获取焦点的子 view 外面套一层 view, 然后增加 focusableInTouchMode=true 属性也可以解决这样的滑动呢?
答: 我们注意到, 调用 addViewInner 方法的时候, 会先判断 view.hasFocus(), 其中 view.hasFocus() 的判断有两个规则: 1. 是当前的 view 在刚显示的时候被展示出来了, hasFocus() 才可能为 true;2. 同一级的 view 有多个 focus 的 view 的话, 那么只是第一个 view 获取焦点.
如果在布局中 view 标签增加 focusableInTouchMode=true 属性的话, 意味这当我们在加载的时候, 标签 view 的 hasfocus 就为 true 了, 然而当在获取其中的子 view 的 hasFocus 方法的值的时候, 他们就为 false 了.(这就意味着 scrollview 虽然会滑动, 但是滑动到添加 focusableInTouchMode=true 属性的 view 的位置, 如果 view 的位置就是填充了 scrollview 的话, 相当于是没有滑动的, 这也就是为什么在外布局增加 focusableInTouchMode=true 属性能阻止 ScrollView 会自动滚动到获取焦点的子 view 的原因) 所以在外部套一层 focusableInTouchMode=true 并不是严格意义上的说法, 因为虽然我们套了一层 view, 如果该 view 不是铺满的 scrollview 的话, 很可能还是会出现自动滑动的. 所以我们在套 focusableInTouchMode=true 属性的情况, 最好是在 ScrollView 的直接子 view 上添加就可以了.
总结
通过上面的分析, 其实我们可以得到多种解决 ScrollView 会自动滚动到获取焦点的子 view 的方法, 比如自定义重写 Scrollview 的 requestChildFocus 方法, 直接返回 return, 就能中断 Scrollview 的自动滑动, 本质上都是中断了 ScrollView 重写的方法 requestChildFocus 的进行, 或者是让 Scrollview 中铺满 ScrollView 的子 view 获取到焦点, 这样虽然滑动, 但是滑动的距离只是为 0 罢了, 相当于没有滑动罢了.**
同理我们也可以明白, 如果是 RecyclerView 嵌套了 RecyclerView, 导致自动滑动的话, 那么 RecyclerView 中也应该重写了 requestChildFocus, 进行自动滑动的准备. 也希望大家通过阅读源码自己验证.
整理下 3 种方法:
第一种.
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusableInTouchMode="true"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
第二种.
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
第三种.
public class StopAutoScrollView extends ScrollView {
public StopAutoScrollView(Context context) {
super(context);
}
public StopAutoScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StopAutoScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void requestChildFocus(View child, View focused) {
}
}
掘金首发 如果觉得有用, 请点个赞或者关注下
来源: https://www.cnblogs.com/WellJohn/p/8391253.html