自从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类中已经写好了大部分方法的实现,只需要调用就可以了。
NestedScrolling 相关Api的调用流程分析
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.youkuaiyun.com.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.youkuaiyun.com.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.youkuaiyun.com.mchenys.demo1.MyNestedScrollChild>
</blog.youkuaiyun.com.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接口的,所以可以完成嵌套滑动.
从这点也可以看出,嵌套滑动的组件,只要有包含关系,无论是否是直接包含都是有效果的.