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

引言,有一天我在調(diào)試一個界面,xml布局里面包含Scroll View,里面嵌套了recyclerView的時候,界面一進(jìn)去,就自動滾動到了recyclerView的那部分,百思不得其解,上網(wǎng)查了好多資料,大部分只是提到了解決的辦法,但是對于為什么會這樣,都沒有一個很好的解釋,本著對技術(shù)的負(fù)責(zé)的態(tài)度,花費(fèi)了一點(diǎn)時間將前后理順了下

1.首先在包含ScrollView的xml布局中,我們在一加載進(jìn)來,ScrollView就自動滾動到獲取焦點(diǎn)的子view的位置,那我們就需要看下我們activity的onCreate中執(zhí)行了什么?

答:當(dāng)我們在activity的onCreate方法中調(diào)用setContentView(int layRes)的時候,我們會調(diào)用LayoutInflater的inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法,這里會找到xml的rootView,然后對rootView進(jìn)行rInflateChildren(parser, temp, attrs, true)加載xml的rootView下面的子View,如果是,其中會調(diào)用addView方法,我們看下addView方法:

public void addView(View child, int index, LayoutParams params) {
    ......
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

addView的方法內(nèi)部是調(diào)用了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的時候,是會調(diào)用requestChildFocus方法,在這里我們需要明白view的繪制原理,是view樹的層級繪制,是繪制樹的最頂端,也就是子view,然后父view的機(jī)制。明白這個的話,我們再繼續(xù)看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);的調(diào)用,這是Android中典型的也是24種設(shè)計(jì)模式的一種(責(zé)任鏈模式),會一直調(diào)用,就這樣,我們肯定會調(diào)用到ScrollView的requestChidlFocus方法,然后Android的ScrollView控件,重寫了requestChildFocus方法:

@Override
public void requestChildFocus(View child, View focused) {
    if (!mIsLayoutDirty) {
        scrollToChild(focused);
    } else {
        mChildToScrollTo = focused;
    }
    super.requestChildFocus(child, focused);
}

因?yàn)樵赼ddViewInner之前調(diào)用了requestLayout()方法:

@Override
public void requestLayout() {
    mIsLayoutDirty = true;
    super.requestLayout();
}

所以我們在執(zhí)行requestChildFocus的時候,會進(jìn)入else的判斷,mChildToScrollTo = focused。

2.接下來我們繼續(xù)分析下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)}成立的情況,這里會一直調(diào)用,直到調(diào)用到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,執(zhí)行performTraversals方法進(jìn)行view樹的重繪制。

3.那么ScrollView為什么會滑到獲取焦點(diǎn)的子view的位置了?

答:通過上面的分析,我們可以看到當(dāng)Scrollview中包含有焦點(diǎn)的view的時候,最終會執(zhí)行view樹的重繪制,所以會調(diào)用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進(jìn)行賦值了,所以這個時候,我們會進(jìn)入到if判斷的執(zhí)行,調(diào)用scrollToChild(mChildToScrollTo)方法:

private void scrollToChild(View child) {
    child.getDrawingRect(mTempRect);
    offsetDescendantRectToMyCoords(child, mTempRect);

    int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);

    if (scrollDelta != 0) {
        scrollBy(0, scrollDelta);
    }
}

很明顯,當(dāng)前的方法就是將ScrollView移動到獲取制定的view當(dāng)中,在這里我們可以明白了,為什么ScrollView會自動滑到獲取焦點(diǎn)的子view的位置了。

4.為什么在ScrollView的子viewGroup中增加android:descendantFocusability=”blocksDescendants”屬性能阻止ScrollView的自動滑動呢?

答:如第一步所說的,view的繪制原理:是view樹的層級繪制,是繪制樹的最頂端,也就是子view,然后父view繪制的機(jī)制,所以我們在ScrollView的直接子view設(shè)置android:descendantFocusability=”blocksDescendants”屬性的時候,這個時候直接return了,就不會再繼續(xù)執(zhí)行父view也就是ScrollView的requestChildFocus(View child, View focused)方法了,導(dǎo)致下面的自動滑動就不會觸發(fā)了。

    @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();
    }

當(dāng)你開心的設(shè)置android:descendantFocusability=”blocksDescendants”屬性以為解決問題了,但是殊不知人家ScrollView的代碼里面將這個descendantFocusability屬性又設(shè)置成了FOCUS_AFTER_DESCENDANTS,所以你在xml中增加是沒有任何作用的。

6.從上面我們分析了,ScrollView一加載就會滑動到獲取焦點(diǎn)的子view的位置了,也明白了增加android:descendantFocusability="blocksDescendants"屬性能阻止ScrollView會自動滾動到獲取焦點(diǎn)的子view的原因,但是為什么在獲取焦點(diǎn)的子view外面套一層view,然后增加focusableInTouchMode=true屬性也可以解決這樣的滑動呢?

答:我們注意到,調(diào)用addViewInner方法的時候,會先判斷view.hasFocus(),其中view.hasFocus()的判斷有兩個規(guī)則:1.是當(dāng)前的view在剛顯示的時候被展示出來了,hasFocus()才可能為true;2.同一級的view有多個focus的view的話,那么只是第一個view獲取焦點(diǎn)。
如果在布局中view標(biāo)簽增加focusableInTouchMode=true屬性的話,意味這當(dāng)我們在加載的時候,標(biāo)簽view的hasfocus就為true了,然而當(dāng)在獲取其中的子view的hasFocus方法的值的時候,他們就為false了。(這就意味著scrollview雖然會滑動,但是滑動到添加focusableInTouchMode=true屬性的view的位置,如果view的位置就是填充了scrollview的話,相當(dāng)于是沒有滑動的,這也就是為什么在外布局增加focusableInTouchMode=true屬性能阻止ScrollView會自動滾動到獲取焦點(diǎn)的子view的原因)所以在外部套一層focusableInTouchMode=true并不是嚴(yán)格意義上的說法,因?yàn)殡m然我們套了一層view,如果該view不是鋪滿的scrollview的話,很可能還是會出現(xiàn)自動滑動的。所以我們在套focusableInTouchMode=true屬性的情況,最好是在ScrollView的直接子view 上添加就可以了。

總結(jié)

通過上面的分析,其實(shí)我們可以得到多種解決ScrollView會自動滾動到獲取焦點(diǎn)的子view的方法,比如自定義重寫Scrollview的requestChildFocus方法,直接返回return,就能中斷Scrollview的自動滑動,本質(zhì)上都是中斷了ScrollView重寫的方法requestChildFocus的進(jìn)行,或者是讓Scrollview中鋪滿ScrollView的子view獲取到焦點(diǎn),這樣雖然滑動,但是滑動的距離只是為0罷了,相當(dāng)于沒有滑動罷了。**
同理我們也可以明白,如果是RecyclerView嵌套了RecyclerView,導(dǎo)致自動滑動的話,那么RecyclerView中也應(yīng)該重寫了requestChildFocus,進(jìn)行自動滑動的準(zhǔn)備。也希望大家通過閱讀源碼自己驗(yàn)證。

整理下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) {
    }
}

如果大家還有更好的解決方案,可以拿出來大家探討,要是文章有不對的地方,歡迎拍磚。

如果你們覺得文章對你有啟示作用,希望你們幫忙點(diǎn)個贊或者關(guān)注下,謝謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評論 25 708
  • 出現(xiàn)嵌套問題,主要是事件的分發(fā)機(jī)制及事件沖突,以下列舉各種沖突及相關(guān)的解決辦法。 一、ScrollView嵌套Li...
    黃海佳閱讀 1,203評論 6 11
  • 最近壞事連連,心情跌倒谷底。 上回丟了火車票,今天更是丟了身份證(下周就要出國,這個節(jié)骨眼上丟了證件,不由得惱火,...
    梅麗爾閱讀 227評論 0 0
  • 說真的,我們這一生里拒絕最多的人, 一種叫“父母”,一種叫“子女”。 大概是因?yàn)楸舜擞懈嗟臅r間在一起, 而更容易...
    娃咋養(yǎng)閱讀 698評論 0 0
  • 100天閱讀33本書之《簡單斷舍離生活》。 作者:山下英子 為什么喊了那么多“斷舍離”的口號,依然過不上極簡生活。...
    hollyzesta閱讀 489評論 0 1

友情鏈接更多精彩內(nèi)容