利用 UIScrollView 有多种方法实现分页,但是各自的效果和用途不尽相同,其中方法 2 和方法 3 的区别也正是一些同类 App 在模仿 Glow 的首页 Bubble 翻转效果时跟 Glow 体验上的的差距所在(但愿他们不会看到本文并且调整他们的实现方式)。本例通过三种方法实现相似的一个场景,你可以通过安装到手机上来感受三种实现方式的不同用户体验。
1.pagingEnabled
这是系统提供的分页方式,最简单,但是有一些局限性:
<li>只能以 frame size 为单位翻页,减速动画阻尼大,减速过程不超过一页
<li>需要一些 hacking 实现 bleeding 和 padding(即页与页之间有 padding,在当前页可以看到前后页的部分内容)
有简单实现 bleeding 和 padding 效果的代码,主要的思路是:
<li>让 scroll view 的宽度为 page 宽度 + padding,并且设置 clipsToBounds 为 NO
<li>这样虽然能看到前后页的内容,但是无法响应 touch,所以需要另一个覆盖期望的可触摸区域的 view 来实现类似 touch bridging 的功能
适用场景:上述局限性同时也是这种实现方式的优点,比如一般 App 的引导页(教程),Calendar 里的月视图,都可以用这种方法实现。
2.Snap
这种方法就是在 didEndDragging 且无减速动画,或在减速动画完成时,snap 到一个整数页。核心算法是通过当前 contentOffset 计算最近的整数页及其对应的 contentOffset,通过动画 snap 到该页。这个方法实现的效果都有个通病,就是最后的 snap 会在 decelerate 结束以后才发生,总感觉很突兀。
修改 targetContentOffset
通过修改 scrollViewWillEndDragging: withVelocity: targetContentOffset: 方法中的 targetContentOffset 直接修改目标 offset 为整数页位置。其中核心代码:
func nearestTargetOffsetForOffset(offset:CGPoint)->CGPoint{
var pageSize:CGFloat = BUBBLE_DIAMETER2+BUBBLE_PADDING2
var page:Int = Int(roundf(Float(offset.x) / Float(pageSize)))
var targetX:CGFloat = CGFloat(pageSize) * CGFloat(page)
return CGPointMake(targetX, offset.y)
}
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
var targetOffset:CGPoint = self.nearestTargetOffsetForOffset(targetContentOffset.memory)
targetContentOffset.memory.x = targetOffset.x
targetContentOffset.memory.y = targetOffset.y
}
适用场景:方法 2 和 方法 3 的原理近似,效果也相近,适用场景也基本相同,但方法 3 的体验会好很多,snap 到整数页的过程很自然,或者说用户完全感知不到 snap 过程的存在。这两种方法的减速过程流畅,适用于一屏有多页,但需要按整数页滑动的场景;也适用于如图表中自动 snap 到整数天的场景;还适用于每页大小不同的情况下 snap 到整数页的场景(不做举例,自行发挥,其实只需要修改计算目标 offset 的方法)。