Android View滑动处理的终极指南

访客 185 0

原文链接:Android View滑动处理大法

对于触控式操作来说,滑动是一项非常重要的手势操作。如何实现应用程序页面的流畅滑动,让用户感受到顺畅无比的体验,是开发人员需要着重解决的问题。这对于提升用户体验而言至关重要。本文将探讨Android中View的滑动相关知识,并介绍如何实现丝般顺畅。

Android View滑动处理的终极指南-第1张图片-谷歌商店上架

如何让View滑动起来

View的滑动是GUI界面的基本特性之一,就像触摸事件一样。这是理所当然的事实,如果平台不支持这个功能,那还有什么意义呢?

View滑动的基本原理

让我们首先了解一下Android中实现View滑动的基本原理。实际上,屏幕本身并没有移动,一个View的可绘制区域对于屏幕和view tree来说都没有发生变化。父布局在layout之后就确定了给某个View的绘制区域,当View的真实高度或宽度超过这个可绘制区域时,需要进行滑动才能使整个View对用户可见。在View内部,通过两个关键成员变量mScrollX和mScrollY来记录滑动后的坐标位置。此外,每个View还有自己相对于父布局的坐标位置mLeft和mTop,在滑动时具体要绘制的区域将以mLeft+mScrollX和mTop+mScrollY为起点进行计算。因此,通过这种方式使得视图可以进行滚动操作。

如何实现View的滑动

对于开发人员来说,要实现View的滑动,需要注意三个关键方法,即View#scrollBy、View#scrollTo以及View#onScrollChanged。这三个方法是实现滑动功能最为重要的核心方法。

scrollBy方法接受滑动距离作为参数,而scrollTo方法需要传入目标坐标值来实现滑动。这两个方法都会修改mScrollX和mScrollY的值,从本质上讲它们是相同的。而onScrollChanged是一个回调函数,用于通知滑动位置的更新。

Scroll手势

要使View实现滑动,需要依赖事件手势的支持。最简单且直接的手势是onScroll手势,在GestureDetector中可以识别此手势,或者可以直接处理触摸事件来得到此手势。这并不复杂,只需通过触摸事件计算滑动距离即可。根据View预设的可滑动方向(如横向),计算不同时间点MotionEvent的坐标值,得到水平距离deltaX,并调用scrollBy方法即可实现滑动效果。垂直方向同理递推。

Android View滑动处理的终极指南-第2张图片-谷歌商店上架

Scroll手势之所以简单,是因为它直接从事件中获取,并且速度较慢,无需额外处理。因此,整体逻辑处理流程并不复杂。

在GestureDetector中,识别手势是通过检查滑动的距离来进行的。这个距离是通过计算(dx x dx + dy x dy)的平方根得到的。如果这个距离大于触摸阈值(touch slop),就会触发onScroll手势回调函数。

Fling手势

Fling是一种快速滑动的手势,就像手指在屏幕上迅速挠一下。这个手势的关键是手指在屏幕上快速滑过一个短距离,就像弹出小球一样。对于Fling手势来说,最重要的是速度,包括水平方向和垂直方向的速度。可以将其类比为高中物理中所讲到的平抛运动。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zolPYiCN-1688998312494)(https://ts1.cn.mm.bing.net/th/id/R-C.16bf196415d4a9adb260a465059753cf?rik=iJZkKq8qecE9ew&riu=http%3a%2f%2fwww.androidpolice.com%2fwp-content%2fuploads%2f2017%2f05%2fnexus2cee_fling-animation.gif&ehk=FJQxdKhU05GazX23tULh7WVJ3Spv1upP6zHbMY8s9YA%3d&risl=&pid=ImgRaw&r=0)]

GestureDetector的Fling识别逻辑如下:当ACTION_UP事件发生时,会检查该事件的速度。如果水平方向或垂直方向的速度超过了设定的阈值,就会触发Fling手势回调。

注意:留意Scroll与Fling的区别,Scroll是慢的,不关心时间与速度,只关心滑动的距离,是在ACTION_MOVE时,手指并未有离开屏幕时就触发了,只要是ACTION_MOVE还在继续,就会继续触发onScroll,并且ACTION_UP时终止整个Scroll,而Fling只关心速度,不关心距离,是在ACTION_UP时,手指离开了屏幕了(此次事件流处理结了)才会触发。

VelocityTracker

了解Fling事件的速度是非常重要的。仔细研究GestureDetector的处理过程,你会发现它利用了一个名为VelocityTracker的对象来处理与速度相关的具体逻辑。因此,深入了解这个对象是非常必要的。

VelocityTracker使用起来并不复杂,获取它的一个对象后,只需要不断的把MotionEvent塞给它就可以了,然后在需要的时候让其计算两个方向上的速度,然后就没有然后了:

velocityTracker = VelocityTracker.obtain();
onTouchEvent(MotionEvent ev) {
    velocityTracker.addMovement(ev);
    if (want to know velocities) {
        velocityTracker.computeCurrentVelocity();
    }
} 

顺滑如丝

在前面提到了,要让View滑动只需使用scrollBy或者scrollTo方法。然而,这种方式直接修改了mScrollX和mScrollY的值,并在下次绘制时通过invalidate方法将目标区域的内容直接绘制出来。换句话说,这两个方法实现的滑动是瞬间跳转的效果。

通常情况下,这也不成问题,就像在onScroll手势的ACTION_MOVE时,持续使用scrollBy来滑动刚刚经过的距离一样,并没有任何困扰或难题。

但是对于Fling事件就不行了,Fling事件,也即快速滑动,要求短时间内进行大距离滑动,或者像有跳转的需求时,也是短时间内要滑动大距离。如果直接scrollBy或者scrollTo一步到位了,会显得 相当的突兀,体验相当不好,卡顿感特别强。如果能像做动画那样,在一定时间内,让其平滑的滑动,就会如丝般顺滑,体验好很多。Scroller就是专门用来解决此问题的。

Scroller

Scroller是对滑动操作的封装,与View没有任何关系,也不能直接操作View。实际上,它类似于属性动画,只是一个滚动位置的计算器。你可以告诉它起始位置和要滚动的距离,然后它会返回随时间变化的位置值。这其实就是一个中学物理题,在给定初始位置和要滚动的距离后,通过一定方式计算每个时间点的位置。具体计算方式由mInterpolater成员控制,默认使用ViscousFluid作为减速度自然指数来计算。如果不喜欢默认的计算方式,你可以自己实现一个Interpolator,并在构造时传入Scroller中使用。

Scroller的作用是实现平滑滚动,避免View的滚动出现跳跃。例如,在滑动ListView时,开始滑动时的位置为x0、y0(即ActionDown的位置),如果要向下滑动500个像素,不平稳指的是从x0一下子跳到x0+500的位置。为了实现平稳效果,需要不断微调x值并进行invalidate操作。这正是Scroller经常被使用的场景之一。

Scroller scroller = new Scroller(getContext()); 
如果滚动器正在计算滚动偏移量(computeScrollOffset()),则执行以下操作:获取当前的X坐标(getCurrX())并将其赋值给变量currX。 

滑动冲突处理

关于View的滑动,最具挑战的问题在于处理手势冲突,尤其是当页面结构变得复杂时。一般来说,滑动手势用于让某个View沿特定方向平移一段距离。如果页面中只有一个可滑动的View,或者不同View的可滑动方向垂直正交,则不会出现冲突问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmf47f23-1688998312494)(https://ts1.cn.mm.bing.net/th/id/R-C.0b4a444cd44ddeebce726777cdddf2f9?rik=Wrup7%2bYTmIZibA&riu=http%3a%2f%2funitid.nl%2fandroidpatterns%2fwp-content%2fuploads%2fscrollthumb.png&ehk=hHYe%2byE4JOmZrc8S4vLHliUmVEJ%2f6uI3oeHeg8s0dMM%3d&risl=&pid=ImgRaw&r=0)]

滑动冲突是指当父View和子View都接收滑动手势,并且方向相同时,就会发生冲突。常见的情况是在ScrollView中嵌套ListView,这通常导致垂直方向上的滑动冲突;或者在ViewPager中嵌套ScrollView,这会导致水平方向上的滑动冲突。

为了解决滑动冲突问题,首先需要确立一个完善的整体设计方案。一旦有了明确的原则,就可以通过技术方案找到解决方法。目前最理想且广泛采用的方案是在子View的边界设置一个margin区域。当ACTION_DOWN事件发生在margin区域之外时,将滑动手势交由父View处理;否则交由子View处理。类似地,在处理全局手势时也可以采用这种方式。如果点击事件发生在屏幕一定范围内(即margin区域),则将该事件归属于当前页面处理;否则认定为全局手势,例如从屏幕左侧向右滑动等操作通常被识别为返回上一页操作。然而,如果离左侧较远进行滑动,则会被视为页面内部的滑动事件(假设存在可滑动组件),此时该事件将被组件消耗掉。

参考资料

  • Detect common gestures
  • Flyweight pattern
  • Design Patterns - Flyweight Pattern
  • Animate a scroll gesture
  • Android Scroller simple example

原创不易,打赏点赞在看收藏分享 总要有一个吧

标签: 手势 距离 速度 事件

发表评论 (已有0条评论)

还木有评论哦,快来抢沙发吧~