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

Posted android超级兵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android MD进阶[四] NestedScrollView 从源码到实战..相关的知识,希望对你有一定的参考价值。

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

前言:相信大家在开发过程中经常会遇到嵌套滚动的场景,最常见的莫过于 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
  				   <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 能够知道那些内容:

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) 
        ifAndroid进阶 四 一个APP引发的思索之ArrayList的add总是添加相同的值

Android NestedScrolling解决滑动冲突问题 - 相关接口

Android NestedScrolling解决滑动冲突问题 - 相关接口

我的渲染技术进阶之旅如何在Windows系统编译Filament的android版本程序?

我的渲染技术进阶之旅如何在Windows系统编译Filament的android版本程序?

我的渲染技术进阶之旅如何在Windows系统编译Filament的android版本程序?