利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)

实现嵌套滑动有三种方案:

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嵌套滑动事件流程图

利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)_第1张图片
NestedScroll嵌套滑动事件.png

最后来总结一下整个流程,分为四个步骤:

  • 步骤一:子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实现下面效果

利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)_第2张图片
NestedScroll.gif
布局文件如下:


    
        
        
        
        
    


自定义MyNestedScrollingParent实现NestedScrollingParent接口

在源码中已经相关解释。主要讲一个方法ViewCompat.canScrollVertically(target, -1),判断target控件是否可以上滑。

在APi14之后,官方提供了canScrollVertically(int direction)方法,所以API14之后判断非常方便,官方文档的解释如下:
利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)_第3张图片
image.png

翻译下来就是:当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中控件实现下面的例子。


利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)_第4张图片
CoordinatorLayout.gif
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" //具体位置
实现上面效果的布局文件


    
        
            

            
        

    
    
        

            

            
            

            
            

            
            

            
            

            
            

            
        

    

    


你可能感兴趣的:(利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior))