澳门新葡亰平台游戏网站完全理解nestedScrolling

澳门新葡亰平台游戏网站 8

从 Android 5.0 Lollipop 开始提供一套 API
来支持嵌入的滑动效果。同样在最新的 Support V4
包中也提供了前向的兼容。有了嵌入滑动机制,就能实现很多很复杂的滑动效果。在
Android Design Support 库中非常总要的
CoordinatorLayout
组件就是使用了这套机制,实现了 Toolbar 的收起和展开功能,如下图所示:

CoordinatorLayout作为协调布局,而真正实现功能的部分在于Behavior,所以我打算将这两地方都捎带说说,若有意见请及时提出帮助我改正

一、概述

Android在support.v4
包中为大家提供了两个非常神奇的类:
NestedScrollingParent
NestedScrollingChild

如果你从未听说过这两个类,没关系,听我慢慢介绍,你就明白这两个类可以用来干嘛了。相信大家都见识过或者使用过CoordinatorLayout
,通过这个类可以非常便利的帮助我们完成一些炫丽的效果,例如下面这样的:

澳门新葡亰平台游戏网站 1

20160814125614746.gif

这样的效果就非常适合使用NestedScrolling机制去完成,并且CoordinatorLayout背后其实也是利用着这套机制,So,我相信你已经明白这套机制可以用来干嘛了。

但是,我相信你还有个问题

这个机制相比传统的自定义ViewGroup事件分发处理有什么优越的地方吗?
恩,我们简单分析下:

按照上图:

假设我们按照传统的事件分发去理解,首先我们滑动的是下面的内容区域,而移动却是外部的ViewGroup在移动,所以按照传统的方式,肯定是外部的Parent拦截了内部的Child的事件;但是,上述效果图,当Parent滑动到一定程度时,Child又开始滑动了,中间整个过程是没有间断的。从正常的事件分发(不手动调用分发事件,不手动去发出事件)角度去做是不可能的,因为当Parent拦截之后,是没有办法再把事件交给Child的,事件分发,对于拦截,相当于一锤子买卖,只要拦截了,当前手势接下来的事件都会交给Parent(拦截者)来处理。

但是NestedScrolling机制来处理这个事情就很好办,所以对这个机制进行深入学习,一来有助于我们编写嵌套滑动时一些特殊的效果;二来是我为了对CoordinatorLayout做分析的铺垫~~~

澳门新葡亰平台游戏网站 2

Behavior的初始化

Behavior是CoordinatorLayout内部静态抽象类,它是一种新的view关系描述,即依赖关系。一般我们都是继承这个类去完成自己的自定义功能

之前我们提及Behavior可以通过注解或者layout_behavior来声明,如果你是通过xml来初始化,那么在CoordinatorLayout初始化的时候就完成了

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    LayoutParams(Context context, AttributeSet attrs) {
        mBehaviorResolved = a.hasValue( R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
        if (mBehaviorResolved) {
            mBehavior = parseBehavior(context, attrs, a.getString(
                R.styleable.CoordinatorLayout_Layout_layout_behavior));}
    }
}

如果你是使用注解进行初始化,那么他在onMeasure的时候通过prepareChildren才进行初始化,注意看setBehavior这里。所以xml里初始化优先级高。xml内指定的话,是在inflate的时候对mBehavior赋值;注解里指定的话,是在onMeasure内赋值,稍有不同。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ....
}

LayoutParams getResolvedLayoutParams(View child) {
    final LayoutParams result = (LayoutParams) child.getLayoutParams();
    if (!result.mBehaviorResolved) {
        Class<?> childClass = child.getClass();
        DefaultBehavior defaultBehavior = null;
        while (childClass != null &&
                (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
            childClass = childClass.getSuperclass();
        }
        if (defaultBehavior != null) {
            try {
                result.setBehavior(defaultBehavior.value().newInstance());
            } catch (Exception e) {
                Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
                        " could not be instantiated. Did you forget a default constructor?", e);
            }
        }
        result.mBehaviorResolved = true;
    }
    return result;
}

前面我们提及反射初始化Behavior的,在这个parseBehavior里面就能看到

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

还有一个需要注意的地方,我们看到反射的方法是2个参数的构造方法

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
        Context.class,
        AttributeSet.class
};

所以我们在自定义Behavior的时候,一定要去重写

二. 实现

NestedScrolling机制能够让父View和子View在滚动式进行配合,其基本流程如下:

  1. 当子view开始滚动之前,可以通知父View,让其先于自己进行滚动;
  2. 子View自己进行滚动;
  3. 子view滚动之后,还可以通知父view继续滚动。
    而要实现这样的交互机制,首先父view要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口,在这套机制中子View是发起者,父view是接受回调并做出响应的。

NestedScrolling提供了一套父 View 和子 View
滑动交互机制。要完成这样的交互,父 View 需要实现 NestedScrollingParent
接口,而子 View 需要实现 NestedScrollingChild 接口。

NestedScrolling概念

其实想说一下为什么叫嵌套滑动,之前我们老是提及这个概念。CoordinatorLayout本身是不能动的,但是一旦其中包含了具备NestedScrolling功能的滚动视图,那就不一样了。它在滑动过程中会对Behavior产生影响,进而可以通过动画或者View之间的关联关系进行改变。这里,就是有嵌套这么一层关系

之前那种TouchEvent形式的滑动方式,一旦子View拦截了事件,除非重新进行一次事件传递,不然父View是拿不到事件的。而NestedScrolling很好的解决了这个问题

在阅读源码的时候,请着重关注这4个类

  1. NestedScrollingChild:
    如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子
    View,就必须实现本接口
  2. NestedScrollingParent:
    作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现
    NestedScrollingParent接口,这个接口方法和
    NestedScrollingChild大致有一一对应的关系
  3. NestedScrollingChildHelper
    实现好了 Child 和 Parent 交互的逻辑
  4. NestedScrollingParentHelper
    实现好了 Child 和 Parent 交互的逻辑
以下是几个关键的类和接口:
//主要接口
NestedScrollingChild
NestedScrollingParent

//帮助类
NestedScrollingChildHelper
NestedScrollingParentHelper 

一些新的系统View已经帮我们实现了以上两个接口,也就是说他们是支持NestedScrolling,例如:
NestedScrollView已经实现了NestedScrollingChild和NestedScrollingParent两个接口
RecycleView已经实现了NestedScrollingChild
CoordinatorLayout实现了NestedScrollingParent

NestedScrollingChild接口:

//开始、停止嵌套滚动

public boolean startNestedScroll(int axes); public void stopNestedScroll();

//触摸滚动相关

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

//惯性滚动相关 public boolean dispatchNestedPreFling(float velocityX, float velocityY);

public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); 

public boolean startNestedScroll(int axes);

开启嵌套滚动流程(实际上是进行了一些嵌套滚动前准备工作)。
当找到了能够配合当前子view进行嵌套滚动的父view时,返回值为true(Returns:true
if a cooperative parent was found and nested scrolling has been enabled
for the current gesture)。
这个看源码就可以了,源码也是很简单的。

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。
此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
第一第二个参数为输入参数,即常规的函数参数,调用函数的时候我们需要为其传递确切的值。而第三第四个参数为输出参数,调用函数时我们只需要传递容器(在这里就是两个数组),在调用结束后,我们就可以从容器中获取函数输出的值。
如果parent消费了一部分或全部距离,则此方法返回true。

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

在子view自己进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。
前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。
返回值:(翻译出来可能有歧义,直接放原文)true if the event was
dispatched, false if it could not be dispatched.

public void stopNestedScroll();

最后,stopNestedScroll()方法与startNestedScroll(int
axes)对应,用于结束嵌套滚动流程;而惯性滚动相关的两个方法与触摸滚动相关的两个方法类似,这里不再赘述。

NestedScrollingParent接口概述:

//当开启、停止嵌套滚动时被调用

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

public void onStopNestedScroll(View target);

//当触摸嵌套滚动时被调用

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

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

//当惯性嵌套滚动时被调用

public boolean onNestedPreFling(View target, float velocityX, float velocityY);

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); 

从命名可以看出,这几个都是回调方法。当调用NestedScrollingChild中的方法时,NestedScrollingParent中与之相对应的方法就会被回调。方法之间的具体对应关系如下:

澳门新葡亰平台游戏网站 3

Paste_Image.png

从上面的接口还有方法我们可以得出一些简单的流程

  1. 调用child的startNestedScroll()来发起嵌套滑动流程(实质上是寻找能够配合child进行嵌套滚动的parent)。parent的onStartNestedScroll()会被调用,若此方法返回true,则OnNestScrollAccepted()也会被调用。
  2. chuld每次滚动前,可以先询问parent是否要滚动,即调用dispatchNestedScroll(),这时可以回调到parent的OnNestedPreScroll(),parent可以在这个回调中先于child滚动。
  3. dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。

实现 NestedScrollingChild

首先来说NestedScrollingChild。如果你有一个可以滑动的
View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View
中,包含一个 NestedScrollingChildHelper
辅助类。NestedScrollingChild接口的实现,基本上就是调用本 Helper
类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent
交互的逻辑。原来的 View 的处理 Touch
事件,并实现滑动的逻辑大体上不需要改变。

需要做的就是,如果要准备开始滑动了,需要告诉
Parent,你要准备进入滑动状态了,调用startNestedScroll()。你在滑动之前,先问一下你的
Parent
是否需要滑动,也就是调用dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent
是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll()。

以上是一些基本原理,有了上面的基本思路,可以参考这篇
文章
,这里面有原理的详细解析。如果还是不清楚,
这里
有对应的代码可以参考。

NestedScrolling滑动机制流程

完整的事件流程大致是这样的:
滑动开始的调用startNestedScroll(),Parent收到onStartNestedScroll()回调,决定是否需要配合Child一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()。每次滑动前,Child
先询问Parent是否需要滑动,即
dispatchNestedPreScroll(),这就回调到Parent的onNestedPreScroll(),Parent可以在这个回调中“劫持”掉Child的滑动,也就是先于Child滑动。Child滑动以后,会调用onNestedScroll(),回调到Parent的onNestedScroll()。最后滑动结束,调用
onStopNestedScroll()表示本次处理结束。

澳门新葡亰平台游戏网站 4

NestedScrollingChild与NestedScrollingChildHelper的交互流程

澳门新葡亰平台游戏网站 5

NestedScrollingChildHelper与ViewParentCompat的交互流程

澳门新葡亰平台游戏网站 6

ViewParentCompat与CoordinatorLayout的交互流程

澳门新葡亰平台游戏网站 7

CoordinatorLayout与Behavior的交互流程

自定义NestedScrolling控件

澳门新葡亰平台游戏网站 8

2017021514065626.gif

先看一下布局文件activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

 >



 <com.qianmo.mynestedscrolling.view.MyNestedScrollParent

  android:layout_width="match_parent"

  android:layout_height="match_parent"

  android:layout_alignParentLeft="true"

  android:layout_alignParentStart="true"

  android:layout_alignParentTop="true"

  android:orientation="vertical">



  <ImageView

   android:layout_width="match_parent"

   android:layout_height="wrap_content"

   android:src="@mipmap/ic_launcher"/>



  <TextView

   android:layout_width="match_parent"

   android:layout_height="wrap_content"

   android:background="#f0f"

   android:text="上面的图片会被隐藏,而这个文字不会被隐藏"/>



  <com.qianmo.mynestedscrolling.view.MyNestedScrollChild

   android:layout_width="match_parent"

   android:layout_height="wrap_content"

   android:orientation="vertical">



   <TextView

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:text="123n456n789n111n222n333n444n555n666n777n888n999n14n12n13n44n55n66n77n88n99n11n22n33n44n55n66n77n88n99n77n88n88n8n88n88n"

    android:textColor="#f0f"

    android:textSize="20sp"/>

  </com.qianmo.mynestedscrolling.view.MyNestedScrollChild>

 </com.qianmo.mynestedscrolling.view.MyNestedScrollParent>

</RelativeLayout> 

布局文件只是简单的嵌套,MyNestedScrollParent继承Linearlayout,并实现NestedScrollingParent接口,MyNestedScrollChild同理,先来看看MyNestedScrollChild这个类吧。

MyNestedScrollChild.java

package com.qianmo.mynestedscrolling.view;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;

public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild {

 private NestedScrollingChildHelper mNestedScrollingChildHelper;

 private final int[] offset = new int[2]; //偏移量

 private final int[] consumed = new int[2]; //消费

 private int lastY;

 private int showHeight; 

 public MyNestedScrollChild(Context context) {

  super(context);

 }

 public MyNestedScrollChild(Context context, AttributeSet attrs) {

  super(context, attrs);

 }
 @Override

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  //第一次测量,因为布局文件中高度是wrap_content,因此测量模式为atmost,即高度不超过父控件的剩余空间

  super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  showHeight = getMeasuredHeight();



  //第二次测量,对稿哦度没有任何限制,那么测量出来的就是完全展示内容所需要的高度

  heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

  super.onMeasure(widthMeasureSpec, heightMeasureSpec);

 }
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)

 @Override

 public boolean onTouchEvent(MotionEvent event) {

  switch (event.getAction()) {

   //按下

   case MotionEvent.ACTION_DOWN:

    lastY = (int) event.getRawY();

    break;

   //移动

   case MotionEvent.ACTION_MOVE:

    int y = (int) (event.getRawY());

    int dy = y - lastY;

    lastY = y;

    if (startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL)

      && dispatchNestedPreScroll(0, dy, consumed, offset)) //如果找到了支持嵌套滑动的父类,父类进行了一系列的滑动

    {
     //获取滑动距离
     int remain = dy - consumed[1];
      if (remain != 0) {
      scrollBy(0, -remain);
      }
    } else {
     scrollBy(0, -dy);
    }
    break;
  }



  return true;

 }

 //限制滚动范围

 @Override

 public void scrollTo(int x, int y) {

  int maxY = getMeasuredHeight() - showHeight;

  if (y > maxY) {

   y = maxY;
  }
  if (y < 0) {
   y = 0;
  }
  super.scrollTo(x, y);
 }
 //初始化helper对象

 private NestedScrollingChildHelper getScrollingChildHelper() {

  if (mNestedScrollingChildHelper == null) {

   mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);

   mNestedScrollingChildHelper.setNestedScrollingEnabled(true);

  }

  return mNestedScrollingChildHelper;

 }
 //实现一下接口

 @Override

 public void setNestedScrollingEnabled(boolean enabled) {

  getScrollingChildHelper().setNestedScrollingEnabled(enabled);

 }
 @Override

 public boolean isNestedScrollingEnabled() {

  return getScrollingChildHelper().isNestedScrollingEnabled();

 }

 @Override

 public boolean startNestedScroll(int axes) {

  return getScrollingChildHelper().startNestedScroll(axes);

 }

 @Override

 public void stopNestedScroll() {

  getScrollingChildHelper().stopNestedScroll();

 }

 @Override

 public boolean hasNestedScrollingParent() {

  return getScrollingChildHelper().hasNestedScrollingParent();

 }
 @Override

 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {

  return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);

 }
 @Override

 public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {

  return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);

 }
 @Override

 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {

  return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);

 }

 @Override

 public boolean dispatchNestedPreFling(float velocityX, float velocityY) {

  return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);

 }
}

主要是在OnTouchEvent中先后调用了startNestedScroll()和dispatchNestedPreScroll()方法,在借助helper来完成NestedScrollingParent接口方法
MyNestedScrollParent.java

package com.qianmo.mynestedscrolling.view;
import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**

 * Created by wangjitao on 2017/2/14 0014.
 * 嵌套滑动机制父View
 */
public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {

 private ImageView img;

 private TextView tv;

 private MyNestedScrollChild myNestedScrollChild;

 private NestedScrollingParentHelper mNestedScrollingParentHelper;

 private int imgHeight;

 private int tvHeight;



 public MyNestedScrollParent(Context context) {

  super(context);

 }



 public MyNestedScrollParent(Context context, AttributeSet attrs) {

  super(context, attrs);

  init();

 }



 private void init() {

  mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

 }



 //获取子view

 @Override

 protected void onFinishInflate() {

  img = (ImageView) getChildAt(0);

  tv = (TextView) getChildAt(1);

  myNestedScrollChild = (MyNestedScrollChild) getChildAt(2);

  img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

   @Override

   public void onGlobalLayout() {

    if (imgHeight <= 0) {

     imgHeight = img.getMeasuredHeight();

    }

   }

  });

  tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

   @Override

   public void onGlobalLayout() {

    if (tvHeight <= 0) {

     tvHeight = tv.getMeasuredHeight();

    }

   }

  });

 }




 //在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动

 @Override

 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {

  if (target instanceof MyNestedScrollChild) {

   return true;

  }

  return false;

 }

 @Override

 public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {

  mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);

 }
 @Override

 public void onStopNestedScroll(View target) {

  mNestedScrollingParentHelper.onStopNestedScroll(target);

 }

 //先于child滚动

 //前3个为输入参数,最后一个是输出参数

 @Override

 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

  if (showImg(dy) || hideImg(dy)) {//如果需要显示或隐藏图片,即需要自己(parent)滚动

   scrollBy(0, -dy);//滚动

   consumed[1] = dy;//告诉child我消费了多少

  }

 }

 //后于child滚动

 @Override

 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
 }


 //返回值:是否消费了fling

 @Override

 public boolean onNestedPreFling(View target, float velocityX, float velocityY) {

  return false;

 }

 //返回值:是否消费了fling

 @Override

 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {

  return false;

 }
 @Override

 public int getNestedScrollAxes() {

  return mNestedScrollingParentHelper.getNestedScrollAxes();

 }
 //下拉的时候是否要向下滚动以显示图片

 public boolean showImg(int dy) {
  if (dy > 0) {
   if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) {
    return true;
   }
  }
  return false;

 }

 //上拉的时候,是否要向上滚动,隐藏图片

 public boolean hideImg(int dy) {

  if (dy < 0) {

   if (getScrollY() < imgHeight) {

    return true;

   }

  }

  return false;

 }


 //scrollBy内部会调用scrollTo
 //限制滚动范围
 @Override
 public void scrollTo(int x, int y) {
  if (y < 0) {
   y = 0;
  }
  if (y > imgHeight) {
   y = imgHeight;
  }
  super.scrollTo(x, y);
 } 
} 

MyNestedScrollParent主要是实现一下功能:

  1. 在onStartNestedScroll()中判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
  2. 在onNestedPreScroll()中获取需要滚动的距离,根据情况决定自己是否要进行滚动,最后还要将自己滚动消费掉的距离存储在consumed数组中回传给child

就这样基本实现了,很简单有没有.

实现 NestedScrollingParent

作为一个可以嵌入 NestedScrollingChild 的父
View,需要实现NestedScrollingParent,这个接口方法和NestedScrollingChild大致有一一对应的关系。同样,也有一个
NestedScrollingParentHelper 辅助类来默默的帮助你实现和 Child
交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。

从上面的 Child 分析可知,滑动开始的调用startNestedScroll(),Parent
收到onStartNestedScroll()回调,决定是否需要配合 Child
一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()。

每次滑动前,Child 先询问 Parent
是否需要滑动,即dispatchNestedPreScroll(),这就回调到 Parent
的onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child
的滑动,也就是先于 Child 滑动。

Child 滑动以后,会调用onNestedScroll(),回调到 Parent
的onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是
后于 Child 滑动。

最后,滑动结束,调用onStopNestedScroll()表示本次处理结束。

其实,除了上面的 Scroll 相关的调用和回调,还有 Fling
相关的调用和回调,处理逻辑基本一致。

主要回调方法介绍

  • onStartNestedScroll

在NestedScrollView的ACTION_DOWN事件中开始流程

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);

NestedScrollingChildHelper里循环查找直到找出CoordinatorLayout,继续发送

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

ViewParentCompat里面,parent只要实现了onStartNestedScroll就可以继续流程,这里也是说添加Behavior的控件必须直接从属于CoordinatorLayout,否则没有效果

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    try {
        return parent.onStartNestedScroll(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStartNestedScroll", e);
        return false;
    }
}

CoordinatorLayout循环通知所有第一层子视图中的Behavior

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;
            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}

它的返回值,决定了NestedScrollingChildHelper.onStartNestedScroll是不是要继续遍历,如果我们的Behavior对这个滑动感兴趣,就返回true,它的遍历就会结束掉。

  • onNestedPreScroll

在ACTION_MOVE中进行触发传递,注意这边的deltaY是已经计算好的偏移量,deltaY>0就是往上滑动,反之往下滑动

final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
    deltaY -= mScrollConsumed[1];
    vtev.offsetLocation(0, mScrollOffset[1]);
    mNestedYOffset += mScrollOffset[1];
}

其实这边所有Behavior接收流程都是一样的,主要看看AppBarLayout对onNestedPreScroll的处理以便于我们后续自定义Behavior的实现。这里的dy就是刚才说的偏移量,target就是发起者NestedScrollView。consumed数组是由xy组成,AppBarLayout执行完成之后存储其本次垂直方向的滚动值。这里scroll方法会将AppBarLayout的移动范围固定在0-AppBarLayout高度这2个值范围内执行滚动操作,如果在范围外的话,AppBarLayout就不执行滚动操作,consumed[1]的值也为0

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
        View target, int dx, int dy, int[] consumed) {
    if (dy != 0 && !mSkipNestedPreScroll) {
        int min, max;
        if (dy < 0) {
            // We're scrolling down
            min = -child.getTotalScrollRange();
            max = min + child.getDownNestedPreScrollRange();
        } else {
            // We're scrolling up
            min = -child.getUpNestedPreScrollRange();
            max = 0;
        }
        consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
    }
}

只要你记得dy是已经处理好的偏移量并且方向不要搞错就行了。这个函数一般在滚动前调用。

  • onNestedScroll

这个实际上是NestedScrollingChild自身改变的回调,看看之前dispatchNestedPreScroll触发之后的部分有一句这个

deltaY -= mScrollConsumed[1];

刚才也说了AppBarLayout在不超过滚动范围的时候,consumed[1]为实际Y方向滚动量,反之则为0,也就是在滚够了的情况下才会调用dispatchNestedScroll

if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
    mIsBeingDragged = true;
    if (deltaY > 0) {
        deltaY -= mTouchSlop;
    } else {
        deltaY += mTouchSlop;
    }
}

再看看源码,使用overScrollByCompat发生了自身的滚动,所以两次滚动之间的值就是scrolledDeltaY,作为已消费的值。未消费部分unconsumedY就是手指之间的距离减去滚动值之差。其实这个也好理解,当这个NestedScrollView滚到最底部的时候滚不动了,那么它的消费值就是0,未消费值就是手指之间的距离

if (mIsBeingDragged) {
    // Scroll to follow the motion event
    mLastMotionY = y - mScrollOffset[1];
    final int oldY = getScrollY();
    final int range = getScrollRange();
    final int overscrollMode = getOverScrollMode();
    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
            0, true) && !hasNestedScrollingParent()) {
        mVelocityTracker.clear();
    }
    final int scrolledDeltaY = getScrollY() - oldY;
    final int unconsumedY = deltaY - scrolledDeltaY;
    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset))
    .........
}

其实我不知道什么情况下unconsumedY是负数,AppBarLayout倒是处理了这个情况。这个函数一般在scroll后调用。

总之滑动过程为AppBarlayout先滑,NestedScrollView再滑

  • onNestedPreFlingonNestedFling
    这个其实与onNestedPreScroll,onNestedScroll之间的关系差不多,我就不多说了

  • onStopNestedScroll
    一切都结束的时候,执行这个方法

  • onDependentViewChanged
    layoutDependsOnonDependentViewRemoved

layoutDependsOn就是用来告诉NestedScrollingParent我们依赖的是哪个View。除了滚动事件会被处理以外,这个View的大小、位置等变化也一样可以通过回调方法进行通知,通知是通过onDependentViewChanged回调告诉Behavior的

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    // We depend on any AppBarLayouts
    return dependency instanceof AppBarLayout;
}

看看源码,在onAttachedToWindow中我们看到了ViewTreeObserver的身影,那么view的各种状态变化都会被他抓到

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors();
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
    ....
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

这里有一个mNeedsPreDrawListener,它是什么情况变成true的?原来是ensurePreDrawListener这个方法里面判断了只要它有依赖关系,就可以添加监听。ensurePreDrawListener在刚才所说的prepareChildren之后调用,符合逻辑。

void ensurePreDrawListener() {
    boolean hasDependencies = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        if (hasDependencies(child)) {
            hasDependencies = true;
            break;
        }
    }
    if (hasDependencies != mNeedsPreDrawListener) {
        if (hasDependencies) {
            addPreDrawListener();
        } else {
            removePreDrawListener();
        }
    }
}

回头看看prepareChildren方法,存储了全部被依赖的子View

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();
    for (int i = 0, count = getChildCount(); i < count; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = getResolvedLayoutParams(view);
        lp.findAnchorView(this, view);
        mChildDag.addNode(view);
        // Now iterate again over the other children, adding any dependencies to the graph
        for (int j = 0; j < count; j++) {
            if (j == i) {
                continue;
            }
            final View other = getChildAt(j);
            final LayoutParams otherLp = getResolvedLayoutParams(other);
            if (otherLp.dependsOn(this, other, view)) {
                if (!mChildDag.contains(other)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(view, other);
            }
        }
    }
    // Finally add the sorted graph list to our list
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    // We also need to reverse the result since we want the start of the list to contain
    // Views which have no dependencies, then dependent views after that
    Collections.reverse(mDependencySortedChildren);}

再来看看onChildViewsChanged方法,循环遍历所有Child,
将每个子View都使用layoutDependsOn来比较一下,
确保所有互相依赖的子View都可以联动起来,如果是依赖关系,再调用onDependentViewChanged。这里checkChild是待检查的View,也就是我们添加Behavior的那个View,child就是被checkChild所依赖的View

....
for (int j = i + 1; j < childCount; j++) {
    final View checkChild = mDependencySortedChildren.get(j);
    final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
    final Behavior b = checkLp.getBehavior();
    if (b != null && b.layoutDependsOn(this, checkChild, child)) {
        if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
            checkLp.resetChangedAfterNestedScroll();
            continue;
        }
        final boolean handled;
        switch (type) {
            case EVENT_VIEW_REMOVED:
                // EVENT_VIEW_REMOVED means that we need to dispatch
                // onDependentViewRemoved() instead
                b.onDependentViewRemoved(this, checkChild, child);
                handled = true;
                break;
            default:
                // Otherwise we dispatch onDependentViewChanged()
                handled = b.onDependentViewChanged(this, checkChild, child);
                break;
        }
        if (type == EVENT_NESTED_SCROLL) {
            // If this is from a nested scroll, set the flag so that we may skip
            // any resulting onPreDraw dispatch (if needed)
            checkLp.setChangedAfterNestedScroll(handled);
        }
    }
}
....

最后我们就来解决上一篇文章中那个思考题,为什么NestedScrollView下面会有一截在屏幕外,这是因为他依赖于AppBarLayout,否则他们的顶点应该在一个位置

private void layoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect parent = mTempRect1;
    parent.set(getPaddingLeft() + lp.leftMargin,
            getPaddingTop() + lp.topMargin,
            getWidth() - getPaddingRight() - lp.rightMargin,
            getHeight() - getPaddingBottom() - lp.bottomMargin);
    if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
            && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match.
        parent.left += mLastInsets.getSystemWindowInsetLeft();
        parent.top += mLastInsets.getSystemWindowInsetTop();
        parent.right -= mLastInsets.getSystemWindowInsetRight();
        parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
    }
    final Rect out = mTempRect2;
    GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
            child.getMeasuredHeight(), parent, out, layoutDirection);
    child.layout(out.left, out.top, out.right, out.bottom);
}

关于onLayout方面的问题,可以通过onLayoutChild这个方法来细细研究

public void onLayoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.checkAnchorChanged()) {
        throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                + " measurement begins before layout is complete.");
    }
    if (lp.mAnchorView != null) {
        layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
    } else if (lp.keyline >= 0) {
        layoutChildWithKeyline(child, lp.keyline, layoutDirection);
    } else {
        layoutChild(child, layoutDirection);
    }
}

onDependentViewRemoved就是移除View后进行调用,想象一下Snackbar与FloatingActionButton的使用场景就可以理解

实战

有了这一套官方的嵌套滑动的解决方案,打算把我的 FlyRefresh
的滑动和下来部分用 NestedScrolling
来实现。我在这篇博客中讲了,之前是通过在PullHeaderLayout的dispatchTouchEvent()中小心处理
Touch 事件来实现的。现在回想起来,这种方法相对复杂,需要清楚知道 Parent
和 Child 的滑动状态,这就导致了,只能支持有限的 Child
类型,例如当时只支持 ListView 和 RecyclerView,为了支持更多的类型,还定义了一个IScrollHandler接口来支持。

让 FlyRefresh
实现NestedScrollingParent,就可以支持所有的NestedScrollingChild作为FlyRefreshLayout的子
View。另外,因为CoordinatorLayout是如此的重要,大部分的 App
都需要使用它作为顶层的 Layout,为了让FlyRefreshLayout能够在
CoordinatorLayout
也能使用,所以我还打算同时实现NestedScrollingChild接口。关键实现代码如下:

public class PullHeaderLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild {

    private final int[] mScrollOffset = new int[2];
    private final int[] mScrollConsumed = new int[2];
    private final NestedScrollingParentHelper mParentHelper;
    private final NestedScrollingChildHelper mChildHelper;
    ...

    // NestedScrollingChild

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    // NestedScrollingParent
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

    @Override
    public void onStopNestedScroll(View target) {
        stopNestedScroll();
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                               int dyUnconsumed) {
        final int myConsumed = moveBy(dyUnconsumed);
        final int myUnconsumed = dyUnconsumed - myConsumed;
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0 && mHeaderController.canScrollUp()) {
            final int delta = moveBy(dy);
            consumed[0] = 0;
            consumed[1] = delta;
            //dispatchNestedScroll(0, myConsumed, 0, consumed[1], null);
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (!consumed) {
            flingWithNestedDispatch((int) velocityY);
            return true;
        }
        return false;
    }

    private boolean flingWithNestedDispatch(int velocityY) {
        final boolean canFling = (mHeaderController.canScrollUp() && velocityY > 0) ||
                (mHeaderController.canScrollDown() && velocityY < 0);
        if (!dispatchNestedPreFling(0, velocityY)) {
            dispatchNestedFling(0, velocityY, canFling);
            if (canFling) {
                fling(velocityY);
            }
        }
        return canFling;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return flingWithNestedDispatch((int) velocityY);
    }

    @Override
    public int getNestedScrollAxes() {
        return mParentHelper.getNestedScrollAxes();
    }

    // Touch event hanlder

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        MotionEvent vtev = MotionEvent.obtain(ev);
        final int actionMasked = MotionEventCompat.getActionMasked(ev);
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);

        switch (actionMasked) {
            ...
            case MotionEvent.ACTION_MOVE:
                ...
                final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int scrolledDeltaY = moveBy(deltaY);
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    }
                }
                break;
            ...
        }
        ...
        return true;
    }

    ...
}

完整的修改,可以看这个
commit
。整个修改下来,代码减少了不少,而且更加整洁了。

参考链接

深入理解CoordinatorLayout(草稿)
Android
嵌套滑动机制(NestedScrolling)
源码看CoordinatorLayout.Behavior原理
android.support.design 学习笔记
1
【译】Nested Scrolling With CoordinatorLayout On
Android

总结

总体来说, NestedScroll
初看起来有些让人费解,但是真的理解以后,就发现这种设计的优秀之处。把滑动整体封装起来,通过
Helper 来实现 Child 和 Parent 之间的连接和交互。通过接口来回调,实现了
Child 和 Parent 的逻辑独立。

Android 5.0的大部分可以滑动的控件都支持了 NestScrolling 接口,最新的
Support V4
中也一样,相信以后越来越多的第三方库都会支持,到时候各种控件的嵌套滑动就能无缝集成了。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图