引言,有一天我在調試一個界面,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)方法:ide
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()方法:this
@Override public void requestLayout() { mIsLayoutDirty = true; super.requestLayout(); }
因此咱們在執行requestChildFocus的時候,會進入else的判斷,mChildToScrollTo = 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樹的重繪製。code
答:經過上面的分析,咱們能夠看到當Scrollview中包含有焦點的view的時候,最終會執行view樹的重繪製,因此會調用view的onLayout方法,咱們看下ScrollView的onLayout方法orm
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) { } }
若是你們還有更好的解決方案,能夠拿出來你們探討,要是文章有不對的地方,歡迎拍磚。
若是大家以爲文章對你有啓示做用,但願大家幫忙點個贊或者關注下,謝謝。