一、关于NestedScrolling
NestedScrolling机制主要是能够让父View和子View在滚动时互相协调配合。其中有两个重要的类,分别是:
接口类
NestedScrollingParent(最新:NestedScrollingParent2) - 代表类:NestedScrollView
NestedScrollingChild(最新:NestedScrollingChild2) - 代表类:RecyclerView
帮助类
NestedScrollingChildHelper
NestedScrollingParentHelper(用处不是很大)
父类继承NestedScrollingParent接口,而子类继承NestedScrollingChild接口,同时让父类包含子类,而不是自接父子关系,就搭起了NestedScrollingParent机制的基本骨架。
其主要流程是:
- 子类滑动,把滑动产生的事件和参数传给父类
- 父类根据子类传过来的参数偏移量、滑动方向等参数、当前滑动View等判断是否关注此事件,如果不关注,那么父View不会参与子View的滑动
- 父View关注了子View的滑动,子View通过PreScroll/PreFling会优先让父view消费事件,其实本质是子View的回调
- 接着,子View消费“剩余事件“(父View不一定消费掉所有事件,比如某时刻的偏移数据)
- 流程终止,子view通知父view本次滑动任务完成
public interface NestedScrollingChild {
/**
* 设置嵌套滑动是否能用
*/
@Override
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判断嵌套滑动是否可用
*/
@Override
public boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动
*
* @param axes 表示方向轴,有横向和竖向
*/
@Override
public boolean startNestedScroll(int axes);
/**
* 停止嵌套滑动
*/
@Override
public void stopNestedScroll();
/**
* 判断是否有父View 支持嵌套滑动
*/
@Override
public boolean hasNestedScrollingParent() ;
/**
* 滑行时调用
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @param consumed 是否被消费
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
*/
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) ;
/**
* 进行滑行前调用
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @return true if a nested scrolling parent consumed the fling
*/
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) ;
/**
* 子view处理scroll后调用
* @param dxConsumed x轴上被消费的距离(横向)
* @param dyConsumed y轴上被消费的距离(竖向)
* @param dxUnconsumed x轴上未被消费的距离
* @param dyUnconsumed y轴上未被消费的距离
* @param offsetInWindow 子View的窗体偏移量
* @return true if the event was dispatched, false if it could not be dispatched.
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) ;
/**
* 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
* @param dx x轴上滑动的距离
* @param dy y轴上滑动的距离
* @param consumed 父view消费掉的scroll长度
* @param offsetInWindow 子View的窗体偏移量
* @return 支持的嵌套的父View 是否处理了 滑动事件
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
}
二、案例展示
我们之前使用ViewDragerHelper实现的《自定义上滑组件NestedScrollLayout》,其实可以通过NestedScrolling机制实现,NestedScrolling相比ViewDragHelper,ViewDragHelper联动使用不当可能产生丢帧问题,联动机制非常强大,相比ViewDragHelper,可减少很多事件处理,让联动变的更简单。当然如果是单View非联动操作ViewDragHelper更有优势,具体问题具体解决。
我们通过本文开头展示效果,给RecyclerView增加一个的Header,这个Header不是通过Adapter实现,而是通过联动效果效果实现。
public class NestedScrollChildLayout extends FrameLayout implements NestedScrollingParent2 {
private final int mFlingVelocity;
private int mOverScrollExtends;
private float startEventX = 0;
private float startEventY = 0;
private float mSlopTouchScale = 0;
private boolean isTouchMoving = false;
private View mHeaderView = null;
private View mBodyView = null;
private View mVerticalScrollView = null;
private VelocityTracker mVelocityTracker;
public NestedScrollChildLayout(@NonNull Context context) {
this(context, null);
}
public NestedScrollChildLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollChildLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSlopTouchScale = ViewConfiguration.get(context).getScaledTouchSlop();
mFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
setClickable(true);
}
public void setOverScrollExtends(int overScrollExtends) {
this.mOverScrollExtends = overScrollExtends;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int height = MeasureSpec.getSize(heightMeasureSpec);
int overScrollExtent = overScrollExtent();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ 0, height-overScrollExtent);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range;
}
}
@Override
protected int computeVerticalScrollRange() {
int childCount = getChildCount();
if (childCount == 0) return super.computeVerticalScrollRange();
int range = getPaddingBottom() + getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
range += child.getHeight() + lp.bottomMargin + lp.topMargin;
}
if (range < getHeight()) {
return super.computeVerticalScrollRange();
}
return range;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mHeaderView = getChildView(LayoutParams.TYPE_HEAD);
mBodyView = getChildView(LayoutParams.TYPE_BODY);
int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
if (mHeaderView != null) {
LayoutParams lp = (LayoutParams) mHeaderView.getLayoutParams();
mHeaderView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mHeaderView.getMeasuredWidth(), childTop + lp.topMargin + mHeaderView.getMeasuredHeight());
childTop += mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
if (mBodyView != null) {
LayoutParams lp = (LayoutParams) mBodyView.getLayoutParams();
mBodyView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mBodyView.getMeasuredWidth(), childTop + lp.topMargin + mBodyView.getMeasuredHeight());
}
}
protected int overScrollExtent() {
return mOverScrollExtends;
}
private View getHeaderView() {
return mHeaderView;
}
private View getBodyView() {
return mBodyView;
}
private View findTouchView(float currentX, float currentY) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float childX = (child.getX() - getScrollX());
float childY = (child.getY() - getScrollY());
if (currentX < childX || currentX > (childX + child.getWidth())) {
continue;
}
if (currentY < childY || currentY > (childY + child.getHeight())) {
continue;
}
return child;
}
return null;
}
private boolean hasHeader() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_HEAD) {
return true;
}
}
return false;
}
public View getChildView(int layoutType) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == layoutType) {
return getChildAt(i);
}
}
return null;
}
private boolean hasBody() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
return true;
}
}
return false;
}
@Override
public void addView(View child) {
assertLayoutType(child);
super.addView(child);
}
private void assertLayoutType(View child) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
assertLayoutParams(lp);
}
private void assertLayoutParams(ViewGroup.LayoutParams lp) {
if (hasHeader() && hasBody()) {
throw new IllegalStateException("header and body has already existed");
}
if (hasHeader()) {
if (!(lp instanceof LayoutParams)) {
throw new IllegalStateException("header should keep only one");
}
if (((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_HEAD) {
throw new IllegalStateException("header should keep only one");
}
}
if (hasBody()) {
if ((lp instanceof LayoutParams) && ((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_BODY) {
throw new IllegalStateException("header should keep only one");
}
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
assertLayoutParams(params);
super.addView(child, index, params);
}
@Override
public void addView(View child, int index) {
assertLayoutType(child);
super.addView(child, index);
}
@Override
public void addView(View child, int width, int height) {
assertLayoutParams(new LinearLayout.LayoutParams(width, height));
super.addView(child, width, height);
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType != LayoutParams.TYPE_BODY) {
return;
}
if (!(child instanceof NestedScrollingChild) && !(child instanceof ScrollFlingChild)) {
throw new RuntimeException("body must be 'view implemention NestedScrollingChild or ScrollFlingChild '");
}
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new LayoutParams(lp);
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == SCROLL_AXIS_VERTICAL) {
//只关注垂直方向的移动
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int offset = computeVerticalScrollOffset();
if (offset <= maxOffset) {
mVerticalScrollView = target;
return true;
}
}
return false;
}
@Override
protected int computeVerticalScrollExtent() {
int computeVerticalScrollExtent = super.computeVerticalScrollExtent();
return computeVerticalScrollExtent ;
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
if (mVerticalScrollView == target) {
Log.d("onNestedScroll", "::::onStopNestedScroll vertical");
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return;
}
if (target == null) return;
if (mVerticalScrollView != target) {
return;
}
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
handleVerticalNestedScroll(dx, dy, consumed, maxOffset, scrollOffset);
}
private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed, int maxOffset, int scrollOffset) {
if (dy == 0) return;
if (!checkScrollableStationTop(mVerticalScrollView)) {
return;
}
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return;
}
consumed[1] = dyOffset;
Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
scrollBy(0, dyOffset);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return super.dispatchTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.addMovement(event);
startEventX = event.getX();
startEventY = event.getY();
isTouchMoving = false;
break;
case MotionEvent.ACTION_MOVE:
float currentX = event.getX();
float currentY = event.getY();
float dx = currentX - startEventX;
float dy = currentY - startEventY;
if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
startEventX = currentX;
startEventY = currentY;
break;
}
View touchView = null;
int offset = (int) -dy;
if (Math.abs(dy) >= mSlopTouchScale) {
touchView = findTouchView(currentX, currentY);
isTouchMoving = touchView != null && touchView == getHeaderView();
}
if (offset != 0 && !canScrollVertically(offset)) {
isTouchMoving = false;
}
startEventX = currentX;
startEventY = currentY;
if (!isTouchMoving) {
break;
}
mVelocityTracker.addMovement(event);
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int targetOffset = scrollOffset + offset;
if (targetOffset >= maxOffset) {
offset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
offset = 0 - scrollOffset;
}
if (offset == 0) {
break;
}
scrollBy(0, offset);
Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
mVelocityTracker.addMovement(event);
if (isTouchMoving) {
isTouchMoving = false;
mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.dispatchTouchEvent(event);
}
private void startFling(VelocityTracker velocityTracker, int x, int y) {
int xVolecity = (int) velocityTracker.getXVelocity();
int yVolecity = (int) velocityTracker.getYVelocity();
if (mVerticalScrollView instanceof NestedScrollingChild) {
Log.d("onNestedScroll", "onNestedScrollfling xVolecity=" + xVolecity + ", yVolecity=" + yVolecity);
((RecyclerView) mVerticalScrollView).fling(xVolecity, -yVolecity);
}
if (mVerticalScrollView instanceof ScrollFlingChild) {
((ScrollFlingChild) mVerticalScrollView).startFling(xVolecity, yVolecity);
}
}
private boolean checkScrollableStationTop(View view) {
if (view instanceof RecyclerView) {
//显示区域最上面一条信息的position
RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();
if (manager == null) {
return true;
}
if (manager.getChildCount() == 0) {
return true;
}
int scrollOffset = ((RecyclerView) view).computeVerticalScrollOffset();
return scrollOffset <= 0;
}
if (view instanceof NestedScrollingChild) {
return view.canScrollVertically(-1);
}
if ((view instanceof View) && !(view instanceof ViewGroup)) {
return true;
}
throw new IllegalArgumentException("不支持非NestedScrollingChild子类ViewGroup");
}
public static class LayoutParams extends FrameLayout.LayoutParams {
public final static int TYPE_HEAD = 0;
public final static int TYPE_BODY = 1;
private int childLayoutType = TYPE_HEAD;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
if (attrs == null) return;
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedScrollChildLayout);
childLayoutType = a.getInt(R.styleable.NestedScrollChildLayout_layoutScrollNestedType, 0);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull MarginLayoutParams source) {
super(source);
}
}
public interface ScrollFlingChild {
public void startFling(int xVolecity, int yVolecity);
}
}
子View属性参数
<declare-styleable name="NestedScrollChildLayout">
<attr name="layoutScrollNestedType" format="flags">
<flag name="Head" value="0"/>
<flag name="Body" value="1"/>
</attr>
</declare-styleable>
三、布局使用
<com.cn.scrolllayout.view.NestedScrollChildLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<LinearLayout
android:id="@+id/head"
android:layout_width="match_parent"
android:layout_height="200dp"
app:nestedChildLayoutType="Header"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@mipmap/img_sample_panda"
android:scaleType="centerCrop"
/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:nestedChildLayoutType="Body"
android:background="@color/colorPrimary"
/>
</com.cn.scrolllayout.view.NestedScrollChildLayout>