实现嵌套滑动有三种方案:
1. 纯事件拦截与派发方案
2. 基于NestingScroll机制的实现方案
3. 基于CoordinatorLayout与Behavior的实现方案
第一种方案:灵活性最高,也最繁琐。因为事件的拦截是一锤子买卖,谁拦截了事件,当前手势接下来的事件都会交给拦截者来处理,除非等到下一次Down事件触发。这很不方便多个View对同一个事件进行处理。
第二种方案:其实就是对原始的事件拦截机制做了一层封装,通过子View实现NestedScrollingChild接口,父View实现NestedScrollingParent 接口,并且在子View和父View中都分别有一个NestedScrollingChildHelper、NestedScrollingParentHelper来代理了父子之间的联动,开发者不用关心具体是怎么联动的,这一点很方便。
第三种方案:其实就是对原始的NestedScrolling机制再次做了一层封装。CoordinatorLayout默认实现了NestedScrollingParent接口。第二种方案只能由子View通知父View,但有时候除了需要通知父View,还需要通知兄弟View,这个时候就该是Behavior出场了。
第一种方案:纯事件拦截与派发方案
文章中有事件分发的相关内容 View事件冲突和解决实例
第二种方案:NestingScroll机制
使用这种方式,一般需要继承NestedScrollingChild和NestedScrollingParent接口。同时NestedScrollingChildHelper和NestedScrollingScrollingParentHelper进行辅助。像RecyclerView,CoordinatorLayout等一些控件已经实现NestedScrollingChild,NestedScrollingParent接口。下面介绍一下接口中方法的作用。
NestedScrollingChild接口
public interface NestedScrollingChild {
/**
* 设置嵌套滑动是否能用
* @param enabled true to enable nested scrolling, false to disable
*/
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判断嵌套滑动是否可用
* @return true if nested scrolling is enabled
*/
public boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动
* @param axes 表示方向轴,有横向和竖向
*/
public boolean startNestedScroll(int axes);
/**
* 停止嵌套滑动
*/
public void stopNestedScroll();
/**
* 判断是否有父View 支持嵌套滑动
* @return whether this view has a nested scrolling parent
*/
public boolean hasNestedScrollingParent();
/**
* 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
* @param dx x轴上滑动的距离
* @param dy y轴上滑动的距离
* @param consumed 父view消费掉的scroll长度
* @param offsetInWindow 子View的窗体偏移量
* @return 支持的嵌套的父View 是否处理了 滑动事件
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* 子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.
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* 滑行时调用
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @param consumed 是否被消费
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
*/
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
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
NestedScrollingParent接口
public interface NestedScrollingParent {
/**
* 调用child的startNestedScroll()来发起嵌套滑动流程(实质上是寻找能够配合child进行嵌套滚动的parent)。
* parent的onStartNestedScroll()会被调用,若此方法返回true,则OnNestScrollAccepted()也会被调用。
* */
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
/**
* 后于child滚动
* @param target
* @param dxConsumed
* @param dyConsumed
* @param dxUnconsumed
* @param dyUnconsumed
*/
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* 先于child滚动
* 前3个为输入参数,最后一个是输出参数
* @param target
* @param dx
* @param dy 向上是正的,向下是负的 是差值
* @param consumed
*/
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();
}
当 NestedScrollingChild(下文用Child代替) 要开始滑动的时候会调用 onStartNestedScroll ,然后交给代理类NestedScrollingChildHelper(下文ChildHelper代替)的onStartNestedScroll请求给最近的NestedScrollingParent(下文Parent代替).
当ChildHelper的onStartNestedScroll方法 返回 true 表示同意一起处理 Scroll 事件的时候时候,ChildHelper会通知Parent回调onNestedScrollAccepted 做一些准备动作
当Child 要开始滑动的时候,会先发送onNestedPreScroll,交给ChildHelper的onNestedPreScroll 请求给Parent ,告诉它我现在要滑动多少距离,你觉得行不行,这时候Parent 根据实际情况告诉Child 现在只允许你滑动多少距离.然后 ChildHelper根据 onNestedPreScroll 中回调回来的信息对滑动距离做相对应的调整.
在滑动的过程中 Child 会发送onNestedScroll通知ChildeHelpaer的onNestedScroll告知Parent 当前 Child 的滑动情况.
当要进行滑行的时候,会先发送onNestedFling 请求给Parent,告诉它 我现在要滑行了,你说行不行, 这时候Parent会根据情况告诉 Child 你是否可以滑行.
Child 通过onNestedFling 返回的 Boolean 值来觉得是否进行滑行.如果要滑行的话,会在滑行的时候发送onNestedFling 通知告知 Parent 滑行情况.
当滑动事件结束就会child发送onStopNestedScroll通知 Parent 去做相关操作.
NestedScroll嵌套滑动事件流程图
最后来总结一下整个流程,分为四个步骤:
- 步骤一:子View的ACTION_DOWN调用startNestedScroll---->父View的onStartNestedScroll判断是否要一起滑动,父ViewonNestedScrollAccepted同意协同滑动
- 步骤二:子View的ACTION_MOVE调用dispatchNestedPreScroll---->父View的onNestedPreScroll在子View滑动之前先进行滑动并消耗需要的距离---->父View完成该次滑动之后返回消耗的距离,子View在剩下的距离中再完成自己需要的滑动
- 步骤三:子View滑动完成之后调用dispatchNestedScroll---->父View的onNestedScroll处理父View和子View之前滑动剩余的距离
- 步骤四:子View的ACTION_UP调用stopNestedScroll---->父View的onStopNestedScroll完成滑动收尾工作
这样,子View和父View的一系列嵌套滑动就完成了,可以看出来整个嵌套滑动还是靠子View来推动父View进行滑动的,这也解决了在传统的滑动事件中一旦事件被子View处理了就很难再分享给父View共同处理的问题,这也是嵌套滑动的一个特点。
NestedScrolling 机制能够让父 View 和子 View 在滚动式进行配合,而要实现这样的交互机制,首先父 view 要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口,在这套机制中子 View是发起者,父 view 是接受回调并做出响应的。
自定义MyNestedScrollingParent实现下面效果
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.chenpeng.julyapplication.NestedScrolling.MyNestedScrollParent
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="@mipmap/ic_launcher"/>
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="#009987"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</com.example.chenpeng.julyapplication.NestedScrolling.MyNestedScrollParent>
</LinearLayout>
自定义MyNestedScrollingParent实现NestedScrollingParent接口
在源码中已经相关解释。主要讲一个方法ViewCompat.canScrollVertically(target, -1),判断target控件是否可以上滑。
在APi14之后,官方提供了canScrollVertically(int direction)方法,所以API14之后判断非常方便,官方文档的解释如下:
翻译下来就是:当direction>0时,判断是否可以下滑,当direction<0时,判断是否可以上滑
public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {
private ImageView mImageView;
private RecyclerView mRecyclerView;
private int mImageViewHeight;
private NestedScrollingParentHelper mNestedScrollingParentHelper;
private OverScroller mScroller;
private static final String TAG = "MyNestedScrollParent";
public MyNestedScrollParent(Context context) {
this(context,null);
Log.i(TAG, "MyNestedScrollParent: 1");
}
public MyNestedScrollParent(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
Log.i(TAG, "MyNestedScrollParent: 2");
}
public MyNestedScrollParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mScroller = new OverScroller(context);
Log.i(TAG, "MyNestedScrollParent: 3");
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = (ImageView) getChildAt(0);
mRecyclerView = (RecyclerView) getChildAt(2);
mImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mImageViewHeight = mImageView.getMeasuredHeight();
Log.i(TAG, "onGlobalLayout: getMeasuredHeight = " + mImageViewHeight + " , getHeight = " + mImageView.getHeight());
}
});
}
/**
* 调用child的startNestedScroll()来发起嵌套滑动流程(实质上是寻找能够配合child进行嵌套滚动的parent)。
* parent的onStartNestedScroll()会被调用,若此方法返回true,则OnNestScrollAccepted()也会被调用。
* */
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
//在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
Log.i(TAG, "onStartNestedScroll: ");
//垂直滑动返回true
return (axes & SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child,target,axes);
Log.i(TAG, "onNestedScrollAccepted: ");
}
@Override
public void onStopNestedScroll(@NonNull View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
Log.i(TAG, "onStopNestedScroll: ");
}
/**
* 后于child滚动
* @param target
* @param dxConsumed
* @param dyConsumed
* @param dxUnconsumed
* @param dyUnconsumed
*/
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
/**
* 先于child滚动
* 前3个为输入参数,最后一个是输出参数
* @param target
* @param dx
* @param dy 向上是正的,向下是负的 是差值
* @param consumed
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
Log.i(TAG, "onNestedPreScroll: dy = " + dy + " , getScrollY() = " + getScrollY() + ", mRecyclerView.getChildAt(0).getTop() = "+ mRecyclerView.getChildAt(0).getTop());
if((dy>0 && getScrollY() < mImageViewHeight ||
(dy < 0 && getScrollY() > 0 && (!ViewCompat.canScrollVertically(target, -1))))){//!ViewCompat.canScrollVertically(target, -1)可以判断是否可继续下滑
scrollBy(0,dy);
consumed[1] = dy;//告诉child我消费了多少
}
}
/**
*scrollBy内部会调用scrollTo , 限制滚动范围
*/
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mImageViewHeight) {
y = mImageViewHeight;
}
super.scrollTo(x, y);
}
@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
//是否消费了fling
if (getScrollY() >= mImageViewHeight)
return false;
fling((int) velocityY);
return true;
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return false;
}
public void fling(int velocityY)
{
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mImageViewHeight);
invalidate();
}
@Override
public void computeScroll()
{
if (mScroller.computeScrollOffset())
{
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
}
第三种方案:基于CoordinatorLayout与Behavior的实现方案
通过依赖compile'com.android.support:design:26.1.0',中的CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout,FloatingActionButton中控件实现下面的例子。
AppBarLayou:
1.AppbarLayout:继承了LinearLayout(默认的AppBarLayout是垂直方向),它是为了Material Design设计的App Bar,它的作用是把AppBarLayout包裹的内容都作为AppBar。说白了它的出现就是为了和CoordinatorLayout搭配使用,实现一些炫酷的效果的。没有CoordinatorLayout,它和Linearlayout没区别。
2.app:layout_scrollFlags=”scroll|enterAlways”
AppBarLayout的直接子控件可以设置的属性:layout_scrollFlags
- scroll|exitUntilCollapsed 如果AppBarLayout的直接子控件设置该属性,该子控件可以滚动,向上滚动NestedScrollView出父布局(一般为CoordinatorLayout)时,会折叠到顶端,向下滚动时NestedScrollView必须滚动到最上面的时候才能拉出该布局
- scroll|enterAlways:只要向下滚动该布局就会显示出来,只要向上滑动该布局就会向上收缩
- scroll|enterAlwaysCollapsed:向下滚动NestedScrollView到最底端时该布局才会显示出来
如果不设置改属性,则改布局不能滑动
3.细心的朋友可能在NestedScrollView中发现这么一条属性
app:layout_behavior="@string/appbar_scrolling_view_behavior"
它的核心就在于Behavior类。我们给CoordinatorLayout的子View设置一个Behavior,就可以接管该View自身的一些事件与其他的View之间的交互事件。包括 touch,measure,layout等事件。我们在NestedScrollView设定的Android提供的Behavior,app:layout_behavior="@string/appbar_scrolling_view_behavior">,其中的string是Behavior的绝对路径。
CoordinatorLayout中Behavior介绍与简单使用
CollapsingToolbarLayout
我们在AppBarLayout中添加CollapsingToolbarLayout。Collapsing是折叠的意思。见名知意,CollapsingToolbarLayout的作用是提供了一个可以折叠的Toolbar,它继承至FrameLayout。
1.大家一定要弄清楚,它们之间的包含关系:
AppBarLayout >CollapsingToolbarLayout>Toolbar
2.CollapsingToolbarLayout作为AppBarLayout的直接子控件,也要设置app:layout_scrollFlags属性
3.app:layout_collapseMode属性是折叠模式,它是CollapsingToolbarLayout的直接子控件需要设置的,它的取值如下:
- pin:在滑动过程中,此自布局会固定在它所在的位置不动,直到CollapsingToolbarLayout全部折叠或者全部展开
- parallax:视察效果,在滑动过程中,不管上滑还是下滑都会有视察效果,不知道什么事视察效果自己看gif图(layout_collapseParallaxMultiplier视差因子 0~1之间取值,当设置了parallax时可以配合这个属性使用,调节自己想要的视差效果)
- 不设置:跟随NestedScrollView的滑动一起滑动,NestedScrollView滑动多少距离他就会跟着走多少距离
contentScrim折叠后的颜色也是展开时的渐变颜色,效果超赞.
title标题,如果设置在折叠时会动画的显示到折叠后的部分上面,拉开时会展开,很好玩的
expandedTitleMargin当title文字展开时文字的margin,当然还有marginTop等属性,脑补吧
app:collapsedTitleTextAppearance=”@style/Text”折叠时title的样式里面可以定义字体大小颜色等
app:collapsedTitleTextAppearance=”@style/Text1”折叠时title的样式里面可以定义字体大小颜色等
当然还有一些,自己试试吧,现在的这些已经完全够用了
FloatingActionButton
app:layout_anchor="@id/app_bar" //在哪个控件上
app:layout_anchorGravity="bottom|right" //具体位置
实现上面效果的布局文件
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="170dp"
app:contentScrim="#ff2077ff"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg"/>
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="parallax"
app:title="Title"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据1"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据2"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据1"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据2"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据1"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据2"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据1"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据2"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据1"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据2"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据1"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="测试数据2"
android:textSize="20sp" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|right"
app:srcCompat="@android:drawable/ic_dialog_email" />
</android.support.design.widget.CoordinatorLayout>