详解Android中的NestedScrolling机制带你玩转嵌套滑动

一、概述

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

  • NestedScrollingParent
  • NestedScrollingChild

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

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

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

  • 这个机制相比传统的自定义ViewGroup事件分发处理有什么优越的地方吗?

恩,我们简单分析下:

按照上图:

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

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

ps:具体在哪个v4版本中添加的,就不去深究了,如果你的v4中没有上述两个类,升级下你的v4版本。NestedScrolling机制这个词,个人称呼,不清楚官方有没有这么叫,勿深究。

二、预期效果

当然讲解这两个类,肯定要有案例的支撑,不然太过于空洞了。好在,我这里有个非常好的案例可以来描述:

很久以前,我写过这样一篇文章:

Android通过自定义控件实现360软件详情页效果

完全按照传统的方式去编写的,而且为了连续滑动,做了一些非常特殊处理,比如手动去分发DOWN事件类的,有兴趣可以阅读下。

效果图是这样的:

今天我们就利用这个效果,作为NestedSroll机制的案例,最后我们还会简单分析一下源码,其实源码还是比较简单的~~

ps:CoordinatorLayout可以很方便实现该效果,后续的文章也会对CoordinateLayout做一些分析。

三、实现

上述效果图,分为3部分:顶部布局;中间的ViewPager指示器;以及底部的RecyclerView;

RecyclerView其实就是NestedSrollingChild的实现类,所以本例主要的角色是去实现NestedScrollingParent.

(1)布局文件

首先预览下布局文件,脑子里面有个大致的布局:

<com.zhy.view.StickyNavLayout xmlns:tools="http://schemas.android.com/tools"
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical" >
 <RelativeLayout
 android:id="@id/id_stickynavlayout_topview"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="#4400ff00" >
 <TextView
  android:layout_width="match_parent"
  android:layout_height="256dp"
  android:gravity="center"
  android:text="软件介绍"
  android:textSize="30sp"
  android:textStyle="bold" />
 </RelativeLayout>

 <com.zhy.view.SimpleViewPagerIndicator
 android:id="@id/id_stickynavlayout_indicator"
 android:layout_width="match_parent"
 android:layout_height="50dp"
 android:background="#ffffffff" >
 </com.zhy.view.SimpleViewPagerIndicator>

 <android.support.v4.view.ViewPager
 android:id="@id/id_stickynavlayout_viewpager"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
  >
 </android.support.v4.view.ViewPager>

</com.zhy.view.StickyNavLayout>

StickyNavLayout是直接继承自LinearLayout的,并且设置的是orientation="vertical",所以直观的就是控件按顺序纵向排列,至于测量需要做一些特殊的处理,因为不是本文的重点,可以自己查看源码,或者上面提到的文章。

(2) 实现NestedScrollingParent

NestedScrollingParent是一个接口,实现它需要实现如下方法:

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

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

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

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

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

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

public int getNestedScrollAxes();

在写具体的实现前,先对需要用到的上述方法做一下简单的介绍:

  • onStartNestedScroll该方法,一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数;假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数,进行纵向判断。
  • onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2
  • onNestedFling你可以捕获对内部View的fling事件,如果return true则表示拦截掉内部View的事件。

主要关注的就是这三个方法~

这里内部View表示不一定非要是直接子View,只要是内部View即可。

下面看一下我们具体的实现:

public class StickyNavLayout extends LinearLayout implements NestedScrollingParent
{
 @Override
 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
 {
 return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
 }
 @Override
 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
 {
 boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
 boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);

 if (hiddenTop || showTop)
 {
  scrollBy(0, dy);
  consumed[1] = dy;
 }
 }
 @Override
 public boolean onNestedPreFling(View target, float velocityX, float velocityY)
 {
 if (getScrollY() >= mTopViewHeight) return false;
 fling((int) velocityY);
 return true;
 }
}
  • onStartNestedScroll中,我们判断了如果是纵向返回true,这个一般是需要内部的View去传入的,你要是不确定,或者担心内部View编写的不规范,你可以直接return true;
  • onNestedPreScroll中,我们判断,如果是上滑且顶部控件未完全隐藏,则消耗掉dy,即consumed[1]=dy;如果是下滑且内部View已经无法继续下拉,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是我们的StickNavLayout滑动。
  • 此外,这里还处理了fling,通过onNestedPreFling方法,这个可以根据自己需求定了,当顶部控件显示时,fling可以让顶部控件隐藏或者显示。

以上代码就能实现下面的效果:

对于fling方法,我们利用了OverScroll的fling的方法,对于边界检测,是重写了scrollTo方法:

public void fling(int velocityY)
{
 mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
 invalidate();
}

@Override
public void scrollTo(int x, int y)
{
 if (y < 0)
 {
  y = 0;
 }
 if (y > mTopViewHeight)
 {
  y = mTopViewHeight;
 }
 if (y != getScrollY())
 {
  super.scrollTo(x, y);
 }
}

详细的解释可以看上面提到的文章,这里就不重复了。

到这里呢,可以看到NestedScrolling机制说白了非常简单:

就是NestedScrollingParent内部的View,在滑动到时候,会首先将dx、dy传入给NestedScrollingParent,NestedScrollingParent可以决定是否对其进行消耗,一般会根据需求消耗部分或者全部(不过这里并没有实际的约束,你可以随便写消耗多少,可能会对内部View造成一定的影响)。

用白话和原本的事件分发机制作对比就是这样的(针对正常流程下一次手势):

  • 事件分发是这样的:子View首先得到事件处理权,处理过程中,父View可以对其拦截,但是拦截了以后就无法再还给子View(本次手势内)。
  • NestedScrolling机制是这样的:内部View在滚动的时候,首先将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。

具体的源码会比本博文复杂,因为涉及到触摸非内部View区域的一些交互,非本博文重点,可以参考源码。

四、原理

原理其实就是看内部View什么时候回调NestedScrollingParent各种方法的,直接定位到内部View的onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent e) {
 switch (action) {
  case MotionEvent.ACTION_DOWN: {
   int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
   if (canScrollHorizontally) {
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
   }
   if (canScrollVertically) {
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
   }
   startNestedScroll(nestedScrollAxis);
  } break;
  case MotionEvent.ACTION_MOVE: {
   if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
    dx -= mScrollConsumed[0];
    dy -= mScrollConsumed[1];
    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
   }
  } break;
  case MotionEvent.ACTION_UP: {
   fling((int) xvel, (int) yvel);
   resetTouch();
  } break;

  case MotionEvent.ACTION_CANCEL: {
   cancelTouch();
  } break;
 }
 return true;
}

可以看到:

ACTION_DOWN调用了startNestedScroll;ACTION_MOVE中调用了dispatchNestedPreScroll;ACTION_UP可能会触发fling以调用resetTouch。

startNestedScroll内部实际上:

#NestedScrollingChildHelper
public boolean startNestedScroll(int axes) {
 if (hasNestedScrollingParent()) {
  // Already in progress
  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;
}

去寻找NestedScrollingParent,然后回调onStartNestedScroll和onNestedScrollAccepted。

dispatchNestedPreScroll中会回调onNestedPreScroll方法,内部的scrollByInternal中还会回调onNestedScroll方法。

fling中会回调onNestedPreFling和onNestedFling方法。

resetTouch中则会回调onStopNestedScroll。

代码其实没什么贴的,大家直接找到onTouchEvent一眼就能看到,调用的方法名都是dispatchNestedXXX方法,实际内部都是通过NestedScrollingChildHelper实现的。

所以如果你需要实现和NestedScrollingParent协作的内部View,记得实现NestedScrollingChild,然后内部借助NestedScrollingChildHelper这个辅助类,核心的方法都封装好了,你只需要在恰当的实际去传入参数调用方法即可。

ok,这样的一个机制一定要去试试,很多滑动相关的效果都可以借此实现;

源码地址:

github地址:https://github.com/hongyangAndroid/Android-StickyNavLayout

本地下载:http://xiazai.jb51.net/201705/yuanma/Android-StickyNavLayout(jb51.net).rar

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Android嵌套滑动冲突的解决方法

    android在嵌套滑动的时候会产生滑动冲突.之前我也碰到,但是以前的笔记本丢失了,所以只能重新再写一章. 一.会产生滑动冲突的情况 那么什么时候会产生滑动冲突呢?比如你有个activity,activity的上半部分是一个布局,下半部分是一个可滑动控件(RecyclerView.ListView等),或者下半部分是个viewpager,里面的fragment布局是一个可滑动控件,这样的页面就会产生滑动冲突. 二.以前的做法 虽然我以前的笔记丢失了,但是当时的解决问题的思路我依然记得. (1)重

  • Android ScrollView嵌套横向滑动控件时冲突问题

    前言:今天在开发的时候遇到这样的问题,最外层是ScrollView,里面嵌套了一个横向滑动的日历控件,在滑动日历的时候很卡顿.看到这种问题,自然而然的就会想到scrollview和其他可滑动控件的冲突问题. 解决思路 用户的左右滑动操作被最外层的scrollView控件处理掉了,所以只要让scrollview对左右滑动事件不监听,让其子控件处理左右滑动事件 .重写scrollview的onInterceptTouchEvent方法,当上下滑动时不处理即可. 代码如下 public void se

  • Android嵌套RecyclerView左右滑动替代自定义view

    以前的左右滑动效果采用自定义scrollview或者linearlayout来实现,recyclerview可以很好的做这个功能,一般的需求就是要么一个独立的左右滑动效果,要么在一个列表里的中间部分一个左右滑动效果 而列表里面也容易,只是需要解决一点小问题,个人认为值得一提的就是高度问题,一般的人采用固定死的高度,可是在列表里面展示和机型的不同,固定死的话很难保证美观,动态的高度才能解决问题的所在 首先在一个列表控件布局上添加一个recyclerview控件 <android.support.v

  • Android解决viewpager嵌套滑动冲突并保留侧滑菜单功能

    重写子pagerview的dispatchTouchEvent方法,在返回前添加一句getParent().requestDisallowInterceptTouchEvent(true)中断掉事件的传递,类如下 public class SupperViewPager extends ViewPager { private int screenWidth;//屏幕宽度 public SupperViewPager(Context context) { super(context); } pub

  • Android中RecyclerView嵌套滑动冲突解决的代码片段

    在纵向RecyclerView嵌套横向RecyclerView时,如果纵向RecyclerView有下拉刷新功能,那么内部的横向RecyclerView的横向滑动体验会很差.(只有纯横向滑动时,才能滑动内部的横向RecyclerView,否则滑动事件就会影响到下拉刷新),添加拦截判断. public class MySwipeRefreshLayout extends SwipeRefreshLayout { private boolean mIsVpDragger; private final

  • 详解Android中的NestedScrolling机制带你玩转嵌套滑动

    一.概述 Android在support.v4包中为大家提供了两个非常神奇的类: NestedScrollingParent NestedScrollingChild 如果你从未听说过这两个类,没关系,听我慢慢介绍,你就明白这两个类可以用来干嘛了.相信大家都见识过或者使用过CoordinatorLayout,通过这个类可以非常便利的帮助我们完成一些炫丽的效果,例如下面这样的: 这样的效果就非常适合使用NestedScrolling机制去完成,并且CoordinatorLayout背后其实也是利用

  • 详解Android中Handler的内部实现原理

    本文主要是对Handler和消息循环的实现原理进行源码分析,如果不熟悉Handler可以参见博文<详解Android中Handler的使用方法>,里面对Android为何以引入Handler机制以及如何使用Handler做了讲解. 概括来说,Handler是Android中引入的一种让开发者参与处理线程中消息循环的机制.我们在使用Handler的时候与Message打交道最多,Message是Hanlder机制向开发人员暴露出来的相关类,可以通过Message类完成大部分操作Handler的功

  • 详解Android应用沙盒机制

    前言 Android使用沙盒来保护用户不受恶意应用的侵害,同时也将应用隔离开来,防止他们互相访问其数据,本文主要对Android应用沙盒中的几种技术做简要的总结. 一.Android应用DAC沙盒 稍微了解Android一点的人都知道,Android上的App并不像Linux上的用户程序那样,启动应用的uid默认就是登录用户的uid,除非你使用sudo或者setuid等机制.而是每个Android应用都对应了一个uid,也就是一个用户,通过Linux系统的DAC机制将应用的数据严格隔离开来. A

  • 详解Android中的ActivityThread和APP启动过程

    ActiviryThread ActivityThread的初始化 ActivityThread即Android的主线程,也就是UI线程,ActivityThread的main方法是一个APP的真正入口,MainLooper在它的main方法中被创建. //ActivityThread的main方法 public static void main(String[] args) { ... Looper.prepareMainLooper(); ActivityThread thread = ne

  • 详解Java中的反射机制和动态代理

    一.反射概述 反射机制指的是Java在运行时候有一种自观的能力,能够了解自身的情况为下一步做准备,其想表达的意思就是:在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制.通俗点讲,通过反射,该类对我们来说是完全透明的,想要获取任何东西都可以,这是一种动态获取类的信息以及动态调用对象方法的能力. 想要使用反射机制,就必须要先获取到该类

  • 详解Android 中的文件存储

    目录 概要 当我们查看手机的文件管理器的时候,会发现里面的文件五花八门,想要找到自己项目所对应的文件非常困难,甚至有可能压根就找不到自己的文件,本文就来介绍一下APP开发过程当中文件存储的注意事项. 通常我们会将存放的文件分为两种:独立文件和专属文件.顾名思义,独立文件就是独立于APP之外的文件,不会随着APP的删除而删除,而专属文件则是专属于某个APP的文件,当APP删除后,会自动清空相对应的专属文件. 独立文件 独立文件指的是存放在shared/external storage direct

  • 一文详解Java中的类加载机制

    目录 一.前言 二.类加载的时机 2.1 类加载过程 2.2 什么时候类初始化 2.3 被动引用不会初始化 三.类加载的过程 3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 四.父类和子类初始化过程中的执行顺序 五.类加载器 5.1 类与类加载器 5.2 双亲委派模型 5.3 破坏双亲委派模型 六.Java模块化系统 一.前言 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程

  • 详解Android中motion_toast的使用

    目录 前言 motion_toast 介绍 示例 最简单用法 其他内置的提醒 自定义 toast 总结 前言 我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等.大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种. 说实话,这种toast 的体验很糟糕.假设是新手用户,他们并不知道 toast 从哪里出来,等出现错误的时候,闪现出来的时候,可能还没抓住内容的重点就消失了(尤其是想截屏抓错误的时候,更抓狂).这是因为一个是这种

  • 图文详解Java中的序列化机制

    目录 概述 对象序列化和反序列化机制 修改默认的序列化机制 使用transient关键字 自定义readObject.writeObject方法 实现Externalizable接口 serialVersionUID的作用 使用序列化clone 概述 java中的序列化可能大家像我一样都停留在实现Serializable接口上,对于它里面的一些核心机制没有深入了解过.直到最近在项目中踩了一个坑,就是序列化对象添加一个字段以后,使用方系统报了反序列化失败,原因是我们双方的序列化对象没有加上seri

  • 详解Android中Intent对象与Intent Filter过滤匹配过程

    如果对Intent不是特别了解,可以参见博文<详解Android中Intent的使用方法>,该文对本文要使用的action.category以及data都进行了详细介绍.如果想了解在开发中常见Intent的使用,可以参见<Android中Intent习惯用法>. 本文内容有点长,希望大家可以耐心读完. 本文在描述组件在manifest中注册的Intent Filter过滤器时,统一用intent-filter表示. 一.概述 我们知道,Intent是分两种的:显式Intent和隐式

随机推荐