VerticalNestedScrollLayout實現了垂直嵌套滾動的通用組件。其內部有且僅有兩個直接子View: 頭部和主體。java
兩個子View通常寫在佈局中,以下:VerticalNestedScrollLayout有兩個直接子View,NestedScrollViewh 和 FrameLayout。android
<com.kaola.base.ui.scroll.VerticalNestedScrollLayout xmlns:vnsl="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" vnsl:isScrollDownWhenFirstItemIsTop="true" >
<android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="wrap_content" >
⋯⋯
</android.support.v4.widget.NestedScrollView>
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" >
⋯⋯
<android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" android:overScrollMode="never" />
⋯⋯
</FrameLayout>
</com.kaola.base.ui.scroll.VerticalNestedScrollLayout>
複製代碼
VerticalNestedScrollLayout做爲嵌套滾動的父組件,須要配合支持嵌套滾動的子View組件進行。git
public interface OnScrollYListener {
void onScrolling(int scrollY, boolean isTop);
void onScrollToTop();
void onScrollToBottom();
}
複製代碼
mRecyclerView.setNestedScrollingEnabled(false);
github
VerticalNestedScrollLayout是繼承LinearLayout實現NestedScrollingParent的父嵌套滾動組件,在initFromAttributes方法裏設置其方向爲垂直,而且獲取佈局中的屬性。三個屬性也能夠經過set⋯⋯方法進行設置bash
private void initFromAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
setOrientation(LinearLayout.VERTICAL);
mParentHelper = new NestedScrollingParentHelper(this);
TypedArray a = context.obtainStyledAttributes(attrs, com.kaola.base.R.styleable.VerticalNestedScrollLayout,
defStyleAttr, 0);
mIsScrollDownWhenFirstItemIsTop =
a.getBoolean(R.styleable.VerticalNestedScrollLayout_isScrollDownWhenFirstItemIsTop, false);
mIsAutoScroll = a.getBoolean(R.styleable.VerticalNestedScrollLayout_isAutoScroll, false);
mHeaderRetainHeight = (int) a.getDimension(R.styleable.VerticalNestedScrollLayout_headerRetainHeight, 0);
a.recycle();
}
複製代碼
經過onFinishInflate方法獲取頭部(mHeaderView)和主體(mBodyView)ide
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mHeaderView = getChildAt(0);
mBodyView = getChildAt(1);
}
複製代碼
而且在addView方法中限制了添加View。佈局
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (getChildCount() > 1) {
throw new IllegalStateException("VerticalNestedScrollLayout can host only two direct child");
}
super.addView(child, index, params);
}
複製代碼
而後是比較重要的測量方法,主要有如下幾步:ui
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//若是不設置無限制高度,mHeaderView高度若是大於屏幕的高,將只會顯示屏幕的高
mHeaderView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
//最大滾動距離:頭部減去保留的高度
mMaxScrollHeight = mHeaderView.getMeasuredHeight() - mHeaderRetainHeight;
//設置主體的高度:代碼中設置match_parent
if (mBodyView.getLayoutParams().height < getMeasuredHeight() - mHeaderRetainHeight) {
mBodyView.getLayoutParams().height = getMeasuredHeight() - mHeaderRetainHeight;
}
//設置自身的高度
setMeasuredDimension(getMeasuredWidth(), mBodyView.getLayoutParams().height + mHeaderView.getMeasuredHeight());
}
複製代碼
紅框表示屏幕,測量後VerticalNestedScrollLayout的高度其實是變高了,若是沒測量就進行嵌套滾動,往上滑動時,底部會出現空白區域this
下面就是NestedScrollingParent接口中方法的實現了,重點介紹onNestedPreScroll 和 onNestedPreFling方法。spa
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (canScroll(target, dy)) {
scrollBy(0, dy);
consumed[1] = dy;
⋯⋯
}
⋯⋯
}
複製代碼
該方法是子View開始滾動以前,調用的,就是子View滾動前讓父View先滾,這裏須要判斷父View是否要滾動。代碼中 hiddenTop是隱藏頭部的行爲、showTop是展現頭部的行爲,知足其中一個,就須要滾動父View。 代碼以下:
private boolean canScroll(View target, int dy) {
boolean hiddenTop = dy > 0 && getScrollY() < mMaxScrollHeight;
boolean showTop = dy < 0 && getScrollY() > 0;
if (mIsScrollDownWhenFirstItemIsTop) {
showTop = showTop && !target.canScrollVertically(-1);
}
return hiddenTop || showTop;
}
複製代碼
若是執行consumed[1] = dy;說明父View消費了全部的垂直滑動距離,若是consumed[1] = dy * 0.5f;則父View消費一半,這樣用戶看到的就是頭部和主體部分同時滾動的視覺效果。
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
if (mIsScrollDownWhenFirstItemIsTop && target.canScrollVertically(-1)) {
return false;
}
if (mScrollAnimator != null && mScrollAnimator.isStarted()) {
mScrollAnimator.cancel();
}
mIsFling = true;
if (velocityX == 0 && velocityY != 0) {
if (velocityY < 0) {
autoDownScroll();
} else {
autoUpScroll();
}
if (mIsScrollDownWhenFirstItemIsTop) {
return true;
}
}
return false;
}
複製代碼
上面是Fling時的處理邏輯,主要實現了自動滾動,若是沒有這段,則頭部看起來沒有慣性,用戶體檢較差。
方法中canScrollVertically(-1)判斷了target是否能夠往下拉。好比RecyclerView沒有置頂,還能夠往下拉,mRecyclerView.canScrollVertically(-1)返回true
而後經過velocityY判斷是自動滾到頂部仍是底部;返回true表示父View消費了Fling事件,false則不消費。
嵌套滾動中的兩個接口,在上文中已經提到。NestedScrollingParent和NestedScrollingChild 接口中的方法以下:
NestedScrollingChild
NestedScrollingParent
子view接受到滾動事件後發起嵌套滾動,詢問父View是否要先滾動,父View處理了本身的滾動需求後,回到子View處理本身的滾動需求,假如父View消耗了一些滾動距離,子View只能獲取剩下的滾動距離作處理。子View處理了本身的滾動需求後又回到父View,剩下的滾動距離作處理。慣性fling的相似。
將上面過程用源碼來解釋(子View爲RecyclerView,父View爲繼承了NestedScrollingParent的視圖)大致以下:
NestedScrollingChild 的 startNestedScroll是嵌套滾動的發起,查看RecyclerView中該方法的調用地方,在onInterceptTouchEvent和onTouchEvent的action ==MotionEvent.ACTION_DOWN時,忽略onInterceptTouchEvent,直接看onTouchEvent。
查看RecyclerView的startNestedScroll,發現是調了NestedScrollingChildHelper裏的startNestedScroll方法,查看startNestedScroll,發現有個遍歷的過程,找到onStartNestedScroll返回true的父View,再執行onNestedScrollAccepted後中止遍歷。到目前嵌套滾動執行的方法順序以下:
(子)startNestedScroll → (父)onStartNestedScroll → (父)onNestedScrollAccepted
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
複製代碼
接下來在RecyclerView的onTouchEvent的 MotionEvent.ACTION_MOVE裏調用了dispatchNestedPreScroll和scrollByInternal
case MotionEvent.ACTION_MOVE: {
⋯⋯
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
⋯⋯
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
⋯⋯
}
} break;
複製代碼
看dispatchNestedPreScroll源碼:發現調了父View的onNestedPreScroll,而且傳入dy 和 consumed。用於作消費計數。
onNestedPreScroll事件在不一樣父View中有不一樣實現,具體能夠看一下VerticalNestedScrollLayout裏該方法的實現
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
⋯⋯
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
⋯⋯
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
複製代碼
scrollByInternal讓RecyclerView本身滾動後又調用了dispatchNestedScroll
boolean scrollByInternal(int x, int y, MotionEvent ev) {
⋯⋯
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
⋯⋯
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}⋯⋯
return consumedX != 0 || consumedY != 0;
}
複製代碼
看dispatchNestedScroll方法,最終調用了父View的onNestedScroll方法。
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
⋯⋯
ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
複製代碼
到目前咱們也能夠看到父View的嵌套滾動方法都是子View調起來的,子View的接口都在TouchEvent事件裏。嵌套滾動執行的方法順序以下:
(子)startNestedScroll → (父)onStartNestedScroll → (父)onNestedScrollAccepted→ (子)dispatchNestedPreScroll → (父)onNestedPreScroll→ (子)dispatchNestedScroll→ (父)onNestedScroll
後面的MotionEvent.ACTION_UP中:
調用fling方法執行了嵌套滾動相關的fling事件 resetTouch();執行了stopNestedScroll事件
過程相似不在贅述。 嵌套滾動執行的方法順序以下:
(子)startNestedScroll → (父)onStartNestedScroll → (父)onNestedScrollAccepted→ (子)dispatchNestedPreScroll → (父)onNestedPreScroll→ (子)dispatchNestedScroll→ (父)onNestedScroll→ (子)dispatchNestedPreFling → (父)onNestedPreFling→ (子)dispatchNestedFling → (父)stopNestedScroll
從LOLLIPOP(SDK21)開始,嵌套滑動的相關邏輯做爲普通方法直接寫進了View和ViewGroup類裏。而SDK21以前的版本 官方在android.support.v4兼容包中提供了兩個接口NestedScrollingChild和NestedScrollingParent, 還有兩個輔助類NestedScrollingChildHelper和NestedScrollingParentHelper來幫助控件實現嵌套滑動。
兼容的原理
兩個接口NestedScrollingChild和NestedScrollingParent分別定義上面提到的View和ViewParent新增的普通方法
在嵌套滑動中會要求控件要麼是繼承於SDK21以後的View或ViewGroup, 要麼實現了這兩個接口, 這是控件可以進行嵌套滑動的前提條件。
那麼怎麼知道調用的方法是控件自有的方法, 仍是接口的方法? 在代碼中是經過ViewCompat和ViewParentCompat類來實現.
ViewCompat和ViewParentCompat經過當前的Build.VERSION.SDK_INT來判斷當前版本, 而後選擇不一樣的實現類, 這樣就能夠根據版本選擇調用的方法.
例如若是版本是SDK21以前, 那麼就會判斷控件是否實現了接口, 而後調用接口的方法, 若是是SDK21以後, 那麼就能夠直接調用對應的方法。
參考:https://www.jianshu.com/p/1806ed9737f6
你也能夠訪問咱們的博客找到咱們,感謝閱讀~