UIScrollView
是iOS中很有意思也是最常用的控件之一,同时也是为数不多可以响应手势的UIKit控件。本篇文章主要研究的是UIScrollView
的分页实现。分页有多种实现方法,且各自的效果和优点各不相同,但它们都依赖于UIScrollView
的代理方法,所以,我们先从UIScrollViewDelegate
说起。
UIScrollViewDelegate
UIScrollView
有意思的功能都是通过它的 delegate
方法实现的。了解这些方法被触发的条件及调用的顺序对于使用 UIScrollView
是很有必要的。
|
|
这个方法在任何方式触发 contentOffset
变化的时候都会被调用(包括用户拖动,减速过程,直接通过代码设置等),可以用于监控 contentOffset
的变化,并根据当前的 contentOffset
对其他 view 做出随动调整。
|
|
用户开始拖动 scroll view 的时候被调用。
|
|
该方法从 iOS 5 引入,在 didEndDragging
前被调用,当 willEndDragging
方法中 velocity
为 CGPointZero
(结束拖动时两个方向都没有速度)时,didEndDragging
中的 decelerate
为 NO,即没有减速过程,willBeginDecelerating
和 didEndDecelerating
也就不会被调用。反之,当 velocity
不为 CGPointZero
时,scroll view 会以 velocity
为初速度,减速直到 targetContentOffset
。值得注意的是,这里的 targetContentOffset
是个指针,没错,你可以改变减速运动的目的地,这在一些效果的实现时十分有用,实例章节中会具体提到它的用法,并和其他实现方式作比较。
|
|
在用户结束拖动后被调用,decelerate
为 YES
时,结束拖动后会有减速过程。注,在 didEndDragging
之后,如果有减速过程,scroll view
的 dragging
并不会立即置为 NO,而是要等到减速结束之后,所以这个 dragging 属性的实际语义更接近 scrolling。
|
|
减速动画开始前被调用。
|
|
减速动画结束时被调用,这里有一种特殊情况:当一次减速动画尚未结束的时候再次drag scroll view
,didEndDecelerating
不会被调用,并且这时 scroll view 的 dragging
和 decelerating
属性都是 YES。新的 dragging 如果有加速度,那么 willBeginDecelerating
会再一次被调用,然后才是 didEndDecelerating
;如果没有加速度,虽然 willBeginDecelerating
不会被调用,但前一次留下的 didEndDecelerating
会被调用,所以连续快速滚动一个 scroll view
时,delegate 方法被调用的顺序(不含 didScroll)可能是这样的:
scrollViewWillBeginDragging:
scrollViewWillEndDragging: withVelocity: targetContentOffset:
scrollViewDidEndDragging: willDecelerate:
scrollViewWillBeginDecelerating:
scrollViewWillBeginDragging:
scrollViewWillEndDragging: withVelocity: targetContentOffset:
scrollViewDidEndDragging: willDecelerate:
scrollViewWillBeginDecelerating:
…
scrollViewWillBeginDragging:
scrollViewWillEndDragging: withVelocity: targetContentOffset:
scrollViewDidEndDragging: willDecelerate:
scrollViewWillBeginDecelerating:
scrollViewDidEndDecelerating:
虽然很少有因为这个导致的 bug,但是你需要知道这种很常见的用户操作会导致的中间状态。例如你尝试在 UITableViewDataSource
的 tableView:cellForRowAtIndexPath:
方法中基于 tableView 的 dragging 和 decelerating 属性判断是在用户拖拽还是减速过程中的话可能会误判(见例 1)。
了解了UIScrollView
的核心代理方法,接下来就来看看UIScrollView
的几种分页方式。
pagingEnabled
这是系统提供的分页方式,最简单,但是有一些局限性:
- 只能以 frame size 为单位翻页,减速动画阻尼大,减速过程不超过一页
- 需要一些 hacking 实现 bleeding 和 padding(即页与页之间有 padding,在当前页可以看到前后页的部分内容)
Sample 中 Pagination 有简单实现 bleeding 和 padding 效果的代码,主要的思路是:
让 scroll view
的宽度为 page 宽度 + padding,并且设置 clipsToBounds
为 NO
这样虽然能看到前后页的内容,但是无法响应 touch,所以需要另一个覆盖期望的可触摸区域的 view 来实现类似 touch bridging
的功能
适用场景:*上述局限性同时也是这种实现方式的优点,比如一般 App 的引导页(教程),Calendar 里的月视图,都可以用这种方法实现。
Snap
这种方法就是在 didEndDragging
且无减速动画,或在减速动画完成时,snap 到一个整数页。核心算法是通过当前 contentOffset
计算最近的整数页及其对应的 contentOffset
,通过动画 snap 到该页。这个方法实现的效果都有个通病,就是最后的 snap 会在 decelerate
结束以后才发生,总感觉很突兀。
修改targetContentOffset
通过修改 scrollViewWillEndDragging: withVelocity: targetContentOffset:
方法中的 targetContentOffset
直接修改目标 offset 为整数页位置。其中核心代码:
|
|
适用场景:方法 2 和 方法 3 的原理近似,效果也相近,适用场景也基本相同,但方法 3 的体验会好很多,snap 到整数页的过程很自然,或者说用户完全感知不到 snap 过程的存在。这两种方法的减速过程流畅,适用于一屏有多页,但需要按整数页滑动的场景;也适用于如图表中自动 snap 到整数天的场景;还适用于每页大小不同的情况下 snap 到整数页的场景(不做举例,自行发挥,其实只需要修改计算目标 offset 的方法)。
完整代码参见 Pagination