View 的滑动原理和实现方式

开发中,为了增加更多炫丽的效果,我们经常在应用中添加滑动效果,今天就来分析一下 View 中滑动效果的实现原理以及几种常见的实现方式。

一、滑动原理

1. Android 中的坐标系

View 基础 中我们提到了 View 中的 X、Y、mLeft、mTop... 等属性,其中这些属性的值都是相对坐标系来说的,Android 中有两种坐标系,这里一一来简单说一下

Android 坐标系: 以屏幕左上角为坐标原点,向右为 X 轴正方向,向下为 Y 轴正方向,MotionEvent 的 getRawX()、getRawY() 方法获取的是点击位置在 Android 坐标系中的坐标

视图坐标系: 以当前控件左上角为坐标原点,向右为 X 轴正方向,向下为 Y 轴正方向,MotionEvent 的 getX()、getY() 方法获取的是点击位置在视图坐标系中的坐标,View 的 mLeft、mTop 等属性也是 View 在父控件的视图坐标系中的坐标

2. 滑动原理

了解了 Android 中的坐标系,再说 View 的滑动原理,其实滑动的原理与动画效果的实现非常相似,都是通过不断改变 View 的坐标来实现这一效果。所以要实现滑动效果就必须要监听用户的触摸事件,并根据事件传入的坐标,动态且不断的改变 View 的坐标,从而实现 View 跟随用户触摸的滑动而滑动

二、滑动方式

滑动过程中触摸坐标改变的监听功能我们可以通过重写 onTouchEvent() 方法实现,onTouchEvent 中根据事件类型确定当前的触摸坐标,如果如果需要实现滑动,再调用实现滑动的方法。

通过滑动原理我们知道所有修改 View 坐标的方法都可以实现滑动功能,而改标 View 坐标的方式有很多种,这里我们就介绍几种常见的滑动方式

1. 通过 layout() 方法

View 中通过滑动事件中计算滑动距离,调用 layout 方法,其原理是改变 View 的 mLeft、mTop 等坐标,将初始位置值及偏移量传入,即需要滑动到的位置的坐标,由上篇文章介绍的 layout 过程 可知,View 完成重新布局后,就达到了移动 View 的效果

2. offsetLeftAndRight() 方法和 offsetTopAndBottom()

offsetLeftAndRight(offset) offsetTopAndBottom(offset) 方法的原理是通过修改 View 的 mLeft、mTop 坐标完成滑动。

在滑动事件中,得到偏移量,调用 offsetLeftAndRight 和 offsetLeftAndRight 方法实现滑动

3. 通过修改 LayoutParams 实现滑动

通过改变 View 的 LayoutParams 参数中的 margin 值,在父 View 的布局过程中会将 View 的坐标加上相应的 margin 偏移量,从而改变 View 在父容器中的坐标,完成滑动

4.使用动画

  • 使用 View 动画,只能改变 View 内容的位置,不能改变 View 的真正坐标

  • 使用属性动画完成滑动,在动画执行的过程中,通过改变 View 的真正坐标实现滑动

这里总结一下,以上介绍的前三种滑动方式以及使用属性动画完成滑动,这些方式都是通过改变 View 在其父容器中的坐标从而实现的滑动,实现的是控件整体发生了滑动

5. View 的 scrollTo()、scrollBy() 方法实现滑动

scrollTo、scrollBy 实现的是 View 内容的滑动,是区别于上面提到的控件整体滑动的,其效果是 View 控件并没有滑动,而是控件上绘制的内容在控件范围内发生了滑动

在发生滑动事件时,通过调用 scrollTo 或者 scrollBy 方法完成 View 内容的滑动。

如果想要通过 scrollTo/scrollBy 方法实现 View 控件的滑动,就在需要滑动的时候,调用 view.getParent().scrollTo ,通过让其父 View 滑动其父 View 中的内容,实现该 View 的滑动效果。

并且还有一点需要注意:如果通过其父 View 调用 scrollTo/scrollBy 方法改变所以子 View 在父 View 中的位置时,并没有修改子 View 真正的坐标位置,而是修改了坐标的偏移量 translateX、translateY 、x、y 的值,其中 x = mLeft + translateX ,y 同理

scrollTo/scrllBy 方法的区别

  • scrollTo(int x,int y) 实现的是相对于参数的绝对滑动,即滑动结果为相对于内容的原始位置,原始位置就是 mScrollX 和 mScrollY 都是 0 的位置,滑动后 mScrollX = x ; mScrollY = y;

  • scrollBy(int x,int y) 中调用了 scrollTo() 方法,不过 scrollBy() 实现的是相对于当前位置的相对滑动,即相对于当前 mScrollX 和 mScrollY 的值进行的滑动,滑动结果为 mScrollX = x + mScrollX(滑动前) mScrollY = y + mScrollY(滑动前)

View 的 mScrollX 和 mScrollY 参数

  • mScrollX 可由 getScrollX() 方法得到,表示的是,View 左边缘跟 View 内容左边缘在水平方向的距离,并且,如果 View 左边缘在内容左边缘左侧时该值为负,View 左边缘在内容左边缘右侧时该值为正。

  • mScrollY 可由 getScrollY() 方法得到,表示的是,View 上边缘跟 View 内容上边缘在竖直方向的距离,并且,如果 View 上边缘在内容上边缘上侧时该值为负,View 左边缘在内容左边缘右侧时该值为正。

6. 通过 Scroller 类实现滑动

Scroller 实现滑动的原理是通过调用 scrollTo 方法实现的,所以实现的也是内容的滑动,有关内容滑动的知识请看上一节通过 scrollTo 方法实现滑动效果

实现方式:

  1. 初始化 Scroller ,调用 Scroller 的 startScroll 方法,将 X,Y 方向的初始、需要偏移值、滑动初始时间以及滑动时间传入,如果不传入滑动时间,则默认时间为 250 毫秒,接着调用 invalidate() 方法执行重绘

  2. 重写 View 的 cumputeScroll 方法,在重绘过程中调用该方法。其中通过调用 Scroller 对象的 computeScrollOffset 方法测量当前时间对应的应该发生滑动值,并将最新的需要滑动的值保存,该方法返回是否滑动完成的 boolean 值,true 滑动未完成,返回 false 表示滑动已经完成。computeScrollOffset 方法中通过时间的流逝计算当前需要滑动到的位置。

  3. cumputeScroll 中,如果滑动未完成,通过 scroller 的 getScrollX getScrollY ,得到当前需要滑动到的位置,调用 View 的 scrollTo 方法滑动到指定位置,

  4. 再次调用 invalidate 方法,实现 View 的重绘。

再总结一下 Scroller 实现滑动的过程,invalidate 方法执行重绘,重绘过程中会调用 cumputeScroll 方法,cumouteScroll 方法中又会通过 Scroller 来计算当前需要滑动到的位置并调用滑动方法实现滑动,接着在调用 invalidate 方法重绘,从而循环绘制,直到滑动完成

7. ViewDragHelper 实现滑动

ViewDragHelper 是 Google 提供的一个类,通过 ViewDragHelper 来实现各种不同的滑动,拖放需求

  1. ViewGroup 中初始化,传入 ViewGroup 和 回调 CallBack ,Callback 中的方法返回值表示哪个 View 可以被移动
  2. 拦截事件,将 View 的触摸事件使用 ViewDragHelper 的方法拦截
  3. 重写 View 的 computeScroll 方法
  4. 在回调中重写监听方法,clampViewPositionHorizontal clampViewPositionVertical 实现横向纵向滑动
  5. 可以通过重写不同状态的回调实现在不同回调时的实现。

好啦,到这里有关 View 滑动的内容就介绍的差不多了,其中要区分滑动实现的是 View 控件的滑动,还是 View 内容的滑动。并且通过动画、Scroller、Handler/postDelay 等方式还可以实现弹性滑动的效果,普通滑动都是瞬时完成的,而弹性滑动则是渐进完成的,如果不太了解弹性滑动的只需要实现前面提到的几种滑动方式,一对比就明白啦。

请期待下篇文章触摸事件的分发和处理

推荐阅读更多精彩内容