聊聊Android嵌套滑动

最近工作中遇到了需求是使用 Bottom-Sheet 交互的弹窗,使用了 design 包里面的 CoordinatorLayout 和 BottomSheetBehavior ,因为弹窗承载的页面相对来说还是比较复杂的页面,所以也踩了好几个坑。之前UI交互类的东西接触的也比较少,于是把Android里面嵌套滑动相关的内容也过了一遍,在这里做一些分享。
在嵌套滑动控件的场景中,可以在Android的事件分发机制本身做一些处理,外部拦截或者内部消化触摸事件。但是这样的解决方法有几个弊端:

  • 代码复杂,难以维护
  • 事件分发机制中子view消耗了事件没有办法通知父View,这样实现的效果非常的突兀,难以达到预期

于是 Android 在 5.0 之后除了一系列的嵌套滑动支持的组件。这些组件现在也都在 androidx 包下面。

嵌套滑动的机制

实现嵌套滑动需要实现以下几个接口:NestedScrollingParent NestedScrollingParent2 NestedScrollingParent3 这3个接口实际上是继承关系:

NestedScrollingParent3 extends NestedScrollingParent2 extends NestedScrollingParent

在可以滑动的view(例如 NestedScrollViewRecyclerView ) 中,开始嵌套滑动都依赖NestedScrollingChildHelper 这个对象。RecyclerView 为例:
嵌套滑动我们最先接触到的可能就是 NestedScrollView 这个控件了,那么它是怎么支持嵌套滑动的呢?我们仍然从它的touch事件处理流程开始看:
在它的 onInterceptTouchEvent 中,当手势是 MOVE 的时候, 如果是垂直方向滑动并且达到滑动定义的距离,就开始执行滑动:
当手势是 DOWN  的时候,开始嵌套滑动:
当手势是 MOVE 的时候,结束嵌套滑动:
最终,是否拦截触摸事件,都交由自己是否正在拖拽状态来觉得,如果是,就拦截。这样 NestedScrollView 里面的view 才可能完全跟着一起滑动。

 if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) {            return true;        }        // ...return mIsBeingDragged;

如果滑动的时候是移动手势的话,事件会被拦截下来交给自己去处理。如果是其他手势,滑动的时候拦截,不滑动的时候不拦截。如果滑动的时候不拦截的话,手势事件会交给子view去处理,如果子view是可以滚动的,这时候就会有冲突,所有滚动的时候事件要拦截下来交给自己处理。
接下来看下,如果拦截下来了, NestedScrollView 是如何处理触摸事件的:DOWN 的时候直接触发嵌套滑动:MOVE 的时候
mIsBegingDragged 的false但是距离还没到的时候,让父布局不要拦截事件,
mIsBegingDragged 为true的时候,分发嵌套预滚动事件。如果消费这个事件,相应的修改距离。
接着分发嵌套滚动事件,中间还有一些针对 Scroll mode的处理,我们这里不关心:UP 的时候会根据距离判断是否需要消费快速滑动,如果不则会进行分发:
所以我们需要关注的就是:startNestedScroll -> dispatchNestedPreScroll -> dispatchNestedScroll -> stopNestedScroll 的这个过程。
这些都是 NestedScrollView里面的 NestedScrollingChildHelper 对象完成的。
开始嵌套滑动的时候,如果view是支持嵌套滑动的,则会view树一直往上寻找

while (p != null) {    // ... todo    if (p instanceof View) {        child = (View) p;    }    p = p.getParent();}

todo的地方会调用ViewParentCompatonStartNestedScroll,如果view的父布局同意view嵌套滑动,则返回true,如果不同意就继续询问父布局的父布局是否同意,如果到view树的最顶端还不支持,那么就返回false,无法进行嵌套滚动了。
那么这时候只要滑动方向是竖直方向,就可以认为是支持子View嵌套滑动了。
接下来看看 dispatchNestedPreScroll:

 public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,@Nullable int[] offsetInWindow, @NestedScrollType int type) {    if (isNestedScrollingEnabled()) {        // todo    }    return false; }

这里如果没有支持嵌套滑动,那么直接返回false,也就是父布局不去处理嵌套滑动事件。继续看正常支持时候的逻辑:

ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);return consumed[0] != 0 || consumed[1] != 0;

这里只要父布局消费了距离,就会返回true。onNestedPreScroll的逻辑就和 onStartNestedScroll非常类似了:
假设还是 NestedScrollView外层套了NestedScrollView:

dispatchNestedPreScroll(dx, dy, consumed, null, type);

会继续往父布局的父布局分发 pre-scroll
接下来会继续执行 dispatchNestedScroll:

ViewParentCompat.onNestedScroll(parent,     mView,    dxConsumed,     dyConsumed,     dxUnconsumed,     dyUnconsumed,     type,     consumed);

onNestedScroll执行逻辑也和之前2个方法类似:
NestedScrollView里,仍然会带着最新的消费距离去继续分发嵌套滚动的事件:
这里父布局会接收到子view传来的 dyUnconsumed ,然后进行 scrollBy.这里正好也就能解释了为什么需要 pre-scroll  这个操作。因为有了一次 pre-scroll 操作,我们才可以让子view在第一次执行嵌套滑动分发的时候,带上自己没有消费的距离,也就是 unconsumedY :
到这里 Android 的嵌套滑动机制就比较明了了,只要实现了这几个接口,再借助系统提供的这几个 Helper 类,我们就能很轻松的实现嵌套滑动效果。并且子 View 在消费了事件之后,还可以把剩下没有消费的事件交给父 View 继续处理,这样滑动事件就不会断的很突兀,非常的给力。

嵌套滚动方案的选择

有了这些接口之后,我们可以看到其实内置的Android 控件都支持了滑动嵌套,那么是否我们平时使用的方法都是正确的呢?不全是,最常见的比如 NestedScrollView 包裹 RecyclerView ,这时候 NestedScrollView 会把 UNSPECIFIED 传递给 RecyclerView 的 onMeasure , 最终表现就是,不管你的列表有多少数据,都给你一次性加载出来。
具体细节这里推荐一篇文章,里面有讲解:https://juejin.im/post/6844903938395734024
于是有没有更好的选择方案呢?有,那就是使用 design 的 CoordinatorLayout 加 Behavior 。CoordinatorLayout 在布局上其实和我们常见的 FrameLayout 没有差别,但是它内部实现了嵌套滑动的接口来支持包裹一个可以支持嵌套滑动的Scroll 组件,并且把交互抽象到 Behavior 中进行处理。常见的有 AppBarLayout.Behavior 和 BottomSheetBehavior , 前者是 appbar 的部分网上滑动之后固定在顶部,后者是从下网上弹出布局,这2种都是 MD 设计中常见的交互。

CoordinatorLayout

这里结合我最近使用到的 BottomSheetBehavior 来介绍一下 CoordinatorLayout 是怎么处理嵌套滑动的。关于bottomsheet的基础使用,我们可以参考官方文档或者网上的文章,这里找了一篇,没有使用过这个组件的可以先快速看一下:https://www.jianshu.com/p/0a7383e0ad0f

这里的 bottomsheet Dialog 的布局,其实是 design 包里面内置的,我们也可以自己实现这个dialog,布局是这样的:
这里需要让它第二个子view传入一个 behavior,这里是系统 BottomSheet 手势的behavior。
分别看下 CoordinatorLayoutBehavior:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,        NestedScrollingParent3

CoordinatorLayout实现了 NestedScrollingParent2NestedScrollingParent3,是一个嵌套滚动的父控件。
Behavior则实现了一系列看起来和View很像的方法:

  • onInterceptTouchEvent
  • onTouchEvent
  • onMeasureChild
  • onLayoutChild
  • onStartNestedScroll
  • onNestedScrollAccepted
  • onStopNestedScroll
  • onNestedScroll
  • onNestedFling

还有几个比较重要的它自己的方法:

  • layoutDependsOn 确定子view是否有其他布局作为依赖项,场景的appbar滚动固定的就会返回true
  • onDependentViewChanged
  • onDependentViewRemoved

CoordinatorLayout 的  onInterceptTouchEvent 方法:
这里会找到顶部的子view然后按照z轴来排序,然后遍历子view查看有没有 behavior,如果拦截到的事件不是 down的话,就触发一次 cancel 手势。然后再处理真正拦截到手势。也就是把拦截触摸事件的行为交给了自己的 Behavior .
看下 Behavior  的拦截:
满足

  1. 当前有滑动的子view
  2. 手势是move
  3. 不忽略事件
  4. 状态不是正在滑动
  5. 手势触发的坐标不在滑动的子view内
  6. 达到了滑动定义的要求

这些同时满足的话,则说明子child不是可以滑动的,那么就直接拦截事件全部交给 CoordinatorLayout 自己处理。否则就不拦截,交给子View去处理。
同理,如果没有拦截事件的话, onTouchEvent 也会交给 Behavior 去处理:
BottomSheetBehavior  里面,则会根据距离来切换 STATE_EXPANDED 、 STATE_COLLAPSED 之类的状态来达到最终的效果。例如上图的,当dy大于0,说明是向上滑动,如果最新的top值比展开的状态坐标小,那么就把状态置为 STATE_EXPANDED , 然后调用 offsetTopAndBottom  做距离上的变换。其他状态的处理也是类似。在这个方法里面,开始了真正的嵌套滑动。当距离到了最大的高度,为 STATE_EXPANDED 的时候,
拦截事件的条件:

state != STATE_DRAGGING

就成立了,这时候事件就被 CoordinatorLayout 拦截下来,内部的滑动控件就开始正常滑动。

总结

到这里,Android的嵌套滑动机制就介绍完了。不过 CoordinatorLayout 和 Behavior  虽然封装的很好,但是在很多场景下其实也还是有意想不到的坑,这个时候就需要具体情况具体分析,在这些关键的方法里面,一般也都可以找到答案。

另外也欢迎大家关注我的公众号:半行代码 :

android控件的touch事件_聊聊Android嵌套滑动相关推荐

  1. 以下哪些属于android控件的touch事件?_聊聊 Android 的 GUI 系统

    你长得辣么好看,我想着要更详细地了解你.今天,让我们一起来聊聊 Android 的 GUI 系统. 缘起 在2019年的 Google I/O 大会上,Jetpack 团队首次为大家介绍了 Jetpa ...

  2. android控件的touch事件_Android touch 事件分发时序

    点击上方"蓝字"关注我们 1,touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService: 2,WMS 是如何通过 ViewRooImp ...

  3. android 控件随手指移动_液体流动控件,隔壁产品都馋哭了

    作者:彭也 链接: https://www.jianshu.com/p/4f0844c72e8a 模拟液体流动的展开特效,适合一些需要侧边展开进行辅助说明的页面,如用户在填写某个表单,需要操作很多步骤 ...

  4. android gridview控件使用详解_作为Android 开发者该如何进阶?

    经常在简书和微信上收到一些同学的私信,说自己马上毕业或者已经毕业一年,从事Android开发相关的工作,现在不知道要学习什么东西了.或者说自己也在摸索着学习,但是不知道学习的路线对不对,感觉很迷茫,想 ...

  5. android控件向内弧度_安卓自定义 View 基础:坐标系、角度弧度、颜色

    安卓自定义View基础 - 坐标系 一.屏幕坐标系和数学坐标系的区别 由于移动设备一般定义屏幕左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向, 所以在手机屏幕上的坐标系与数学中常见的坐标系是 ...

  6. android控件之间的过渡动画效果,Android - 交换控件位置:基于LayoutParams的瞬间交换与基于ObjectAnimator动画效果交换...

    现需要交换两个控件(本文中是两个RelativeLayout),找到了两个方法: 1.使用LayoutParams改变两个layout的属性,即其相对关系(below等),实现位置的交换,但是并没有交 ...

  7. android 控件高度和图片一样高,Android 根据图片宽高比例设置控件宽高

    这个方式适用于一个界面少量图片. 主要的计算公式就是得到当前控件最大的显示宽度(高度),一般填充屏幕的话,就直接取屏幕的宽度了. 得到宽度除以图片宽除以高,也可以直接得到图片宽高的比例.如下公式 这种 ...

  8. Android控件捕获点击事件的范围

    View的Tween动画过程中点击事件的位置并不会因为动画位置的改变而改变,是因为在动画过程中layout的位置实际上没有变,因此曾经一度认为View的点击事件(其实不仅仅是点击事件,包括所有的触摸事 ...

  9. android控件向内弧度_描边/内间距/四个角不同弧度(包括圆角)

    YLCircleImageView 如果依赖失败,可以直接下载Library中的YLCircleImageView 和 style.xml放入项目中 QQ:375984181 功能 具备描边功能 具备 ...

最新文章

  1. python 使用mysqldb模块通过ssh隧道连接mysql
  2. Pytorch-张量相加的四种方法 / .item()用法
  3. lesson2-python3数据类型
  4. [19/04/11-星期四] 多线程_并发协作(生产者/消费者模式_2种解决方案(管程法和信号灯法))...
  5. Flex4/Flash多文件上传(带进度条)实例分享
  6. 面试题 : Intent、IntentFilter、PendingIntent的区别
  7. linux clone线程,如何在Linux上使用clone()创建真正的线程?
  8. 好代码是管出来的——C#的代码规范
  9. 【CodeForces - 215A】Bicycle Chain (水题)
  10. oracle变量名,Oracle中的替换变量,变量名,变量名
  11. log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory). log4j:WARN
  12. linux快速查找文件中所包含的指定字段的个数
  13. [转]Flex 中的皮肤
  14. python查看服务器日志_python读取服务器日志的方法
  15. Ubuntu U盘启动出现“Failed to load ldlinux.c32”解决
  16. heartbeat+lvs+Keepalive
  17. pinphp3.0后台系统权限管理的bug
  18. 微型计算机必须具备的输入设备,一台微型计算机必须具备的输出设备是显示器。...
  19. flv直播流播放视频,websocket响应造成内存泄漏 浏览器崩溃
  20. 人民币小写转大写金额(可达千百万亿)

热门文章

  1. 创建对称矩阵(numpy)
  2. 55岁自学python编程-热门专业三年一换?奥鹏教育解析编程还能火多久
  3. php和python-现在自学php和python那个合适?
  4. python画折线图-利用python画出折线图
  5. python基础教程廖雪峰云-为什么看不懂廖雪峰的Python学习教程?
  6. 地铁票务管理系统_地铁票务管理工作总结
  7. Vue的babel-plugin-transform-remove-console依赖使用方法
  8. H5搜索页调起软键盘
  9. Vue-动态组件和插槽
  10. vb怎么自动连接服务器,VB 如何制作连接服务器的进程