ScrollView(RecyclerView等)为什么会自动滚动原理分析,还有阻止自动滑动的解决方...
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
引言,有一天我在調(diào)試一個(gè)界面,xml布局里面包含Scroll View,里面嵌套了recyclerView的時(shí)候,界面一進(jìn)去,就自動(dòng)滾動(dòng)到了recyclerView的那部分,百思不得其解,上網(wǎng)查了好多資料,大部分只是提到了解決的辦法,但是對于為什么會這樣,都沒有一個(gè)很好的解釋,本著對技術(shù)的負(fù)責(zé)的態(tài)度,花費(fèi)了一點(diǎn)時(shí)間將前后理順了下
1.首先在包含ScrollView的xml布局中,我們在一加載進(jìn)來,ScrollView就自動(dòng)滾動(dòng)到獲取焦點(diǎn)的子view的位置,那我們就需要看下我們activity的onCreate中執(zhí)行了什么?
答:當(dāng)我們在activity的onCreate方法中調(diào)用setContentView(int layRes)的時(shí)候,我們會調(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());}......}} }這里我們看到,我們在添加一個(gè)hasFocus的子view的時(shí)候,是會調(diào)用requestChildFocus方法,在這里我們需要明白view的繪制原理,是view樹的層級繪制,是繪制樹的最頂端,也就是子view,然后父view的機(jī)制。明白這個(gè)的話,我們再繼續(xù)看ViewGroup的requestChildFocus方法,
@Overridepublic void requestChildFocus(View child, View focused) {if (DBG) {System.out.println(this + " requestChildFocus()");}if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {return;}// Unfocus us, if necessarysuper.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的時(shí)候,會進(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 necessarysuper.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"屬性能解決自動(dòng)滑動(dòng)的原因),我們先來看看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()會啟動(dòng)一個(gè)runnable,執(zhí)行performTraversals方法進(jìn)行view樹的重繪制。
3.那么ScrollView為什么會滑到獲取焦點(diǎn)的子view的位置了?
答:通過上面的分析,我們可以看到當(dāng)Scrollview中包含有焦點(diǎn)的view的時(shí)候,最終會執(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)行賦值了,所以這個(gè)時(shí)候,我們會進(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會自動(dòng)滑到獲取焦點(diǎn)的子view的位置了。
4.為什么在ScrollView的子viewGroup中增加android:descendantFocusability=”blocksDescendants”屬性能阻止ScrollView的自動(dòng)滑動(dòng)呢?
答:如第一步所說的,view的繪制原理:是view樹的層級繪制,是繪制樹的最頂端,也就是子view,然后父view繪制的機(jī)制,所以我們在ScrollView的直接子view設(shè)置android:descendantFocusability=”blocksDescendants”屬性的時(shí)候,這個(gè)時(shí)候直接return了,就不會再繼續(xù)執(zhí)行父view也就是ScrollView的requestChildFocus(View child, View focused)方法了,導(dǎo)致下面的自動(dòng)滑動(dòng)就不會觸發(fā)了。
@Overridepublic void requestChildFocus(View child, View focused) {......if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {return;}......if (mParent != null) {mParent.requestChildFocus(this, focused);}}5.相信在這里有不少人有疑問了:如果是按照博主你的解釋,是不是在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)你開心的設(shè)置android:descendantFocusability=”blocksDescendants”屬性以為解決問題了,但是殊不知人家ScrollView的代碼里面將這個(gè)descendantFocusability屬性又設(shè)置成了FOCUS_AFTER_DESCENDANTS,所以你在xml中增加是沒有任何作用的。
6.從上面我們分析了,ScrollView一加載就會滑動(dòng)到獲取焦點(diǎn)的子view的位置了,也明白了增加android:descendantFocusability="blocksDescendants"屬性能阻止ScrollView會自動(dòng)滾動(dòng)到獲取焦點(diǎn)的子view的原因,但是為什么在獲取焦點(diǎn)的子view外面套一層view,然后增加focusableInTouchMode=true屬性也可以解決這樣的滑動(dòng)呢?
答:我們注意到,調(diào)用addViewInner方法的時(shí)候,會先判斷view.hasFocus(),其中view.hasFocus()的判斷有兩個(gè)規(guī)則:1.是當(dāng)前的view在剛顯示的時(shí)候被展示出來了,hasFocus()才可能為true;2.同一級的view有多個(gè)focus的view的話,那么只是第一個(gè)view獲取焦點(diǎn)。
如果在布局中view標(biāo)簽增加focusableInTouchMode=true屬性的話,意味這當(dāng)我們在加載的時(shí)候,標(biāo)簽view的hasfocus就為true了,然而當(dāng)在獲取其中的子view的hasFocus方法的值的時(shí)候,他們就為false了。(這就意味著scrollview雖然會滑動(dòng),但是滑動(dòng)到添加focusableInTouchMode=true屬性的view的位置,如果view的位置就是填充了scrollview的話,相當(dāng)于是沒有滑動(dòng)的,這也就是為什么在外布局增加focusableInTouchMode=true屬性能阻止ScrollView會自動(dòng)滾動(dòng)到獲取焦點(diǎn)的子view的原因)所以在外部套一層focusableInTouchMode=true并不是嚴(yán)格意義上的說法,因?yàn)殡m然我們套了一層view,如果該view不是鋪滿的scrollview的話,很可能還是會出現(xiàn)自動(dòng)滑動(dòng)的。所以我們在套focusableInTouchMode=true屬性的情況,最好是在ScrollView的直接子view 上添加就可以了。
總結(jié)
通過上面的分析,其實(shí)我們可以得到多種解決ScrollView會自動(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)于沒有滑動(dòng)罷了。**
同理我們也可以明白,如果是RecyclerView嵌套了RecyclerView,導(dǎo)致自動(dòng)滑動(dòng)的話,那么RecyclerView中也應(yīng)該重寫了requestChildFocus,進(jìn)行自動(dòng)滑動(dòng)的準(zhǔn)備。也希望大家通過閱讀源碼自己驗(yàn)證。
整理下3種方法:
第一種.
第二種.
<ScrollViewandroid:id="@+id/scrollView"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"><LinearLayoutandroid: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);}@Overridepublic void requestChildFocus(View child, View focused) {} }如果覺得有用,請點(diǎn)個(gè)贊唄
最后給大家分享一份非常系統(tǒng)和全面的Android進(jìn)階技術(shù)大綱及進(jìn)階資料,及面試題集
想學(xué)習(xí)更多Android知識,請加入Android技術(shù)開發(fā)交流 7520 16839
進(jìn)群與大牛們一起討論,還可獲取Android高級架構(gòu)資料、源碼、筆記、視頻
包括 高級UI、Gradle、RxJava、小程序、Hybrid、移動(dòng)架構(gòu)、React Native、性能優(yōu)化等全面的Android高級實(shí)踐技術(shù)講解性能優(yōu)化架構(gòu)思維導(dǎo)圖,和BATJ面試題及答案!
群里免費(fèi)分享給有需要的朋友,希望能夠幫助一些在這個(gè)行業(yè)發(fā)展迷茫的,或者想系統(tǒng)深入提升以及困于瓶頸的
朋友,在網(wǎng)上博客論壇等地方少花些時(shí)間找資料,把有限的時(shí)間,真正花在學(xué)習(xí)上,所以我在這免費(fèi)分享一些架構(gòu)資料及給大家。希望在這些資料中都有你需要的內(nèi)容。
轉(zhuǎn)載于:https://my.oschina.net/u/3956562/blog/3016270
總結(jié)
以上是生活随笔為你收集整理的ScrollView(RecyclerView等)为什么会自动滚动原理分析,还有阻止自动滑动的解决方...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JS 中的装饰器
- 下一篇: 上线清单 —— 20 个 Laravel