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

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

1.首先在包含ScrollView的xml布局中,我們?cè)谝患虞d進(jìn)來(lái),ScrollView就自動(dòng)滾動(dòng)到獲取焦點(diǎn)的子view的位置般堆,那我們就需要看下我們activity的onCreate中執(zhí)行了什么在孝?

答:當(dāng)我們?cè)赼ctivity的onCreate方法中調(diào)用setContentView(int layRes)的時(shí)候,我們會(huì)調(diào)用LayoutInflater的inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法淮摔,這里會(huì)找到xml的rootView浑玛,然后對(duì)rootView進(jìn)行rInflateChildren(parser, temp, attrs, true)加載xml的rootView下面的子View,如果是噩咪,其中會(huì)調(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());
    }
    ......
    }
   }
}

這里我們看到,我們?cè)谔砑右粋€(gè)hasFocus的子view的時(shí)候胃碾,是會(huì)調(diào)用requestChildFocus方法涨享,在這里我們需要明白view的繪制原理,是view樹(shù)的層級(jí)繪制仆百,是繪制樹(shù)的最頂端厕隧,也就是子view,然后父view的機(jī)制俄周。明白這個(gè)的話吁讨,我們?cè)倮^續(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);
        }
    }

在上面會(huì)看到 mParent.requestChildFocus(this, focused);的調(diào)用峦朗,這是Android中典型的也是24種設(shè)計(jì)模式的一種(責(zé)任鏈模式)建丧,會(huì)一直調(diào)用,就這樣波势,我們肯定會(huì)調(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();
}

所以我們?cè)趫?zhí)行requestChildFocus的時(shí)候尺铣,會(huì)進(jìn)入else的判斷拴曲,mChildToScrollTo = focused。

2.接下來(lái)我們繼續(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);
    }
}
}

首先澈灼,我們會(huì)判斷ViewGroup的descendantFocusability屬性,如果是FOCUS_BLOCK_DESCENDANTS值的話店溢,直接就返回了(這部分后面會(huì)解釋叁熔,也是android:descendantFocusability="blocksDescendants"屬性能解決自動(dòng)滑動(dòng)的原因),我們先來(lái)看看if (mParent != null)mParent.requestChildFocus(this, focused)}成立的情況逞怨,這里會(huì)一直調(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()會(huì)啟動(dòng)一個(gè)runnable福澡,執(zhí)行performTraversals方法進(jìn)行view樹(shù)的重繪制叠赦。

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

答:通過(guò)上面的分析,我們可以看到當(dāng)Scrollview中包含有焦點(diǎn)的view的時(shí)候除秀,最終會(huì)執(zhí)行view樹(shù)的重繪制糯累,所以會(huì)調(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;
    ......
}
}

從第一步我們可以看到册踩,我們?cè)趓equestChildFocus方法中泳姐,是對(duì)mChildToScrollTo進(jìn)行賦值了,所以這個(gè)時(shí)候暂吉,我們會(huì)進(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移動(dòng)到獲取制定的view當(dāng)中慕的,在這里我們可以明白了阎肝,為什么ScrollView會(huì)自動(dòng)滑到獲取焦點(diǎn)的子view的位置了。

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

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

    @Override
    public void requestChildFocus(View child, View focused) {
        ......
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }

        ......
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }

5.相信在這里有不少人有疑問(wèn)了:如果是按照博主你的解釋仪际,是不是在ScrollView上面加android:descendantFocusability=”blocksDescendants”屬性也能阻止自動(dòng)滑動(dòng)呢朦蕴?

答:按照前面的分析的話,似乎是可以的弟头,但是翻看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)你開(kāi)心的設(shè)置android:descendantFocusability=”blocksDescendants”屬性以為解決問(wèn)題了,但是殊不知人家ScrollView的代碼里面將這個(gè)descendantFocusability屬性又設(shè)置成了FOCUS_AFTER_DESCENDANTS赴恨,所以你在xml中增加是沒(méi)有任何作用的疹娶。

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

答:我們注意到额港,調(diào)用addViewInner方法的時(shí)候,會(huì)先判斷view.hasFocus()歧焦,其中view.hasFocus()的判斷有兩個(gè)規(guī)則:1.是當(dāng)前的view在剛顯示的時(shí)候被展示出來(lái)了移斩,hasFocus()才可能為true;2.同一級(jí)的view有多個(gè)focus的view的話,那么只是第一個(gè)view獲取焦點(diǎn)向瓷。
如果在布局中view標(biāo)簽增加focusableInTouchMode=true屬性的話肠套,意味這當(dāng)我們?cè)诩虞d的時(shí)候,標(biāo)簽view的hasfocus就為true了猖任,然而當(dāng)在獲取其中的子view的hasFocus方法的值的時(shí)候你稚,他們就為false了。(這就意味著scrollview雖然會(huì)滑動(dòng)朱躺,但是滑動(dòng)到添加focusableInTouchMode=true屬性的view的位置刁赖,如果view的位置就是填充了scrollview的話,相當(dāng)于是沒(méi)有滑動(dòng)的长搀,這也就是為什么在外布局增加focusableInTouchMode=true屬性能阻止ScrollView會(huì)自動(dòng)滾動(dòng)到獲取焦點(diǎn)的子view的原因)所以在外部套一層focusableInTouchMode=true并不是嚴(yán)格意義上的說(shuō)法乾闰,因?yàn)殡m然我們套了一層view,如果該view不是鋪滿的scrollview的話盈滴,很可能還是會(huì)出現(xiàn)自動(dòng)滑動(dòng)的涯肩。所以我們?cè)谔譮ocusableInTouchMode=true屬性的情況,最好是在ScrollView的直接子view 上添加就可以了巢钓。

總結(jié)

通過(guò)上面的分析病苗,其實(shí)我們可以得到多種解決ScrollView會(huì)自動(dòng)滾動(dòng)到獲取焦點(diǎn)的子view的方法,比如自定義重寫Scrollview的requestChildFocus方法症汹,直接返回return硫朦,就能中斷Scrollview的自動(dòng)滑動(dòng),本質(zhì)上都是中斷了ScrollView重寫的方法requestChildFocus的進(jìn)行背镇,或者是讓Scrollview中鋪滿ScrollView的子view獲取到焦點(diǎn)咬展,這樣雖然滑動(dòng),但是滑動(dòng)的距離只是為0罷了瞒斩,相當(dāng)于沒(méi)有滑動(dòng)罷了破婆。**
同理我們也可以明白,如果是RecyclerView嵌套了RecyclerView胸囱,導(dǎo)致自動(dòng)滑動(dòng)的話祷舀,那么RecyclerView中也應(yīng)該重寫了requestChildFocus,進(jìn)行自動(dòng)滑動(dòng)的準(zhǔn)備烹笔。也希望大家通過(guò)閱讀源碼自己驗(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) {
    }
}

如果大家還有更好的解決方案,可以拿出來(lái)大家探討谤职,要是文章有不對(duì)的地方饰豺,歡迎拍磚。

如果你們覺(jué)得文章對(duì)你有啟示作用允蜈,希望你們幫忙點(diǎn)個(gè)贊或者關(guān)注下冤吨,謝謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒿柳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子锅很,更是在濱河造成了極大的恐慌其馏,老刑警劉巖凤跑,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爆安,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡仔引,警方通過(guò)查閱死者的電腦和手機(jī)扔仓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咖耘,“玉大人翘簇,你說(shuō)我怎么就攤上這事《梗” “怎么了版保?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)夫否。 經(jīng)常有香客問(wèn)我彻犁,道長(zhǎng),這世上最難降的妖魔是什么凰慈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任汞幢,我火速辦了婚禮,結(jié)果婚禮上微谓,老公的妹妹穿的比我還像新娘森篷。我一直安慰自己,他們只是感情好豺型,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布仲智。 她就那樣靜靜地躺著,像睡著了一般姻氨。 火紅的嫁衣襯著肌膚如雪坎藐。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天哼绑,我揣著相機(jī)與錄音岩馍,去河邊找鬼。 笑死抖韩,一個(gè)胖子當(dāng)著我的面吹牛蛀恩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茂浮,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼双谆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼壳咕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起顽馋,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤谓厘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后寸谜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體竟稳,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年熊痴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了他爸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡果善,死狀恐怖诊笤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情巾陕,我是刑警寧澤讨跟,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站鄙煤,受9級(jí)特大地震影響晾匠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜馆类,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一混聊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乾巧,春花似錦句喜、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至旷太,卻和暖如春展懈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背供璧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工存崖, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人睡毒。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓来惧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親演顾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子供搀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,104評(píng)論 25 707
  • 出現(xiàn)嵌套問(wèn)題隅居,主要是事件的分發(fā)機(jī)制及事件沖突,以下列舉各種沖突及相關(guān)的解決辦法葛虐。 一胎源、ScrollView嵌套Li...
    黃海佳閱讀 1,141評(píng)論 6 11
  • 最近壞事連連,心情跌倒谷底屿脐。 上回丟了火車票涕蚤,今天更是丟了身份證(下周就要出國(guó),這個(gè)節(jié)骨眼上丟了證件摄悯,不由得惱火赞季,...
    梅麗爾閱讀 187評(píng)論 0 0
  • 說(shuō)真的,我們這一生里拒絕最多的人次绘, 一種叫“父母”瘪阁,一種叫“子女”。 大概是因?yàn)楸舜擞懈嗟臅r(shí)間在一起邮偎, 而更容易...
    娃咋養(yǎng)閱讀 530評(píng)論 0 0
  • 100天閱讀33本書(shū)之《簡(jiǎn)單斷舍離生活》管跺。 作者:山下英子 為什么喊了那么多“斷舍離”的口號(hào),依然過(guò)不上極簡(jiǎn)生活禾进。...
    hollyzesta閱讀 387評(píng)論 0 1