自从Lollipop开始,谷歌给我们带来了一套全新的嵌套滑动机制 - NestedScrolling来实现一些普通情况下不容易办到的滑动效果。Lollipop及以上版本的所有View都已经支持了这套机制,Lollipop之前版本可以通过Support包进行向前兼容。
它和我们已熟知的dispatchTouchEvent不太一样。 我们先来看传统的事件分发,它是由父View发起,一旦父View需要自己做滑动效果就要拦截掉事件并通过自己的onTouch进行消耗,这样子View就再没有机会接手此事件,如果自己不拦截交给子View消耗,那么不使用特殊手段的话父View也没法再处理此事件。
// Lollipop及以上版本的View源码多了这么几个方法: public void setNestedScrollingEnabled(boolean enabled); public boolean isNestedScrollingEnabled(); public boolean startNestedScroll(int axes); public void stopNestedScroll(); public boolean hasNestedScrollingParent(); public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); public boolean dispatchNestedPreFling(float velocityX, float velocityY); //Lollipop及以上版本的ViewGroup源码多了这么几个方法: public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); public void onStopNestedScroll(View target); public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); public boolean onNestedPreFling(View target, float velocityX, float velocityY); public int getNestedScrollAxes();前面已经说过Lollipop及以上版本的所有View都已经支持了NestedScrolling,Lollipop之前版本需要通过Support包进行向前兼容,需要Support包里的 以下4个类:
NestedScrollingParent // 接口 NestedScrollingParentHelper //辅助类 NestedScrollingChild //接口 NestedScrollingChildHelper //辅助类`上面NestedScrollingParent和NestedScrollingChild两个接口分别包含了ViewGroup和View中 涉及到NestedScrolling的所有Api.
那要怎么实现接口中辣么多的方法呢? 这就要用到上面的Helper辅助类了,Helper类中已经写好了大部分方法的实现,只需要调用就可以了。
1.首先子View需要找到一个支持NestedScrollingParent的父View,告知父View我准备开始和你一起处理滑动事件了,一般情况下都是在onTouchEvent的ACTION_DOWN中调用 public boolean startNestedScroll(int axes)//参数表示方向 axes可传如下参数
/** * Indicates no axis of view scrolling. */ public static final int SCROLL_AXIS_NONE = 0; /** * Indicates scrolling along the horizontal axis. */ public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0; /** * Indicates scrolling along the vertical axis. */ public static final int SCROLL_AXIS_VERTICAL = 1 << 1;2.然后父View就会被回调 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) //返回值表示是否接受嵌套滑动 和 public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)//紧接着上面方法之后调用,可做初始化操作
3.然后每次子View在滑动前都需要将滑动细节传递给父View,一般情况下是在 ACTION_MOVE中调用
/** * * @param dx x轴滑动距离 * @param dy y轴滑动距离 * @param consumed 子View创建给父View使用的数组,用于保存父View的消费距离 * @param offsetInWindow 子View创建给父View使用的数组,保存了子View滑动前后的坐标偏移量 * @return 返回父View是否有消费距离 */ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)4.然后父View就会被回调
/** * @param target 子View * @param dx 子View需要在x轴滑动的距离 * @param dy 子View需要在y轴滑动的距离 * @param consumed 子View传给父View的数组,用于保存消费的x和y方向的距离 **/ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)5.父View处理完后,接下来子View就要进自己的滑动操作了,滑动完成后子View还需要调用下面的方法将自己的滑动结果再次传递给父View. public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, int[] offsetInWindow)
6.然后父View就会被回调
/** * @param target 子View * @param dxConsumed x轴被子View消耗的距离 * @param dyConsumed y轴被子View消耗的距离 * @param dxUnconsumed x轴未被子View消耗的距离 * @param dyUnconsumed y轴未被子View消耗的距离 **/ public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)这个步骤的前提是: 父View没有将滑动值全部消耗掉,因为父View全部消耗掉,子View就不用再进行滑动了.
7.随着ACTION_UP或者ACTION_CANCEL的到来,子View需要调用 public void stopNestedScroll()//告知父View本次NestedScrollig结束.
8.父View对应的会被回调 public void onStopNestedScroll(View target)//可以在此方法中做一些对应停止的逻辑操作比如资源释放等.
9.如果当子View ACTION_UP时伴随着fling的产生,就需要子View在stopNestedScroll前调用 public boolean dispatchNestedPreFling(View target, float velocityX, float velocityY) 和 public boolean dispatchNestedFling(View target, float velocityX, float velocityY, boolean consumed)
10.父View对应的会被回调 public boolean onNestedPreFling(View target, float velocityX, float velocityY) 和 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) 这点和之前的scroll处理逻辑是一样的,返回值代表父View是否消耗掉了fling,参数 consumed代表子View是否消耗掉了fling,fling不存在部分消耗,一旦被消耗就是指全部。
下面来一个Demo演示下如何使用的. 效果图:
布局文件
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <blog.csdn.net.mchenys.demo1.MyNestedScrollParent android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/imageview" android:background="@drawable/icon_default" android:layout_width="match_parent" android:layout_height="200dp"/> <TextView android:textColor="#fff" android:text="固定栏" android:gravity="center" android:layout_width="match_parent" android:layout_height="50dp" android:background="@color/colorAccent"/> <blog.csdn.net.mchenys.demo1.MyNestedScrollChild android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:scaleType="fitXY" android:src="@drawable/icon_default" android:layout_gravity="center_horizontal" android:layout_width="250dp" android:layout_height="300dp"/> <ImageView android:scaleType="fitXY" android:src="@drawable/icon_default" android:layout_gravity="center_horizontal" android:layout_width="250dp" android:layout_height="300dp"/> <ImageView android:scaleType="fitXY" android:src="@drawable/icon_default" android:layout_gravity="center_horizontal" android:layout_width="250dp" android:layout_height="300dp"/> <ImageView android:scaleType="fitXY" android:src="@drawable/icon_default" android:layout_gravity="center_horizontal" android:layout_width="250dp" android:layout_height="300dp"/> <ImageView android:scaleType="fitXY" android:src="@drawable/icon_default" android:layout_gravity="center_horizontal" android:layout_width="250dp" android:layout_height="300dp"/> <ImageView android:scaleType="fitXY" android:src="@drawable/icon_default" android:layout_gravity="center_horizontal" android:layout_width="250dp" android:layout_height="300dp"/> </blog.csdn.net.mchenys.demo1.MyNestedScrollChild> </blog.csdn.net.mchenys.demo1.MyNestedScrollParent> </FrameLayout>MyNestedScrollParent.java
package blog.csdn.net.mchenys.demo1; import android.content.Context; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.OverScroller; import android.widget.TextView; public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent { private static final String TAG = "MyNestedScrollParent"; private ImageView img; private TextView tv; private MyNestedScrollChild myNestedScrollChild; private NestedScrollingParentHelper mNestedScrollingParentHelper; private int imgHeight; private int tvHeight; private OverScroller mScroller; public MyNestedScrollParent(Context context) { super(context); } public MyNestedScrollParent(Context context, AttributeSet attrs) { super(context, attrs); mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mScroller = new OverScroller(context); } //获取子view @Override protected void onFinishInflate() { super.onFinishInflate(); img = (ImageView) getChildAt(0); tv = (TextView) getChildAt(1); myNestedScrollChild = (MyNestedScrollChild) getChildAt(2); img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (imgHeight <= 0) { imgHeight = img.getMeasuredHeight(); } } }); tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (tvHeight <= 0) { tvHeight = tv.getMeasuredHeight(); } } }); } //在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动 @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { if (target instanceof MyNestedScrollChild) { return true; } return false; } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); } @Override public void onStopNestedScroll(View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); } //先于child滚动 //前3个为输入参数,最后一个是输出参数 /** * * @param target 子View * @param dx 子View需要在x轴滑动的距离 * @param dy 子View需要在y轴滑动的距离 * @param consumed 子View传给父View的数组,用于保存消费的x和y方向的距离 */ @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (showImg(dy) || hideImg(dy)) {//如果需要显示或隐藏图片,即需要自己(parent)滚动 scrollBy(0, -dy);//滚动 consumed[1] = dy;//告诉child我消费了多少 } } //后于child滚动 /** * * @param target 子View * @param dxConsumed x轴被子View消耗的距离 * @param dyConsumed y轴被子View消耗的距离 * @param dxUnconsumed x轴未被子View消耗的距离 * @param dyUnconsumed y轴未被子View消耗的距离 */ @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (dyUnconsumed > 0) { // 如果子View还有为消费的,可以继续消费 scrollBy(0, -dyUnconsumed);//滚动 } } //返回值:是否消费了fling 先于child fling @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { if (getScrollY() >= 0 && getScrollY() < imgHeight) { fling((int) velocityY); return true; } return false; } //返回值:是否消费了fling,后于child fling @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if(!consumed){ fling((int) velocityY); return true; } return false; } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } //下拉的时候是否要向下滚动以显示图片 public boolean showImg(int dy) { if (dy > 0) { if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) { return true; } } return false; } //上拉的时候,是否要向上滚动,隐藏图片 public boolean hideImg(int dy) { if (dy < 0) { if (getScrollY() < imgHeight) { return true; } } return false; } //scrollBy内部会调用scrollTo //限制滚动范围 @Override public void scrollTo(int x, int y) { if (y < 0) { y = 0; } if (y > imgHeight) { y = imgHeight; } super.scrollTo(x, y); } public void fling(int velocityY) { Log.e("parent", "velocityY:" + velocityY); mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, imgHeight); invalidate(); } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); postInvalidate(); } } //处理自身的滚动逻辑 private int lastY; private VelocityTracker mVelocityTracker; @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { //按下 case MotionEvent.ACTION_DOWN: lastY = (int) event.getRawY(); if (!mScroller.isFinished()) { //fling mScroller.abortAnimation(); } break; //移动 case MotionEvent.ACTION_MOVE: int y = (int) (event.getRawY()); int dy = y - lastY; lastY = y; scrollBy(0, -dy); break; case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); int vy = (int) mVelocityTracker.getYVelocity(); fling(-vy); break; } return true; } }MyNestedScrollChild.java
package blog.csdn.net.mchenys.demo1; import android.content.Context; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.OverScroller; public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild { private static final String TAG = "MyNestedScrollChild"; private NestedScrollingChildHelper mNestedScrollingChildHelper; private final int[] offset = new int[2]; //偏移量 private final int[] consumed = new int[2]; //消费 private int lastY; private int maxScrollY;//最大滚动距离 private OverScroller mScroller; private VelocityTracker mVelocityTracker; public MyNestedScrollChild(Context context) { this(context, null); } public MyNestedScrollChild(Context context, AttributeSet attrs) { super(context, attrs); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); mScroller = new OverScroller(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int contentHeight = 0; for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); measureChild(view, widthMeasureSpec, heightMeasureSpec); contentHeight += view.getMeasuredHeight();//内容高度 } int parentHeight = ((ViewGroup) getParent()).getMeasuredHeight();//父view高度 int pinTopHeight = (int) (getResources().getDisplayMetrics().density * 50 + 0.5);//固定头的高度 int visibleHeight = parentHeight - pinTopHeight;//可见高度 maxScrollY = contentHeight - visibleHeight; setMeasuredDimension(getMeasuredWidth(), visibleHeight); } @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { //按下 case MotionEvent.ACTION_DOWN: lastY = (int) event.getRawY(); if (!mScroller.isFinished()) { //fling mScroller.abortAnimation(); } break; //移动 case MotionEvent.ACTION_MOVE: int y = (int) (event.getRawY()); int dy = y - lastY; lastY = y; if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) && dispatchNestedPreScroll(0, dy, consumed, offset)) { //父类有消费距离 //获取滑动距离 int remain = dy - consumed[1]; if (remain != 0) { scrollBy(0, -remain); //这个时候由于子View已经全部消费调了剩余的距离,其实可以不用调用下面这个方法了. //dispatchNestedScroll(0, remain, 0, 10, offset); } } else { scrollBy(0, -dy); } break; case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); float vy = mVelocityTracker.getYVelocity(); if (!dispatchNestedPreFling(0, -vy)) { //父View没有fling,则子View处理 fling(-vy); //这句话可以不用调了,因为这子View已经处理了fling //dispatchNestedFling(0, -vy, true); } break; } return true; } //限制滚动范围 @Override public void scrollTo(int x, int y) { Log.d(TAG, "Y:" + y + " maxScrollY:" + maxScrollY); if (y > maxScrollY) { y = maxScrollY; } if (y < 0) { y = 0; } super.scrollTo(x, y); } public void fling(float velocityY) { mScroller.fling(0, getScrollY(), 0, (int) velocityY, 0, 0, 0, maxScrollY); invalidate(); } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); postInvalidate(); } } //实现一下接口 @Override public void setNestedScrollingEnabled(boolean enabled) { mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mNestedScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mNestedScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mNestedScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mNestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } /** * * @param dx x轴滑动距离 * @param dy y轴滑动距离 * @param consumed 子View创建给父View使用的数组,用于保存父View的消费距离 * @param offsetInWindow 子View创建给父View使用的数组,保存了子View滑动前后的坐标偏移量 * @return 返回父View是否有消费距离 */ @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } }本例中的布局由三部分组成,一个是带下拉刷新的自定义布局,一个是带嵌套滑动的自定义布局,最后一个是ViewPager,如下图所示:
1.红色区域是带下拉刷新的LinearLayout,通过重写onInterceptTouchEvent实现触摸事件拦截,重写onTouchEvent实现刷新头的滑动. 2.绿色区域是带嵌套滚动的LinearLayout,实现了NestedScrollingParent接口处理和子View中的RecycleView的嵌套滑动效果.其内部包含了焦点图、tab导航条、ViewPager. 同时还重写了onInterceptTouchEvent方法拦截事件交给自己的onTouchEvent去处理焦点图的滑动,当焦点图划出屏幕后,该View将不能继续上拉滑动,但是事件已经拦截,如何传递给RecycleView呢? 这里可以巧妙的在其onTouchEvent方法中调用RecycleView的onTouchEvent方法去处理,否则就只能松手后才能滑动RecycleView. 至于当焦点图划出屏幕后,如果RecycleView已经滚动了一段距离,下拉RecycleView不松手也能划出焦点图这个效果就是NestedScrolling 来实现的了. 3.蓝色区域就是ViewPager,它里面展示的是Fragment,而Fragment里面放的才是RecycleView.由于RecycleView是实现了NestedScrollingChild接口的,所以可以完成嵌套滑动. 从这点也可以看出,嵌套滑动的组件,只要有包含关系,无论是否是直接包含都是有效果的.
Demo下载