android MD进阶[四] NestedScrollView 从源码到实战..

android MD进阶[四] NestedScrollView 从源码到实战..,第1张

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[ 2022-4-21]

android MD进阶[四] NestedScrollView 从源码到实战..
  • 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[ 2022-4-21]
  • NestedScrollingChild
    • NestedScrollingChild 和 NestedScrollingChild2的区别:
    • NestedScrollingChild3 和 NestedScrollingChild2 的区别:
  • NestedScrollingParent
  • NestedScrollView源码分析
    • 我通过分析 NestedScrollView 能够知道那些内容:
      • 1.为什么NestedScrollView只能添加 1个 ChildView
      • 2.NestedScrollView的事件分发流程
      • 3.站在设计者的角度思考,为什么要这样设计
      • 4. dispatchNestedPreScroll() 和 dispatchNestedScroll() 的区别
      • 5.ParentView.onStartNestedScroll() child 和 taget 的区别
  • 实战

前言:相信大家在开发过程中经常会遇到嵌套滚动的场景,最常见的莫过于 nestedScrollView,前段时间一直在搞别的,把 md 系列都断更了,从现在开始慢慢的都补起来!

NestedScrollView比较特殊 ,要想看懂他的源码,必须得了解2个东西,NestedScrollingChildNestedScrollingParent,首先就从这两个接口的参数聊起~

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的区别:

可以看出,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 的区别:

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

NestedScrollingParent
public interface NestedScrollingParent {

  /**
  	当NestedScrollingChildHelper.startNestedScroll()时候执行,用来接受ChildView#onTouchEvent#DOWN事件
  	@param child: 如果只有嵌套一层 那么 child = target
  				   
  							
  								
  									
  								
  							
  						
  					如果格式为这样,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 能够知道那些内容: 1.为什么NestedScrollView只能添加 1个 ChildView

先来捋一遍 setContentView流程:

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

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

回到 NestedScrollView 的源码中…

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

ScrollView can host only one direct child

2.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) {
      ... 省略....
    }
  }

再来看一眼流程图:

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

在 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来消费

来看看流程图:

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() 方法上.

看一眼流程图:

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

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

就以 ACTION_MOVE childView通过dispatchNestedPreScroll()分发事件给parentViewonNestedPreScroll()来举例

首先看看这两个方法

# 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);
    }
}

来看一眼运行的效果图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-koZyw3oe-1649590679179)(/Users/shizhenjiang/Desktop/博客/nestedScrollView/ints.gif)]

结果很明显,我就不在多啰嗦了…

所以这里到底有什么用? 给我看这个有什么用?

在来细品一下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 的条件

来两张图看看它到底是否和源码表达的一样

parentView消费了事件parentView 没有消费事件

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

4. 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()方法
5.ParentView.onStartNestedScroll() child 和 taget 的区别

2张图搞清楚:

图 1图 2

可以很清晰的看出:

  • 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;
}
实战

先来看一眼效果:

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

通过前面的详细介绍,大家应该对 NestScrollView 有一定的了解了,

那么就直接来看代码了:

tips:为了整洁度,我把没必要的 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)
    }
}

完整代码

下期分享: CoordinatorLayout源码分析与实战!

分析源码和写博客都很费精力,请给存粹的分享者一波关注,感谢!

猜你喜欢:

  • android material design 风格组件(MaterialButton,MaterialButtonToggleGroup,Chip,ChipGroup)大汇总(一).
  • android MD风格组件(TextInputLayout AutoCompleteTextView MaterialButton SwitchMaterial MaterialRadio)(二)
  • android MD风格组件(BottomNavigationView,配合lottie使用) (三)

参考链接:

  • 链接 1

  • 链接 2

原创不易,您的点赞就是对我最大的支持!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/722512.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-26
下一篇 2022-04-26

发表评论

登录后才能评论

评论列表(0条)

保存