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
你也能够访问咱们的博客找到咱们,感谢阅读~