Android嵌套滑动,我用NestedScrollView

3cedf0b3d83f3413a2ed549623a52234.png

/   今日科技快讯   /

近日,流媒体巨头奈飞(Netflix)发布了截至2022年3月31日的第一季度财报。财报显示,奈飞第一季度营收78.7亿美元,同比增长9.8%;净利润为16亿美元,同比下降5.9%;每股收益为3.53美元,同比下降5.9%。与去年第四度相比,净订户流失20万。财报发布后,奈飞股价在盘后交易中暴跌25%以上。

/   作者简介   /

本篇文章来自android超级兵的投稿,文章主要对了Material Design系列中NestedScrollView的进行了相关的探索,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

android超级兵的博客地址:

https://blog.csdn.net/weixin_44819566?type=blog

/   前言   /


相信大家在开发过程中经常会遇到嵌套滚动的场景,最常见的莫过于NestedScrollView。NestedScrollView比较特殊,要想看懂他的源码,必须得了解2个东西,NestedScrollingChild和NestedScrollingParent,首先就从这两个接口的参数聊起~

/   了解一下   /

NestedScrollingChild

public interface NestedScrollingChild {
   /**
     开启/关闭滚动视图 
   */
      void setNestedScrollingEnabled(boolean enabled);

    /**
      是否开启滚动时图
    */
    boolean isNestedScrollingEnabled();

    /**
     开启滚动时候时候调用,用来通知parentView开始滚动,常在TouchEvent.ACTION_DOWN事件中调用
     tips:代理给 NestedScrollingChildHelper.startNestedScroll()方法即可

     @param axes: 滚动方向
                     SCROLL_AXIS_HORIZONTAL 水平
                     SCROLL_AXIS_VERTICAL 垂直
                     SCROLL_AXIS_NONE 没有方向
    */
       boolean startNestedScroll(@ScrollAxis int axes);

    /**
     停止滚动时候调用,用来通知parentView停止滚动,常在TouchEvent.ACTION_UP / ACTION_CANCLE 中调用
     tips: 代理给 NestedScrollingChildHelper.stopNestedScroll()即可
    */
      void stopNestedScroll();

    /**
      判断当前view是否有嵌套滑动的parentView正在接受事件 
      tips:代理给 NestedScrollingChildHelper.hasNestedScrollingParent()即可

      return true:有嵌套滑动的parentView
    */
      boolean hasNestedScrollingParent();

    /**
     当前view消费滚动距离后调用该方法,吧剩下的滚动距离传递给parentView,
     如果当前没有发生嵌套滚动,或者不支持嵌套滚动,那么该方法就没啥用.. 常在TouchEvent.ACTION_MOVE中调用 
     tips:代理给NestedScrollingChildHelper.dispatchNestedScroll()即可

     @param dxConsumed: 已经消费的水平(x)方向距离
     @param dyConsumed: 已经消费的垂直方(y)向距离
     @param dxUnconsumed: 未消费过的水平(x)方向距离
     @param dyUnconsumed: 未消费过的垂直(y)方向距离
     @param offsetInWindow:  滑动之前和滑动之后的偏移量 
                     if(offsetInWindow != null){
                             x = offsetInWindow[0] 
                             y = offsetInWindow[1]
                     }
     return true: 有嵌套滚动(parentView extents NestedScrollingParent)
    */
      boolean dispatchNestedScroll(int dxConsumed,
                                 int dyConsumed,
                                 int dxUnconsumed,
                                 int dyUnconsumed,
                                 @Nullable int[] offsetInWindow);

    /** 
      将事件分发给 parentView,如果 parentView 消费则返回true 
      常在TouchEvent.ACTION_MOVE中调用
      tips:代理给 NestedScrollingChildhelper.dispatchNestedPreScroll()即可

      @param dx:水平(x)滚动的距离(以像素为单位)
      @param dy:垂直(y)滚动的距离(以像素为单位)
      @param consumed: 主要用来父容器消费封装,并且通知子容器 x = consumed[0]; y = consumed[1];
      @param offsetInWindow:滑动之前和滑动之后的偏移量 
      return true: 表示父容器消费了事件 
    */
    boolean dispatchNestedPreScroll(int dx, 
                                    int dy,
                                    @Nullable int[] consumed,
                                                            @Nullable int[] offsetInWindow);

    /**
      用来处理惯性滑动
      tips:代理给 NestedScrollingChildhelper.dispatchNestedFling()即可

      @param velocityX: 用来处理x轴惯性滑动
      @param velocityY: 用来处理y轴惯性滑动
      @param consumed: 当前view是否消费了事件
      return true: 有嵌套滚动(parentView extents NestedScrollingParent)
    */
   boolean dispatchNestedFling(float velocityX, 
                               float velocityY,
                               boolean consumed);

    /**
      分发fling事件给parentView
      tips:代理给 NestedScrollingChildhelper.dispatchNestedPreFling()即可

      @param velocityX: 用来处理x轴惯性滑动
      @param velocityY: 用来处理y轴惯性滑动
      return true: 父容器消费了事件
    */
  boolean dispatchNestedPreFling(float velocityX, 
                                 float velocityY);
}

NestedScrollingChild和NestedScrollingChild2的区别:

0a22edb5d4116ab0c28e9f44462b526d.png

可以看出,NestedScrollingChild2只是比NestedScrollingChild多了一个参数NestedScrollType。

@IntDef({TYPE_TOUCH, TYPE_NON_TOUCH})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP_PREFIX)
public @interface NestedScrollType {}
  • NestedScrollType.TYPE_TOUCH 表示正常的滑动

  • NestedScrollType.TYPE_NON_TOUCH 表示在滑动过程中迅速点击屏幕,终止滑动

NestedScrollingChild3和NestedScrollingChild2的区别:

5e5dee4ff3c4326c4a246f9c8c6f384b.png

可以看出,也是多了一个参数,其实很简单,就是google工程师在编写NestedScrollView的时候,没有考虑清楚,所以就这样加上了…

99c26a32204e59f87e9a82b62fe72c11.png

NestedScrollingParent

public interface NestedScrollingParent {

  /**
      当NestedScrollingChildHelper.startNestedScroll()时候执行,用来接受ChildView#onTouchEvent#DOWN事件
      @param child: 如果只有嵌套一层 那么 child = target
                     <ParentNestedScrollView>
                              <A_ViewGroup>
                                  <B_ViewGroup>
                                      <ChildNestedScrollView/>
                                  </B_ViewGroup>
                              </A_ViewGroup>
                          </ParentNestedScrollView>
                      如果格式为这样,child = A_ViewGroup
      @param target: 本次嵌套滚动的view (ChildNestedScrollView)
      @param axes: 滚动方向 
                      SCROLL_AXIS_HORIZONTAL 水平
                      SCROLL_AXIS_VERTICAL 垂直
      return true: 表示接收嵌套事件 
  */
  boolean onStartNestedScroll(@NonNull View child,
                              @NonNull View target, 
                              @ScrollAxis int axes);

  /**
    当 onStartNestedScroll() 返回true时候执行,常用来做一些初始化工作
      tips: 代理给NestedScrollingParent.onNestedScrollAccepted()方法即可

        参数和onStartNestedScroll()相同 
  */
  void onNestedScrollAccepted(@NonNull View child, 
                              @NonNull View target,
                              @ScrollAxis int axes);

  /**
    当NestedScrollingChildHelper.stopNestedScroll()时候执行
        tips:代理给NestedScrollingParent.onStopNestedScroll()即可

        @param target:childNestedScrollView
  */
  void onStopNestedScroll(@NonNull View target);

  /**
   当NestedScrollingChildHelper.dispatchNestedScroll()时候调用
   @param target:childNestedScrollView
   @param dxConsumed: 已经消费的x距离
   @param dyConsumed: 已经消费的y距离
   @param dxUnconsumed: 未消费的x距离
   @param dyUnconsumed:    未消费的y距离
  */
  void onNestedScroll(@NonNull View target,
                      int dxConsumed,
                      int dyConsumed,
                                int dxUnconsumed,
                      int dyUnconsumed);

    /**
           当NestedScrollingChildHelper.dispatchNestedPreScroll()时候调用
            @param target:childNestedScrollView
            @param dx: x位置
            @param dy: y位置
            @param consumed: 表示parentView需要消费的距离 x = consumed[0]; y = consumed[1];
            tips: 只有consumed 改变值才说明parentView消费了事件
                        那么 NestedScrollingChild.dispatchNestedPreScroll() 才会返回true
    */
   void onNestedPreScroll(@NonNull View target,
                          int dx,
                          int dy,
                          @NonNull int[] consumed);

    /**
            fling事件
            @param target:childNestedScrollView
            @param velocityX: x轴滚动速度
            @param velocityY: y轴滚动速度
            @param consumed: 是否消费
            return true:有嵌套滚动事件
    */
   boolean onNestedFling(@NonNull View target,
                         float velocityX,
                         float velocityY, 
                         boolean consumed);

    /**
        fling事件parentView消费
            @param velocityX: x轴滚动速度
            @param velocityY: y轴滚动速度
    */
   boolean onNestedPreFling(@NonNull View target,
                            float velocityX, 
                            float velocityY);

    /**
       获取滚动的方向
       ViewCompat#SCROLL_AXIS_HORIZONTAL
       ViewCompat#SCROLL_AXIS_VERTICAL
       ViewCompat#SCROLL_AXIS_NONE
    */
    int getNestedScrollAxes();
}

tips:NestedScrollingParent2和NestedScrollingParent3改动和NestedScrollingChlid2/NestedScrollingChlid3一样,就不重复解释啦。

走到这里,前胃菜就结束啦,接下来先来分析一波NestedScrollView源码!

/   NestedScrollView源码分析   /

我通过分析 NestedScrollView 能够知道那些内容。

为什么NestedScrollView只能添加1个ChildView

先来捋一遍 setContentView流程:

912c4c5a23fa14e528ab67174843c646.png

流程图非常清晰,最终会调用到ViewGroup.addView(View,LauoutParams)上,先来测试一下这个addView是什么。

2a6bd7df108106b3c768531e023090c6.png

从图这里得知,在super.addView()中累加ChildCount的值,但是说了这么多,和NestedScrollView有什么关系呢?

回到NestedScrollView的源码中…

可以从NestedScrollView#addView(View child, ViewGroup.LayoutParams params)中看出,在添加第二个View的时候,直接就报错了,报错信息为:

ScrollView can host only one direct child

524d94cf6198589d4034782619bfc2d2.png

NestedScrollView的事件分发流程

众所周知,事件分发主要分为:

  • onInterceptTouchEvent

  • onTouchEvent

    • ACTION_DOWM

    • ACTION_MOVE

    • ACTION_UP / ACTION_CANCEL

本篇主要讲解事件传递流程,onInterceptTouchEvent就不提了,就从onTouchEvent来开始聊。

onTouchEvent#ACTION_DOWN事件

# NestedScrollView.java

public boolean onTouchEvent(MotionEvent ev) {
   switch(ev.getActionMasked()){
         case MotionEvent.ACTION_DOWN: {
           .... 省略....
           startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
         }
   }
 }

public boolean startNestedScroll(int axes, int type) {
    return mChildHelper.startNestedScroll(axes, type);
}
# NestedScrollingChildHelper.java
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
  // 是否有嵌套滚动的 parentView
  if (hasNestedScrollingParent(type)) {
              // Already in progress
       return true;
  }
   // 是否开启了嵌套滚动机制
   if (isNestedScrollingEnabled()) {
     while (p != null) {
       // 调用parentView 的 onStartNestedScroll() 方法 
       if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {

       // 如果返回 true 则再次调用parentView 的onNestedScrollAccepted()方法
         ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
       }
       ... 省略...
     }
}
  // 如果有嵌套滚动的 parentView 就直接调用他的 onStartNestedScroll()方法
  public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
                                            int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
      return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                                                                   nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
      ... 省略....
    }
    return false;
  }

  // 如果 onStartNestedScroll() 返回 true 那么就立即执行 该方法 
  public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
                                            int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
      // First try the NestedScrollingParent2 API
      ((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target,
                                                               nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
      ... 省略....
    }
  }

再来看一眼流程图:

8a14dab68da3d7ec6b47a6aef3dfe290.png

至此,DOWN第一步的事件就传递完成了,第一步聊的详细一些,那么就再来捋一遍流程。

fe25b7ff4cdf40184f14dcae567cf358.png

在TouchEvent.DOWN事件中通过NestedScrollingChildHelper调用NestedScrollingChild#startNestedScroll()方法,那么NestedScrollingChildHelper就会通过么ViewParentCompat调用到 NestedScrollingParent#onStartNestedScroll()上,parentView用来判断是否需要嵌套滚动,如果需要的话,返回true,则立即调用到NestedScrollingParent#onNestedScrollAccepted上完成最初的事件传递。

onTouchEvent#ACTION_MOVE事件

ACTION_MOVE事件和ACTION_DOWN事件原理相同。

# NestedScrollView.java

public boolean onTouchEvent(MotionEvent ev) {
   switch(ev.getActionMasked()){
         case MotionEvent.ACTION_MOVE: {
           .... 省略....
              // 如果父 view 消费了事件,则返回 true
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {

            }
            .... 省略....
              // 将当前消费的和未消费的距离再次传递给 parentView
            dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH, mScrollConsumed);
         }
   }
 }

//代理给 NestedScrollingChildHelper 的同名方法即可
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                       int type) {
  return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}

//代理给 NestedScrollingChildHelper的同名方法即可
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                 int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
  mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                                    offsetInWindow, type, consumed);
}
# NestedScrollingChildHelper.java

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
   // 是否支持嵌套滚动
   if (isNestedScrollingEnabled()) {
       ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
   }
}
# ViewParentCompat.java

 public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed, int type) {
        if (parent instanceof NestedScrollingParent2) {
            ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            ...省略...
        }
    }

通过当前方法,即可吧chlidView的move事件传递给parentView来消费。

来看看流程图:

d67f9177df187ee4f354ba7ed0ef1baf.png

ACTION_UP / ACTION_CANCEL原理和ACTION_DOWN / ACTION_MOVE一样,都是通过ViewParentCompat调用到parentView。

public boolean onTouchEvent(MotionEvent ev) {
  switch(..){
    case MotionEvent.ACTION_UP:
      // 通过 VelocityTracker 与 OverScroller 来实现 fling 事件传递
      final VelocityTracker velocityTracker = mVelocityTracker;
      if (!edgeEffectFling(initialVelocity)
          && !dispatchNestedPreFling(0, -initialVelocity) // 分发事件给parentView,询问 parentView 是否消费
         ) {
        dispatchNestedFling(0, -initialVelocity, true); // 分发事件给 parentView 表示有嵌套滚动事件
        fling(-initialVelocity);  // 如果 parentView 没有消费 fling 事件.则自身消费掉 
      }
      // 传递结束事件(stopNestedScroll)给 parentView
      endDrag();
      break;
    case MotionEvent.ACTION_CANCEL:
      ...省略...
        // 传递结束事件(stopNestedScroll)给 parentView
        endDrag();
      break;
  }
}

private void endDrag() {
  ... 省略 ...
  stopNestedScroll(ViewCompat.TYPE_TOUCH);
}

public void stopNestedScroll(int type) {
  mChildHelper.stopNestedScroll(type);
}

继续往下执行NestedScrollingChildHelper.stopNestedScroll()方法。

# NestedScrollingChildHelper.java

  public void stopNestedScroll(@NestedScrollType int type) {
    ... 
    ViewParentCompat.onStopNestedScroll(parent, mView, type);
}
# ViewParentCompat.java
public static void onStopNestedScroll(ViewParent parent, View target, int type) {
        if (parent instanceof NestedScrollingParent2) {
            ((NestedScrollingParent2) parent).onStopNestedScroll(target, type);
        } 
      ...
}

最终就会调用到parentView的onStopNestedScroll()方法上。看一眼流程图:

dbbef658545e1d2572e6c628c2a953e5.png

tips:这里 fling 是借助的 OverScroller() 就不展开说了,有兴趣的同学可以自主了解一下。

站在设计者的角度思考,为什么要这样设计

就以ACTION_MOVE中childView通过dispatchNestedPreScroll()分发事件给parentView的onNestedPreScroll()来举例。

首先看看这两个方法。

# NestedScrollingChild.java

  /**
          @param dx:水平(x)滚动的距离(以像素为单位)
      @param dy:垂直(y)滚动的距离(以像素为单位)
      @param consumed: 主要用来父容器消费封装,并且通知子容器 x = consumed[0]; y = consumed[1];
      @param offsetInWindow:滑动之前和滑动之后的偏移量 
      return true: 表示父容器消费了事件 
  */
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);
# NestedScrollingParent.java

void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

问题:这里为什么要通过数组传递?

在java中,没有指针的概念,所以就没办法像C一样来操作内存。那么就导致传递一个基本基本数据类型传递给方法,那么到了方法中,就会生成一个新的基本数据类型。

来看一段代码就明白了:

public static class Test {
    int[] mTestInts = new int[2];
    ArrayList<Integer> mIntList = new ArrayList<>(2);
    int mInt = 23;
    Random mRandom = new Random();

    public void test() {
        loadInts(mTestInts);
        loadIntArray(mIntList);
        loadInt(mInt);

        System.out.println("int[] first:"+mTestInts[0]+"\tsecond:"+mTestInts[1]);
        System.out.println("list first:"+mIntList.get(0)+"\tsecond:"+mIntList.get(1));
        System.out.println("mInt:"+mInt);
    }
    public void loadInt(int tempInt){
        tempInt += 52;
    }

    public void loadIntArray(ArrayList<Integer> list) {
        list.add(mRandom.nextInt(10));
        list.add(mRandom.nextInt(10));
    }

    public void loadInts(int[] ints) {
        if(ints instanceof Object){System.out.println("int[] extents Object");}
        ints[0] = mRandom.nextInt(10);
        ints[1] = mRandom.nextInt(10);
    }
}

再来细品一下NestedScrollView#onTouchEvent#ACTION_MOVE的源码:

public boolean onTouch(){
    case ACTION_MOVE:
  ....
    // 分发事件给 parentView,如果 parentView 消费则返回 true
  if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
            ....
                    }
    break:  
}

走进NestedScrollingChildHelper.dispatchNestedPreScroll源码细读。

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
        @Nullable int[] offsetInWindow, @NestedScrollType int type) {
  //如果开启了滑动就执行
    if (isNestedScrollingEnabled()) {
       ...
        if (dx != 0 || dy != 0) {
            ....
              // 如果 consumed  == null 就创建一个空数组返回
            if (consumed == null) {
                consumed = getTempNestedScrollConsumed();
            }
            consumed[0] = 0;
            consumed[1] = 0;
            ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

          // 如果 parentView 没有消费 一点距离,则返回 false
          // 反之消费了则返回 true 
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

private int[] getTempNestedScrollConsumed() {
  if (mTempNestedScrollConsumed == null) {
    mTempNestedScrollConsumed = new int[2];
  }
  return mTempNestedScrollConsumed;
}

通过这段源码得知,consumed非常关键,是证明parentView是否消费,dispatchNestedPreScroll() == true的条件。

tips:这里代码不重要,代码底部会给出,重要的是思路!!有了思路,这些代码迟早闭着眼写出来。

dispatchNestedPreScroll() 和 dispatchNestedScroll() 的区别

  • dispatchNestedPreScroll()只有在parentView消费了事件的时候,并且有嵌套的parentView,才返回 true,证明 parentView 消费了事件。

  • dispatchNestedScroll()则不同,只要有嵌套的parentView就会执行(parentView extents NestedScrollingParent) , 无论parentView是否消费事件

  • 参数也很大不同,dispatchNestedPreScroll()是用来处理 x / y 滑动距离的,dispatchNestedScroll()则是用来处理已经消费和未消费的滑动距离的

  • childView.dispatchNestedPreScroll()会调用到ParentView.onNestedPreScroll()方法

  • childView.dispatchNestedScroll会调用到ParentView.onNestedScroll()方法

ParentView.onStartNestedScroll()中child和taget的区别

2张图搞清楚:

25c9f9e00b2e9e9c86b91372794de754.png

可以很清晰的看出:

  • child 代表嵌套的第一给 view

  • taget 则代表嵌套滑动的 childView

源码位置:

# NestedScrollingChildHelper.java

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
              // 如果有嵌套滚动的 view 就返回 true
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
              // 此时 child == 嵌套滚动的 View,
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
              // 找到嵌套滚动的 View 就立即返回
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

/   实战   /

79217e7b6a6171f434dd607df3a4875d.gif

这个效果非常典型,可以很好地练习NestScrollChildView和NestScollParentView。

通过前面的详细介绍,大家应该对NestScrollView有一定的了解了,那么就直接来看代码了,为了整洁度,我把没必要的log和注释都删了。

ChildNestedScrollView.kt

# ChildNestedScrollView.kt

class ChildNestedScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr), NestedScrollingChild3 {

    private val childHelper by lazy {
        NestedScrollingChildHelper(this).apply { isNestedScrollingEnabled = true }
    }

    // 滚动消耗
    private val mScrollConsumed = IntArray(2)

    // 偏移量
    private val mScrollOffset = IntArray(2)

    private var lastTouchY = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val touchX = event.x.toInt()
        val touchY = event.y.toInt()

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastTouchY = touchY
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
            }
            MotionEvent.ACTION_MOVE -> {
                var tempY = lastTouchY - touchY
                // 分发事件给parent 询问parent是否执行
                // true 表示父view消费了事件
                if (dispatchNestedPreScroll(
                        0,
                        tempY,
                        mScrollConsumed,
                        mScrollOffset,
                        ViewCompat.TYPE_TOUCH
                    )
                ) { // 父亲消费
                    tempY -= mScrollConsumed[1]
                    if (tempY == 0) return true
                } else {
                  // 自己消费
                    scrollBy(0, tempY)
                }
                lastTouchY = touchY
                // true 支持嵌套滚动
               if( dispatchNestedScroll(0,
                    tempY,
                    0,
                    scrollY - measuredHeight,
                    mScrollOffset,
                    ViewCompat.TYPE_TOUCH)){
                   Log.i("szj分发事件","dispatchNestedScroll\t lastTouchY:${lastTouchY}")
               }

            }
            // 抬起/取消
            MotionEvent.ACTION_CANCEL,
            MotionEvent.ACTION_UP -> {
                stopNestedScroll(ViewCompat.TYPE_TOUCH)
            }
        }
        return true
    }

    override fun startNestedScroll(axes: Int, type: Int): Boolean = let {
        Log.i(TAG, "child startNestedScroll axes:$axes type:$type ")
        childHelper.startNestedScroll(axes)
    }

    override fun stopNestedScroll(type: Int) {
        Log.i(TAG, "child stopNestedScroll $type")
        childHelper.stopNestedScroll(type)
    }

    // NestedScrollingChild2
    override fun dispatchNestedScroll(
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        offsetInWindow: IntArray?,
        type: Int
    ): Boolean = let {
        childHelper.dispatchNestedScroll(
            dxConsumed,
            dyConsumed,
            dxUnconsumed,
            dyUnconsumed,
            offsetInWindow,
            type
        )
    }

    override fun dispatchNestedPreScroll(
        dx: Int,
        dy: Int,
        consumed: IntArray?,
        offsetInWindow: IntArray?,
        type: Int
    ): Boolean = let {
        childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
    }

    /*
     * 作者:android 超级兵
     * 创建时间: 4/9/22 3:47 PM
     * TODO  最终xml会调用到这里..添加
     */
    override fun addView(child: View, params: ViewGroup.LayoutParams?) {
        super.addView(child, params)
    }

    @SuppressLint("LongLogTag")
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var tempHeightMeasureSpec = heightMeasureSpec

        val widthSize = MeasureSpec.getSize(widthMeasureSpec)

        // 遍历所有的view 用来测量高度
        children.forEach {
            tempHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                MeasureSpec.getSize(tempHeightMeasureSpec),
                MeasureSpec.UNSPECIFIED
            )

            // 测量子view
            measureChild(it, widthMeasureSpec, tempHeightMeasureSpec)
        }
        setMeasuredDimension(widthSize, children.first().measuredHeight)
    }

    override fun scrollTo(x: Int, y: Int) {
        var tempY = y
        if (tempY < 0) tempY = 0
        super.scrollTo(x, tempY)
    }
}

ParentNestedScrollView.kt

# ParentNestedScrollView.kt

class ParentNestedScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr), NestedScrollingParent3 {

    private val parentHelper by lazy {
        NestedScrollingParentHelper(this)
    }

    // 第一个View
    private val firstView by lazy {
        children.first()
    }

    private var mChildHeight = 0

    @SuppressLint("LongLogTag")
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var tempHeightMeasureSpec = heightMeasureSpec
        mChildHeight = 0
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(tempHeightMeasureSpec)

        children.forEach {
            tempHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.UNSPECIFIED)

            // 测量子view
            measureChild(it, widthMeasureSpec, tempHeightMeasureSpec)
            mChildHeight += it.measuredHeight
        }


        setMeasuredDimension(widthSize, heightSize)
    }

    /*
     * 作者:android 超级兵
     * 创建时间: 4/7/22 4:51 PM
     * TODO  子view调用 startNestedScroll()时候执行
     */
    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean =  true


    /*
     * 作者:android 超级兵
     * 创建时间: 4/7/22 4:52 PM
     * TODO 如果onStartNestedScroll()返回true的话,就会紧接着调用该方法
     *  常用来做一些初始化工作
     */
    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        parentHelper.onNestedScrollAccepted(child, target, axes, type)
    }

    /*
     * 作者:android 超级兵
     * 创建时间: 4/7/22 4:55 PM
     * TODO 当子view调用 stopNestedScroll() 时候调用
     */
    override fun onStopNestedScroll(target: View, type: Int) {
        parentHelper.onStopNestedScroll(target, type)
    }


    /*
     * 作者:android 超级兵
     * 创建时间: 4/7/22 4:45 PM
     * TODO 当子view调用 dispatchNestedPreScroll() 时候调用
     *   tips:在childNestedScrollView.onTouchEvent#ACTION_MOVE:中
     */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int,
                                   consumed: IntArray, type: Int) {
        // (dy > 0 &&  scrollY < firstView.height) 如果 向上滑动 并且 当前滑动的距离 < 第一个View的高 说明还有滑动空间
        // (dy < 0 && scrollY > 0) 如果当前向下滑动 并且还有滑动空间
        if ((dy > 0 && scrollY < firstView.height) || (dy < 0 && scrollY > 0)) {
            // 父容器消费了多少通知子view
            consumed[1] = dy // 关键代码!!parentView正在消费事件,并且通知 childView
            scrollBy(0, dy)
        }
    }

    override fun scrollTo(x: Int, y: Int) {
        var tempY = y
        if (tempY < 0) tempY = 0
        super.scrollTo(x, tempY)
    }
}

完整代码地址:

https://gitee.com/lanyangyangzzz/material-project

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

更多 ViewBinding 的封装思路

你知道Edge这种项目是如何进行版本管理的吗?

欢迎关注我的公众号

学习技术或投稿

ca4ecad5b1305c726bb6cac7bd584861.png

857004de5acff77584b261841aa878ea.png

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值