引言,有一天我在调试一个界面,xml布局里面包含Scroll View,里面嵌套了recyclerView的时候,界面一进去,就自动滚动到了recyclerView的那部分,百思不得其解,上网查了好多资料,大部分只是提到了解决的办法,可是对于为何会这样,都没有一个很好的解释,本着对技术的负责的态度,花费了一点时间将先后理顺了下android
答:当咱们在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)方法:bash
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方法,ide
@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。ui
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方法this
@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树的重绘制。spa
答:经过上面的分析,咱们能够看到当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的位置了。
答:如第一步所说的,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);
}
}
复制代码
答:按照前面的分析的话,彷佛是能够的,可是翻看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中增长是没有任何做用的。
答:咱们注意到,调用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) {
}
}
复制代码
若是你们还有更好的解决方案,能够拿出来你们探讨,要是文章有不对的地方,欢迎拍砖。
若是大家以为文章对你有启示做用,但愿大家帮忙点个赞或者关注下,谢谢