前言

Android实现简易版滑动

上次文章中实现了简易的ScrollerView滑动,但实际使用中许多场景都会涉及到嵌套滑动,在今天的博文中我们基于上次的ScrollLayout来进一步实现嵌套滑动。

嵌套滑动预备知识:https://juejin.cn/post/6844904184911773709

整体页面结构

<?xml version="1.0" encoding="utf-8"?>
<com.example.nestedscroll.ScrollParentLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="300dp"android:text="I'm TOP!"android:gravity="center"android:textSize="24sp"android:background="@color/teal_700"/><com.example.nestedscroll.ScrollChildLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:text="I'm 1"android:gravity="center"android:textSize="24sp"android:background="@color/red1"/><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:text="I'm 2"android:gravity="center"android:textSize="24sp"android:background="@color/red2"/><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:text="I'm 3"android:gravity="center"android:textSize="24sp"android:background="@color/red1"/>... 后边还有n个TextView</com.example.nestedscroll.ScrollChildLayout></com.example.nestedscroll.ScrollParentLayout>

嵌套结构中父ViewGroup为ScrollParentLayout,子ViewGroup为ScrollChildLayout。

class ScrollParentLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
) : NestedScrollLayout(context, attrs), NestedScrollingParent3 class ScrollChildLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
) : NestedScrollLayout(context, attrs), NestedScrollingChild3 
  • ScrollParentLayout和ScrollChildLayout均继承自NestedScrollLayout(NestedScrollLayout为ScrollLayout的copy,以不修改ScrollLayout实现)以提供滑动功能
  • ScrollParentLayout实现了NestedScrollingParent3接口,作为嵌套滑动的父控件
  • ScrollChildLayout实现了NestedScrollingChild3接口,作为嵌套滑动的子控件

页面滑不动

运行后发现页面滑不动,查看NestedScrollLayout的onInterceptTouchEvent()实现,为简单实现滑动效果,上节中简单将NestedScrollLayout设置为拦截所有触摸事件。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {return true
}

这直接导致了页面滑不动,因为ScrollParentLayout其子View的高度经过onMeasure后都是固定的了,所以ScrollParentLayout的控件高度和内容高度相等,ScrollParentLayout不可滑动。同时由于ScrollParentLayout在外层拦截了触摸事件,ScrollChildLayout无法接收到触摸事件,因此也无法响应,所以页面无法滑动。

结合嵌套滑动的机制(NestedScrollingParent,NestedScrollingChild机制),滑动时间需由子控件来接收,然后通过嵌套滑动机制来确定父控件是否消费部分滑动距离,因此ScrollParentLayout需要保证不拦截触摸事件,同时ScrollChildLayout需要接收到触摸事件。

//ScrollParentLayout.kt
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {return false
}
//ScrollChildLayout.kt
//实现参考了NestedScrollView
//实现参考了NestedScrollView
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {if (ev == null) return falseval action = ev.actionif (action == MotionEvent.ACTION_MOVE && isBeingDragged) {return true}var currY = ev.ywhen (action) {MotionEvent.ACTION_MOVE -> {if (abs(currY - lastY) >= touchSlop) {isBeingDragged = trueval parent = parentparent?.requestDisallowInterceptTouchEvent(true)}}MotionEvent.ACTION_DOWN -> {isBeingDragged = false//开始嵌套滑动,注意不是startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)}MotionEvent.ACTION_CANCEL,MotionEvent.ACTION_UP,-> {//结束嵌套滑动isBeingDragged = falsestopNestedScroll()}}return isBeingDragged
}

重写onInterceptTouchEvent()中,我们默认不拦截触摸事件,只有当View表现为正在滑动时才进行拦截,以处理滑动,并在开始滑动时调用startNestedScroll(),手指抬起时调用stopNestedScroll(),由于一个事件序列中会有多个ACTION_MOVE事件,而startNestedScroll()仅仅只在第一次判定为滑动时调用,所以引入了isBeingDragged变量,用以判断当前是否已经在嵌套滑动了,如果是则直接返回true,对应的逻辑为下边的代码。

    if (action == MotionEvent.ACTION_MOVE && isBeingDragged){return true}

经过处理后子View可以正常滑动了。

嵌套Scroll

ScrollChildLayout实现NestedScrollChild3接口

嵌套滑动机制中为我们提供了NestedScrollingChildHelper工具类,封装了基本的子ScrollView向父ScrollView传递滑动事件的操作,我们只需要NestedScrollingChildHelper对应的方法即可。注意NestedScrollingChildHelper要手动设置isNestedScrollingEnabled为ture。

private val childHelper = NestedScrollingChildHelper(this).apply {//注意要手动设置isNestedScrollingEnabled为ture,只有开启此开关,嵌套滑动才有效isNestedScrollingEnabled = true
}override fun startNestedScroll(axes: Int, type: Int): Boolean {return childHelper.startNestedScroll(axes, type)
}override fun stopNestedScroll(type: Int) {return childHelper.stopNestedScroll(type)
}override fun hasNestedScrollingParent(type: Int): Boolean {return childHelper.hasNestedScrollingParent(type)
}override fun dispatchNestedScroll(dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,offsetInWindow: IntArray?,type: Int,consumed: IntArray,
) {childHelper.dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsetInWindow,type,consumed)
}override fun dispatchNestedScroll(dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,offsetInWindow: IntArray?,type: Int,
): Boolean {return childHelper.dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsetInWindow,type)
}override fun dispatchNestedPreScroll(dx: Int,dy: Int,consumed: IntArray?,offsetInWindow: IntArray?,type: Int,
): Boolean {return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
}override fun dispatchNestedFling(velocityX: Float,velocityY: Float,consumed: Boolean,
): Boolean {return childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
}override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {return childHelper.dispatchNestedPreFling(velocityX, velocityY)
}

ScrollParentLayout实现NestedScrollParent3接口

嵌套滑动机制中也提供了NestedScrollingParentHelper工具类,我们可以使用此工具类来实现onNestedScrollAccepted()和onStopNestedScroll(),其他很多接口需要我们自行根据业务需要实现。

private val parentHelper = NestedScrollingParentHelper(this)override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {//判断是否处理嵌套滑动return axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
}override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {parentHelper.onNestedScrollAccepted(child, target, axes, type)
}override fun onStopNestedScroll(target: View, type: Int) {parentHelper.onStopNestedScroll(target, type)
}override fun onNestedScroll(target: View,dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,type: Int,consumed: IntArray,
) {}override fun onNestedScroll(target: View,dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,type: Int,
) {}override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {//TODO
}override fun onNestedFling(target: View,velocityX: Float,velocityY: Float,consumed: Boolean
): Boolean {//TODO
}override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {//TODO
}

让嵌套Scroll生效

上边的onInterceptTouchEvent()中我们通过在TOUCH_DOWN事件中调用了startNestedScroll()方法,开启了嵌套滑动,此方法主要用于确定嵌套滑动的NestedScrollingParent是谁。

接下来就需要由ScrollChildLayout来在滑动时将事件分发给ScrollParentLayout。滑动事件在onTouchEvent()的ACTION_MOVE事件中处理,这里将其抽离出来单独放在handleScroll()方法中。

override fun handleScroll(currX: Float, currY: Float) {val deltaX = currX - lastXval deltaY = currY - lastYvar realDeltaY = deltaY.toInt()if (dispatchNestedPreScroll(0,realDeltaY,scrollConsumed,scrollOffset,ViewCompat.TYPE_TOUCH)) {realDeltaY -= scrollConsumed[1]}if (canScrollVertically(1) || canScrollVertically(-1)) {//防止滑出边界realDeltaY = limitRange(realDeltaY, scrollY, -getScrollRange() + scrollY)scrollBy(0, -realDeltaY)}
}

上面代码中,利用嵌套滑动机制,首先dispatchNestedPreScroll()将滑动距离交由ScrollParentLayout来处理,ScrollParentLayout来先消费一部分距离,将剩下未消费的距离交由ScrollChildLayout继续处理,

ScrollChildLayout在判断了是否滑出边界后,调用scrollBy()方法处理剩下的滑动距离。

然后ScrollParentLayout也需要配合完成相应的滑动操作,ScrollParentLayout在onNestedPreScroll()方法中接收到对应的嵌套滑动距离,判断自身是否要消费。

回顾下目前布局结构是:

-ScrollParentLayout

-TopView

-ScrollChildLayout

ScrollParentLayout有两种常见处理方式:

  1. TopView和ScrollChildLayout同步调用layout(left,top,right,bottom)方法,TopView更新top和bottom,ScrollChildLayout更新top。(实现时遇到些问题,暂未采用)
  2. ScrollParentLayout调用scrollBy方法()整体滑动。看起来比较简单,下边代码即采用此方案。但需要先改造onMeasure()方法,让其能计算出其内容的高度(包括所有可滑动的子View内容的高度)作为其view的height属性
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)//visibleHeight为控件可见高度visibleHeight = measuredHeightif (orientation == VERTICAL) {var totalLength = paddingTop + paddingBottomfor (child in children) {totalLength += child.marginTop + child.measuredHeight + child.marginBottom}totalHeight = totalLength}//将measureHeight设置为内容的高度setMeasuredDimension(measuredWidth, totalHeight)
}

在onNestedPreScroll中,我们需要计算出ScrollParentLayout需要消费的滑动距离,主要要保证最后交由ScrollParentLayout处理的滑动的最终位置在[0, topViewHeight]范围内(即保证TopView可见或刚好不可见的部分才交由ScrollParentLayout处理)。

override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {var consumedY = 0//scrollY以向下为正向,整体相对于初始位置的偏移 -topViewHeight <= scrollY <= 0if (target == scrollChildLayout) {//下滑 && TopView还能再下滑(在初始位置之上)if (dy > 0 && scrollY > 0 && !scrollChildLayout.canScrollVertically(-1)) {consumedY = Math.min(scrollY, dy)//上滑 && TopView还能向上滑(TopView还可见)} else if (dy < 0 && scrollY < topViewHeight) {consumedY = Math.max(-topViewHeight + scrollY, dy)}}if (consumedY != 0) {scrollBy(0, -consumedY)consumed[1] = consumedY}
}

问题1:嵌套滑动距离小于手指滑动距离,滑动抖动

这个问题由于MotionEvent所对应的View(ScrollChildLayout)移动了所导致的,正常的跟手滑动为ScrollChildLayout不动,则每次滑动的deltaY = currY - lastY。currY和lastY都是通过event.getY()获取到的,event.getY()获取到的y值是相对于当前View(ScrollChildLayout)的Y值。由于当前View也朝相同方向滑动了,这导致计算出来的deltaY偏小,从而导致嵌套滑动距离小于手指滑动距离。(TODO滑动抖动)

解决办法(参考NestedScrollView):

我们需要获取到在ScrollParentLayout滑动时ScrollChildLayout的偏移量,查看dispatchNestedPreScroll()方法,可以使用offsetInWindow这个参数来获取ScrollParentLayout此次嵌套滑动的偏移量,然后在最后赋值lastY = currY - offsetInWindow[1]来校准偏移量。

/*** 在滑动之前,将滑动值分发给NestedScrollingParent* @param dx 水平方向消费的距离* @param dy 垂直方向消费的距离* @param consumed 输出坐标数组,consumed[0]为NestedScrollingParent消耗的水平距离、* consumed[1]为NestedScrollingParent消耗的垂直距离,此参数可空。* @param offsetInWindow 含有View从此方法调用之前到调用完成后的屏幕坐标偏移量,* 可以使用这个偏移量来调整预期的输入坐标(即上面4个消费、剩余的距离)跟踪,此参数可空。* @return 返回NestedScrollingParent是否消费部分或全部滑动值*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,@NestedScrollType int type);

至此就可以流畅的嵌套Scroll了~。

嵌套Fling

回顾前文中非嵌套的fling,通过OverScroller来实现滑动。OverScroller需配合computeScroll()方法一起处理fling动作。

NestedScrollChild接口提供了对应的dispatchNestedFling()和dispatchNestedPreFling()方法,NestedScrollParent接口也提供了对应的onNestedFlin()和onNestedPreFling()方法。由于目前还没想到使用的时机,暂时不知道咋用。。所以暂不使用这两个。通过scroll相关的接口也可以实现嵌套fling的效果。

fling事件一般在ACTION_UP事件中处理,先通过overScroller开始fling,然后开启嵌套滑动,注意嵌套滑动的类型是ViewCompat.TYPE_NON_TOUCH,代表的就是fling类型。

//ScrollChildLayout.kt
override fun touchUp() {velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity.toFloat())val yVelocity = velocityTracker.yVelocityif (abs(yVelocity) >= minFlingVelocity) {flingWithOverScroller(-yVelocity)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)lastScrollY = scrollYViewCompat.postInvalidateOnAnimation(this)}
}

同时computeScroll()方法中也要配合实现嵌套滑动,在子View调用scrollBy()方法的之前先通过dispatchNestedPreScroll()询问父View是否需要处理嵌套滑动事件,然后子View再消耗剩下的滑动距离,实现方法类似处理ACTION_MOVE事件中的嵌套滑动处理。但要注意滑动的类型是ViewCompat.TYPE_NON_TOUCH。

//ScrollChildLayout.kt
override fun computeScroll() {if (overScroller.computeScrollOffset()) {val deltaY = overScroller.currY - lastScrollYvar unconsumed = deltaYlastScrollY = overScroller.currYif (dispatchNestedPreScroll(0,unconsumed,scrollConsumed,null,ViewCompat.TYPE_NON_TOUCH)) {unconsumed -= scrollConsumed[1]totalParentConsumeScrollY += scrollConsumed[1]}if (unconsumed != 0 && canScrollVertically(1) || canScrollVertically(-1)) {//防止滑出边界val selfConsume = getRealScrollDistance(unconsumed)scrollBy(0, -selfConsume)}}if (!overScroller.isFinished) {ViewCompat.postInvalidateOnAnimation(this)} else {stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)}awakenScrollBars()}

之所以能通过startNestedScroll()的方式来处理嵌套fling,是因为嵌套scroll本质上是在调用scrollBy()方法之前询问父View是否要消费滑动距离,而ACTION_MOVE中的跟手滑动和fling中的惯性滑动,都是调用的scrollBy()方法,所以都可以通过startNestedScroll()来处理嵌套滑动。

问题1:嵌套fling的滑动距离明显不够,比预期的要短

这个问题的原因类似于嵌套Scroll中的嵌套滑动距离过短,它们都是由于当前View(ScrollChildLayout)的位置也发生了变化,导致了计算的手指移动距离过短而导致的。由于fling事件需要通过velocityTracker.addMovement(event)事先添加该次触摸事件序列中的所有事件,然后根据所有的event来计算出速度,由于event不加处理的情况下,会由于View(ScrollChildLayout)的滑动导致event的位置不准确,这样计算出的速度也是不准确的。我们可以使用类似上边处理嵌套滑动的手段计算出当前View(ScrollChildLayout)滑动的偏差。然后将event加上对应的偏差值,然后再添加到velocityTracker中即可校准速度。

//ScrollChildLayout.ktoverride fun handleScroll(currX: Float, currY: Float) {...if (dispatchNestedPreScroll(0,unconsumed,scrollConsumed,scrollOffset,ViewCompat.TYPE_TOUCH)) {unconsumed -= scrollConsumed[1]//计算此次滑动事件序列的总偏差值,用于校正fling的速度nestedYOffset += scrollOffset[1]lastY -= scrollOffset[1]}...
}override fun onTouchEvent(event: MotionEvent?): Boolean {...val offsetEvent = MotionEvent.obtain(event)//根据总的嵌套滑动偏移量,校正速度offsetEvent.offsetLocation(0f, nestedYOffset.toFloat())velocityTracker.addMovement(offsetEvent)offsetEvent.recycle()...
}

未解决的问题:

  1. 嵌套滑动时,可滑动边界判断不准,滑动到底部还多出一段空白(高度等于TopView)。

原因:ScrollChildLayout的可滑动范围=totalHeight - visibleHeight,初始时visibleHeight= ScrollParentLayout.visibleHeight - TopViewHeight,而随着ScrollChildLayout的向上滑动,其visibleHeight会慢慢增加,直到等于ScrollParentLayout.visibleHeight。目前ScrollChildLayout.visibleHeight未动态修改。

Android 实现嵌套滑动相关推荐

  1. 使用Android SwipeRefreshLayout了解Android的嵌套滑动机制

    SwipeRefreshLayout 是在Android Support Library, revision 19.1.0加入到support v4库中的一个下拉刷新控件,关于android的下拉刷新 ...

  2. Android嵌套滑动冲突

    android在嵌套滑动的时候会产生滑动冲突.之前我也碰到,但是以前的笔记本丢失了,所以只能重新再写一章. 一.会产生滑动冲突的情况 那么什么时候会产生滑动冲突呢?比如你有个activity,acti ...

  3. android 嵌套分组拖动_Android NestedScrolling嵌套滑动机制

    Android NestedScrolling嵌套滑动机制 最近项目要用到官网的下拉刷新SwipeRefreshLayout,它是个容器,包裹各种控件实现下拉,不像以前自己要实现事件的拦截,都是通过对 ...

  4. android控件的touch事件_聊聊Android嵌套滑动

    聊聊Android嵌套滑动 最近工作中遇到了需求是使用 Bottom-Sheet 交互的弹窗,使用了 design 包里面的 CoordinatorLayout 和 BottomSheetBehavi ...

  5. 干货:五分钟带你看懂NestedScrolling嵌套滑动机制

    Android NestedScrolling嵌套滑动机制 Android在发布5.0之后加入了嵌套滑动机制NestedScrolling,为嵌套滑动提供了更方便的处理方案.在此对嵌套滑动机制进行详细 ...

  6. Android Tv版嵌套滑动实现极光云视听顶部导航效果

    Android Tv版嵌套滑动实现极光云视听顶部导航效果 通过这篇文章您可以和小王一起: 了解嵌套滑动的流程,原理 自定义Behavior的原理. 简单的实现TV版的嵌套滑动 小王最近很开心,上次快速 ...

  7. Android仿小米时钟嵌套滑动(NestedScroll, 自定义behavior)

    最近无聊刷手机的时候, 发现小米时钟的嵌套滑动很有意思, 就试着做了下 先上对比图: 分析下小米时钟的滑动 闹钟列表 向上滑动时, 时钟面 透明度上升, 快到最大滑动时逐渐显示数字时钟. 在闹钟列表向 ...

  8. 一种嵌套滑动冲突的解决方案

    非嵌套滑动 | 嵌套滑动 相比起非嵌套滑动的自定义分发事件的方案,嵌套滑动冲突有比较成熟的 Google 解决方案:NestedScrolling . 三层嵌套的滑动冲突 UI 层级如下: 最外层(底 ...

  9. Android SrcollView嵌套recyclerView的使用

    今天,简单讲讲Android里使用SrcollView嵌套recyclerView需要注意的地方. 不废话了直接上代码,在使用时加上下面的代码就可以 recyclerView.setLayoutMan ...

最新文章

  1. python3程序下载安装_程序猿的语言,Python 3.7.0下载安装
  2. spring中怎么访问MySQL过程_【FunnyBear的Java之旅 - Spring篇】7步连接MySQL
  3. VS2013 C#中调用DLL
  4. [原]一步一步自己制作弹出框
  5. java线程并发库之--线程同步工具Exchanger的使用
  6. python 通过pip安装库 pycharm里面使用第三方库
  7. python不同数据类型的式子_Python 基础篇:数据类型、数据运算、表达
  8. 【重难点】【Redis 01】为什么使用 Redis、Redis 的线程模型、Redis 的数据类型及其底层数据结构
  9. C语言输出规定长度的整数,不够位数前面补零
  10. php 发送图片,php+curl 发送图片处理代码分享
  11. 开源、绿色,解压即可运行的数据库连接工具推荐
  12. 电阻电容等封装对应功率
  13. 引用次数在15000次以上的都是什么神仙论文?
  14. 【安信可首款4G模组CA-01直连阿里物联网平台②】一机一密认证方式连接
  15. 阿狸和桃子的游戏题解
  16. Revit开发读取CAD信息
  17. docker 容器资源限制
  18. jmeter模拟需验签的请求时注意参数中含有特殊字符要特别处理
  19. php解决缓慢http请求,php CURL 服务器响应慢的问题
  20. 语音识别-人工智能实验室旗下语音识别频道,汇集最新最全的语音识别新闻及资讯,让您掌握语音识别第一手的资讯-中国人工智能网-Powered by www.AiLab.cn

热门文章

  1. 深度学习中的温度参数(temperature parameter)--疑问待解决
  2. eclipse/UAP debug模式
  3. linux进程间通信快速入门【二】:共享内存编程(mmap、XSI、POSIX)
  4. HTMLCSS常用单词及音标 (上)
  5. GIS创新实践【实验2】疫情地图制作与发布
  6. git 代码没了,git rebase 合并提交记录,git stash
  7. zstuoj 4355
  8. 修改ELF可执行文件entry入口感染一个程序
  9. JAVASE相关知识点
  10. 浅描工作环境电脑维护以及性能测试流程