ScrollView(RecyclerView等)為什麼會自動滾動原理分析,還有阻止自動滑動的解決方

口壤迅坎發表於2020-09-22

  引言,有一天我在除錯一個介面,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) {

  }

  }

  掘金首發如果覺得有用,請點個贊或者關注下


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69920894/viewspace-2723130/,如需轉載,請註明出處,否則將追究法律責任。

相關文章