Kealdish's Studio.

【转】UIScrollView实践小记——分页

字数统计: 1.4k阅读时长: 5 min
2016/03/21 Share

UIScrollView是iOS中很有意思也是最常用的控件之一,同时也是为数不多可以响应手势的UIKit控件。本篇文章主要研究的是UIScrollView的分页实现。分页有多种实现方法,且各自的效果和优点各不相同,但它们都依赖于UIScrollView的代理方法,所以,我们先从UIScrollViewDelegate说起。

UIScrollViewDelegate

UIScrollView有意思的功能都是通过它的 delegate 方法实现的。了解这些方法被触发的条件及调用的顺序对于使用 UIScrollView 是很有必要的。

1
- (void)scrollViewDidScroll:(UIScrollView *)scrollView

这个方法在任何方式触发 contentOffset 变化的时候都会被调用(包括用户拖动,减速过程,直接通过代码设置等),可以用于监控 contentOffset 的变化,并根据当前的 contentOffset 对其他 view 做出随动调整。

1
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView

用户开始拖动 scroll view 的时候被调用。

1
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

该方法从 iOS 5 引入,在 didEndDragging 前被调用,当 willEndDragging 方法中 velocityCGPointZero(结束拖动时两个方向都没有速度)时,didEndDragging 中的 decelerate 为 NO,即没有减速过程,willBeginDeceleratingdidEndDecelerating 也就不会被调用。反之,当 velocity 不为 CGPointZero 时,scroll view 会以 velocity 为初速度,减速直到 targetContentOffset。值得注意的是,这里的 targetContentOffset 是个指针,没错,你可以改变减速运动的目的地,这在一些效果的实现时十分有用,实例章节中会具体提到它的用法,并和其他实现方式作比较。

1
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

在用户结束拖动后被调用,decelerateYES 时,结束拖动后会有减速过程。注,在 didEndDragging 之后,如果有减速过程,scroll viewdragging并不会立即置为 NO,而是要等到减速结束之后,所以这个 dragging 属性的实际语义更接近 scrolling

1
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView

减速动画开始前被调用。

1
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

减速动画结束时被调用,这里有一种特殊情况:当一次减速动画尚未结束的时候再次drag scroll viewdidEndDecelerating 不会被调用,并且这时 scroll view 的 draggingdecelerating 属性都是 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,但是你需要知道这种很常见的用户操作会导致的中间状态。例如你尝试在 UITableViewDataSourcetableView: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 为整数页位置。其中核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (CGPoint)nearestTargetOffsetForOffset:(CGPoint)offset
{
CGFloat pageSize = BUBBLE_DIAMETER + BUBBLE_PADDING;
NSInteger page = roundf(offset.x / pageSize);
CGFloat targetX = pageSize * page;
return CGPointMake(targetX, offset.y);
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGPoint targetOffset = [self nearestTargetOffsetForOffset:*targetContentOffset];
targetContentOffset->x = targetOffset.x;
targetContentOffset->y = targetOffset.y;
}

适用场景:方法 2 和 方法 3 的原理近似,效果也相近,适用场景也基本相同,但方法 3 的体验会好很多,snap 到整数页的过程很自然,或者说用户完全感知不到 snap 过程的存在。这两种方法的减速过程流畅,适用于一屏有多页,但需要按整数页滑动的场景;也适用于如图表中自动 snap 到整数天的场景;还适用于每页大小不同的情况下 snap 到整数页的场景(不做举例,自行发挥,其实只需要修改计算目标 offset 的方法)。

完整代码参见 Pagination

CATALOG
  1. 1. UIScrollViewDelegate
  2. 2. pagingEnabled
  3. 3. Snap
  4. 4. 修改targetContentOffset