您好,登錄后才能下訂單哦!
引言,有一天我在調試一個界面,xml布局里面包含Scroll View,里面嵌套了recyclerView的時候,界面一進去,就自動滾動到了recyclerView的那部分,百思不得其解,上網查了好多資料,大部分只是提到了解決的辦法,但是對于為什么會這樣,都沒有一個很好的解釋,本著對技術的負責的態度,花費了一點時間將前后理順了下
答:當我們在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。
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樹的重繪制。
答:通過上面的分析,我們可以看到當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的位置了。
答:如第一步所說的,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) {
}
}
掘金首發如果覺得有用,請點個贊或者關注下
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。