解决多层嵌套滑动冲突

  CoordinatorLayout作为顶层布局与NestedScrollView配合使用,可以用来协调子View的嵌套滑动。但是,如果要在CoordinatorLayout的外层嵌套XRefreshView下拉刷新控件,并且NestedScrollView嵌套多种可滑动的控件,这时候就出现了滑动冲突,具体嵌套结构如下所示:


1534665037068.jpg

XML布局结构大致如下:

<XRefreshView>
  <MyCoordinatorLayout>
    <AppBarLayout>
      <CollapsingToolbarLayout>
        ......
      </CollapsingToolbarLayout>
    </AppBarLayout>
  </MyCoordinatorLayout>

<NestedScrollView>
  <VerticalLinearLayout>
    <LinearLayout/>
    <SmartTabLayout/>
    <ViewPager/>
  </VerticalLinearLayout>
</NestedScrollView>
</XRefreshView>

<!--XRefreshView开源下拉刷新控件-->
<!--SmartTabLayout开源分类轴控件-->
<!--MyCoordinatorLayout:自定义控件,继承CoordinatorLayout-->
<!--VerticalLinearLayout:自定义控件,继承LinearLayout-->
<!--ViewPager下每个Fragment的布局是RecyclerView-->
<RecyclerView/>

  对于这些滑动冲突,我们该如何解决呢?下面我们就来逐一分析,解决这些滑动冲突。

  1、XRefreshView嵌套CoordinatorLayout

  我们知道事件分发是从上往下传递的(从Activity->Window->DecorView->......View),所以我们是不是可以在需要下拉刷新的时候,将事件拦截,交给下拉刷新控件处理,其他时候都不拦截事件。那么我们要选择用内部拦截法还是外部拦截法来处理滑动冲突呢?

  外部拦截法,那我们就需要在XRefreshView的onInterceptTouchEvent进行处理。XRefreshView是一个开源控件,从源码中,我们可以看到XRefreshView并未覆写onInterceptTouchEvent,而是覆写了dispatchTouchEvent方法,并在方法中进行了一系列复杂的事件分发。若使用外部拦截法来处理,就需要理清XRefreshView原有的事件分发规则,根据我们的实际需求对源码进行修改,对于父容器需要的事件进行拦截。

  内部拦截法,自定义CoordinatorLayout,覆写dispatchTouchEvent方法,通过调用requestDisallowInterceptTouchEvent方法来干扰父容器对事件的拦截。从XRefreshView的源码中我们发现该控件提供了一个disallowInterceptTouchEvent方法,从方法注释可知,子View需要事件的时候,可设置为true,不允许父容器拦截触摸事件。

XRefreshView.java

 /**
    //XRefreshView.java

     * if child need the touch event,pass true
     */
    public void disallowInterceptTouchEvent(boolean isIntercept) {
        mIsIntercept = isIntercept;
    }

  相比两种方法,这里内部拦截法更简单,所以最终选择了内部拦截法。

  触发下拉刷新的时机:垂直向下滑动,appBarLayout完全展开状态,允许XRefreshView拦截事件。

//MyCoordinatorLayout.java
private int mLastX,mLastY;
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //内部拦截法
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(xRefreshView!=null){
                    xRefreshView.disallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if(Math.abs(deltaY)>Math.abs(deltaX)){
//                    if(observed!=null && observed.getAppBarLayoutStatus() == 1 && deltaY>0 && xRefreshView!=null && isTop()){
//                        //垂直向下滑动,appBarLayout展开状态,列表第一个item可见,将事件交还给父容器XRefreshView
//                        xRefreshView.disallowInterceptTouchEvent(false);
//                    }

                    // 注意:若此时item位置不是第一个可见时,不能下拉刷新,若不需要item位置第一个可见时就可以下拉刷新,可以把isTop判断去掉,若下所示:
                    if(observed!=null && observed.getAppBarLayoutStatus() == 1 && deltaY>0 && xRefreshView!=null){
                        //垂直向下滑动,appBarLayout展开状态,将事件交还给父容器XRefreshView
                        xRefreshView.disallowInterceptTouchEvent(false);
                    }else{
                        //判断触摸点是否落在banner上
                        bannerView = getBannerView();
                        if(bannerView!=null){
                            isTouchPointInBannerView = Util.calcViewScreenLocation(bannerView).contains(ev.getRawX(),ev.getRawY());
                        }else{
                            isTouchPointInBannerView = false;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                isTouchPointInBannerView = false;
                break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }

  2、NestedScrollView嵌套多种布局控件(LinearLayout、SmartTabLayout、ViewPager,ViewPager中又嵌套了RecyclerView)

  这里主要解决的不是滑动冲突,而是NestedScrollView嵌套ViewPager无法显示的问题。网上的解决方案有2种,一是重写ViewPager的onMeasure方法,遍历每个页面,获取最高的页面高度来设置ViewPager的高度,二是给NestedScrollView设置android:fillViewport="true"允许 NestedScrollView中的组件去充满它。对于第一种方案,存在一个问题,每个页面的内容高度都一样,并且滑动其中一个页面的列表时,其他页面的列表也会滑动,所以这里采用了方案二。

  在SmartTablayout上方我们设置了一个LinearLayout,这个LinearLayout可以用来作为广告布局的一个父容器。当滑动NestedScrollView时,这个LinearLayout需要可以往屏幕外滑出,直到smartTabLayout保持在置顶位置。那要怎么让LinerLayout可以滑出屏幕直至不可见呢?答案是增大可滑动的空间,在原来内容高度的基础上增加广告布局父容器的高度。我们知道,NestedScrollView不能直接嵌套多个布局,只能有一个直接子类(允许直接嵌套的子类有RecyclerVIew、ViewPager、LinearLayout),这里选择在线性布局里嵌套多个布局,并且自定义这个直接子类,重新去计算它的高度(在系统计算的高度上,加上允许滑出屏幕的高度),具体如下所示:

xml布局:

......
        <android.support.v4.widget.NestedScrollView
                android:id="@+id/nestedScrollView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_behavior="@string/appbar_scrolling_view_behaviorr"
                android:fillViewport="true">
                <com.lmz.viewdemo.view.VerticalLinearLayout
                    android:id="@+id/NestedVerLinearLayout"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:orientation="vertical"
                    android:descendantFocusability="blocksDescendants">
                    <LinearLayout
                        android:id="@+id/linBanner"
                        android:layout_width="match_parent"
                        android:layout_height="150dp"
                        android:orientation="vertical"
                        android:background="@drawable/banner"/>
                    <com.ogaclejapan.smarttablayout.SmartTabLayout
                        android:id="@+id/smartTabLayout"
                        android:layout_width="match_parent"
                        android:layout_height="40dp"
                        android:layout_toLeftOf="@+id/ivCategoryBtn"
                        android:background="#d8d8d8"
                        android:overScrollMode="never"
                        app:stl_defaultTabTextHorizontalPadding="24dp"
                        app:stl_dividerColor="@android:color/transparent"
                        app:stl_dividerThickness="0dp"
                        app:stl_indicatorColor="#ff3444"
                        app:stl_indicatorInterpolation="linear"
                        app:stl_indicatorThickness="4dp"
                        app:stl_titleOffset="auto_center"
                        app:stl_underlineColor="@android:color/transparent"
                        app:stl_underlineThickness="0dp"/>
                    <android.support.v4.view.ViewPager
                        android:id="@+id/viewpager"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"/>
                </com.lmz.viewdemo.view.VerticalLinearLayout>
            </android.support.v4.widget.NestedScrollView>
......

VerticalLinearLayout.java

public class VerticalLinearLayout extends LinearLayout{

    private int maxOffsetY;

    public VerticalLinearLayout(Context context) {
        super(context);
        init();
    }

    public VerticalLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public VerticalLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        View child;
        int childCount = getChildCount();
        int offset = 0;
        for(int i=0;i<childCount;i++){
            child = getChildAt(i);
            if(child!=null && child.getVisibility()!=View.GONE
                    && !(child instanceof ViewPager)  && !(child instanceof SmartTabLayout)){
                measureChildWithMargins(child,widthMeasureSpec,0,MeasureSpec.UNSPECIFIED,0);
                offset = offset + child.getMeasuredHeight();
            }
        }
        this.maxOffsetY = offset;//可滑出屏幕的最大距离
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(specSize + offset,MeasureSpec.EXACTLY));
    }

    public int getMaxOffsetY(){
        return maxOffsetY;
    }

}

  3、CoordnatorLayout嵌套NestedScrollView(NestedScrollView内嵌套多种控件)

  在进入正题之前,我们先来简单回顾下NestedScrolling滑动机制。关键接口NestedScrollingParent和NestedScrollingChild,以及他们所对应的Helper(NestedScrollingParentHelper、NestedScrollingChildHelper)。

  嵌套滑动首先由子view触发调用startNestedScroll方法,寻找能够配合子View嵌套滑动parent。在子View处理滑动事件之前调用dispatchNestedPreScroll,询问parent是否需要在子View之前处理滑动,通过回调onNestedPreScroll方法告知parent当前滑动的距离,若父类有消耗滑动距离,可通过onNestedPreScroll方法的consumed这个输出参数来告知子View。子View处理完滑动事件后调用dispatchNestedScroll方法通过回调onNestedScroll告知parent,关于子view消费的部分和子view没有消费的部分,parent可对未消费部分进行处理。

CoordnatorLayout实现了NestedScrollingParent
NestedScrollView实现了NestedScrollingParent和NestedScrollingChild
RecyclerView实现了NestedScrollingChild

以下是嵌套滑动child和parent对应关系

1534679979957.jpg

  简单的回顾了NestedScrolling滑动机制,现在开始进入正题。

  当我们的触摸点在RecyclerView或在分类轴上方广告父容器时,配合他们滑动的parent是谁呢?我们可以自定义CoordinatorLayout和NestedScrollView,覆写 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) 方法,打印target参数,发现只有CoordinatorLayout的onNestedPreScroll有打印日志,并且target的输出是RecyclerView或NestedScrollView,所以配合RecyclerView和NestedScrollView嵌套滑动的parent是CoordinatorLayout,除此之外,还发现调用super.onNestedPreScroll(target, dx, dy, consumed, type)方法,在AppbarLayout折叠或下滑时consumed[1]=0。也就是AppBarLayout折叠或下滑的时候,CoordinatorLayout告知子VIew,父View在Y轴距离上没有消耗,这就是滑动冲突的原因。

  比如,appbarLayout折叠时,滑动触摸点在RecycleView上,进行上滑操作,只能滑动列表,分类轴上方广告容器滑动不了,这是由于parent告知RecyclerView它在y轴上消耗0,将所有y轴距离都交给了RecycleView来消耗。

  既然知道了配合滑动的parent是CoordinatorLayout以及滑动冲突原因,那么就可以在onNestedPreScroll方法中按业务制定滑动规则,来分配dy的消耗。这里的处理规则可以看以下代码中的注释。

  注:分类轴上方广告容器(代码中都称banner)。

//MyCoordinatorLayout.java
@Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
        Log.e(TAG,"target:"+target);
//        Log.e(TAG,"super before dy:"+dy);
        super.onNestedPreScroll(target, dx, dy, consumed, type);//必须放在前面调用,后面对父容器消耗的dy进行处理,来解决与子元素的滑动冲突
//        Log.e(TAG,"super after dy:"+dy+",dx:"+dx+",consumed[1]:"+consumed[1]);
        if(consumed[1] == 0 && !isTouchPointInBannerView){
            //AppbarLayout折叠或下滑时,consumed[1]=0,并且触摸点不在Banner上
            nsvMaxOffsetY = getNestedScrollViewMaxOffset();
            if(nsvMaxOffsetY>0 && nestedScrollView !=null){
                //NestedScrollView存在最大滑出屏幕的偏移量时,需要对dy消耗进行处理
                if(dy>0){
                    //上滑
                    if(nsvMaxOffsetY == nestedScrollView.getScrollY()){
                        //banner隐藏时,不消耗dy,交给列表,列表滑动dy
                        consumed[1] = 0;
                    }else{
                        //banner可见
                        //触摸点在RecyclerView上时,设置父容器消耗dy,不让列表滑动,同时滚动NestedScrollView
                        consumed[1] = dy;
                        nestedScrollViewScrollBy(0,dy);
                    }

                }else{
                    //下滑
                    if(nestedScrollView.getScrollY() == nsvMaxOffsetY){
                        //banner隐藏
                        if(isTop()){
                            //列表第一个item可见,设置父容器消耗dy,不让列表滑动,同时滚动NestedScrollView
                            consumed[1] = dy;
                            nestedScrollViewScrollBy(0,dy);
                        }else{
                            //列表第一个item不可见,父容器不消耗dy,交给RecyclerView消耗dy
                            consumed[1] = 0;
                        }
                    }else if(nestedScrollView.getScrollY()>0){
                        //banner可见未完全展开
                        //触摸点在RecyclerView上时,设置父容器消耗dy,不让列表滑动,同时滚动NestedScrollView
                        consumed[1] = dy;
                        nestedScrollViewScrollBy(0,dy);
                    }
                }
            }
        }
    }

那么多层嵌套滑动冲突的解决到这里就结束了。源码地址:https://github.com/lmz14/NestedScrollDemo

版权声明: 转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容