UIScrollView
可以滚动显示宽度或高度大于其bounds
的内容。有些时候,需要有分页效果。每一页有统一的大小,相邻无缝水平或垂直排列。当水平或垂直滚动松开手后,会在其中一页完全显示的位置停下,滚动的距离是一页宽度或高度的整数倍。具体实现方法分两种情况讨论:分页大小等于、小于bounds
大小。分页大小大于bounds
大小的情况,不知道有什么应用场景,不讨论。
分页大小等于 bounds 大小
如果分页大小与 bounds 大小相等,把UIScrollView
的isPagingEnabled
属性设置为true
即可。此属性的官方解释
If the value of this property is true, the scroll view stops on multiples of the scroll view’s bounds when the user scrolls.
每一页的大小为bounds
的大小,每次水平或垂直滚动的距离是bounds
宽度或高度的整数倍。
分页大小小于 bounds 大小
用UIScrollView
和UICollectionView
实现的方法不一样,需要分别讨论。
代码已上传 GitHub:https://github.com/Silence-GitHub/PageScrollViewDemo
UIScrollView
的clipsToBounds
属性默认为true
,超出bounds
的子视图(超出部分)是看不到的。可以把clipsToBounds
设置为false
,把isPagingEnabled
设置为true
,把bounds
设置为需要的分页大小,在视觉上就基本达到分页效果了。然而,这样会出现的问题是:
- 滚动条只在
bounds
以内显示(所以分页效果只是视觉上“基本达到”)
UIScrollView
显示的内容会超出所在UIViewController
的view
所在范围,当UINavigationController
发生 push 或 pop 时,可能会看到超出部分,不美观
- 触摸
bounds
以外的区域没有响应
对于第 1 个问题,需要隐藏滚动条,把showsVerticalScrollIndicator
和showsHorizontalScrollIndicator
都设置为false
。既然要分页效果,滚动条就没必要显示。可以用UIPageControl
或自定义控件来显示当前分页在所有分页中的位置。非要显示滚动条的情况不讨论。
对于第 2 个问题,可以把当前所在UIViewController
的view
的clipsToBounds
设置为true
;或者把 scroll view 放在另一个UIView
上,把这个UIView
的clipsToBounds
设置为true
。
对于第 3 个问题,需要重载func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
方法。此方法的官方介绍
Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
此方法返回包含触摸点的最上层视图(UIView
),没有则返回nil
。触摸屏幕时,屏幕上的视图通过此方法寻找发生触摸的视图。
Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. This can occur if the current view’s clipsToBounds property is set to false and the affected subview extends beyond the view’s bounds.
当触摸点在bounds
之外,此方法返回nil
,表示当前视图不是发生触摸的视图。这就是问题的原因。需要自定义UIScrollView
,重载此方法,让此方法在bounds
之外触摸当前视图也返回被触摸的视图。自定义类PageScrollView
class PageScrollView: UIScrollView {
var interactionAreaNotInBounds: [CGRect] = [] // Use bounds coordinate system
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = false
isPagingEnabled = true
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Bounds is changed when scrolling
// Update interaction area not in bounds according to current bounds
let bounds = self.bounds
let areas = interactionAreaNotInBounds.map { (rect) -> CGRect in
return CGRect(x: bounds.minX + rect.minX,
y: bounds.minY + rect.minY,
width: rect.width,
height: rect.height)
}
// Find area contains point
for area in areas where area.contains(point) {
// Check subview
for subview in subviews {
// Convert point from current coordinate system to that of subview
let convertedPoint = convert(point, to: subview)
// Hit-test subview and return it if it is hit
if let view = subview.hitTest(convertedPoint, with: event) {
return view
}
}
// Return self if no subview is hit
return self
}
// No area contains point
// Do super hit-test
return super.hitTest(point, with: event)
}
}
初始化PageScrollView
并确定frame
或bounds
后,需要给interactionAreaNotInBounds
属性赋值。把bounds
之