UIScrollView 和 UICollectionView 分页效果
UIScrollView 和 UICollectionView 分页效果
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 分页
UIScrollView 的 clipsToBounds 属性默认为 true,超出 bounds 的子视图(超出部分)是看不到的。可以把 clipsToBounds 设置为 false,把 isPagingEnabled 设置为 true,把 bounds 设置为需要的分页大小,在视觉上就基本达到分页效果了。然而,这样会出现的问题是:
- 滚动条只在 bounds 以内显示(所以分页效果只是视觉上“基本达到”)
- UIScrollView 显示的内容会超出所在 UIViewController 的 view 所在范围,当 UINavigationController 发生 push 或 pop 时,可能会看到超出部分,不美观
- 触摸 bounds 以外的区域没有响应
对于第 1 个问题,可以设置 scrollIndicatorInsets 属性的值,调整滚动条位置。或者隐藏滚动条,把 showsVerticalScrollIndicator 和 showsHorizontalScrollIndicator 都设置为 false。可以用 UIPageControl 或自定义控件来显示当前分页在所有分页中的位置。
对于第 2 个问题,可以把当前所在 UIViewController 的 view 的 clipsToBounds 设置为 true;或者把 scroll view 放在另一个 UIView 上,把这个 UIView 的 clipsToBounds 设置为 true。
对于第 3 个问题,需要重载 hitTest(_:with:) 方法。此方法的官方介绍
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 之外会响应触摸的区域(用 bounds 最初的坐标)写成数组进行赋值。例如,frame 为 (30, 0, 100, 100),要让左边宽 30、高 100 的区域为响应区域,则给 interactionAreaNotInBounds 赋值为 [CGRect(x: -30, y: 0, width: 30, height: 100)]。
当要分页的页数较少、每页内容不多的时候,可以用这个方法实现。如果要显示很多页的内容,一次把所有分页视图加到 scroll view 上,影响性能。这种情况可以用 UICollectionView 实现,UICollectionViewCell 是重用的,节约资源。用 UICollectionView 实现的方法不同。
UICollectionView 分页
如果 UICollectionView 用以上的方法实现,出现的问题是,不在 bounds 之内的 UICollectionViewCell 可能消失。因为 cell 是重用的,移出 bounds 之后可能就被移除而准备重用。UICollectionView 继承自 UIScrollView,可以通过 UIScrollViewDelegate 的方法,模拟分页效果。具体实现方法与分页大小有关。
分页较大
当分页较大时,比如水平滚动,一页宽度大于屏幕宽度一半,每次滚动的最远距离就限制到相邻分页。这样的限制与 isPagingEnabled 的效果基本符合。实现 UIScrollViewDelegate 的一个方法即可。
private var selectedIndex: Int = 0 // index of page displayed
private let cellWidth: CGFloat = UIScreen.main.bounds.width - 100
private let cellHeight: CGFloat = 100
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Destination x
let x = targetContentOffset.pointee.x
// Page width equals to cell width
let pageWidth = cellWidth
// Check which way to move
let movedX = x - pageWidth * CGFloat(selectedIndex)
if movedX < -pageWidth * 0.5 {
// Move left
selectedIndex -= 1
} else if movedX > pageWidth * 0.5 {
// Move right
selectedIndex += 1
}
if abs(velocity.x) >= 2 {
targetContentOffset.pointee.x = pageWidth * CGFloat(selectedIndex)
} else {
// If velocity is too slow, stop and move with default velocity
targetContentOffset.pointee.x = scrollView.contentOffset.x
scrollView.setContentOffset(CGPoint(x: pageWidth * CGFloat(selectedIndex), y: scrollView.contentOffset.y), animated: true)
}
}
selectedIndex 表示当前分页序号,默认显示最左边的一页,因此初始化为 0。如果最开始显示其他页,需要改变selectedIndex 的值。通过 selectedIndex 的值,将要停下来的坐标 x,计算出位移 movedX。当位移绝对值大于分页宽度的一半时,滚动到位移方向的相邻页。
给 targetContentOffset.pointee.x 赋值,改变滚动终点的 x 坐标。宽度较大的分页效果滚动速率不能太慢,所以当速率小于 2 时,给 targetContentOffset.pointee.x 赋值为当前位置即停止滚动,调用 setContentOffset(_:animated:) 方法,立即以默认速度滚动到终点。
现在,还有一个小问题,就是滚动到最后一页时,滚动停止的位置不固定。最后一页停止的位置有时候靠屏幕左边,有时靠右。从最后一页往回滚动可能会有点奇怪(突然加速)。解决办法是增加一个 UICollectionViewCell 放到最后,cell 的宽度为屏幕宽度减分页宽度,使最后一页滚动的停止位置都靠屏幕左边。假设分页数量(UICollectionViewCell 的数量)为 numberOfItems,以下是 cell 的大小
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch indexPath.item {
case numberOfItems:
return CGSize(width: UIScreen.main.bounds.width - cellWidth, height: cellHeight)
default:
return CGSize(width: cellWidth, height: cellHeight)
}
}
分页较小
当分页较小时,屏幕宽度可以显示好几个分页,就不能把滚动距离限制到相邻分页。直接判断滚动终点离哪个分页比较近,以近的分页为终点。
private let cellWidth: CGFloat = 100
private let cellHeight: CGFloat = 100
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Destination x
let x = targetContentOffset.pointee.x
// Page width equals to cell width
let pageWidth = cellWidth
// Destination page index
var index = Int(x / pageWidth)
// Check whether to move to next page
let divideX = CGFloat(index) * pageWidth + pageWidth * 0.5
if x > divideX {
// Should move to next page
index += 1
}
// Move to destination
targetContentOffset.pointee.x = pageWidth * CGFloat(index)
}
同样需要在最后增加一个 cell,防止滚动到最后一页出问题。假设屏幕宽度最多能容纳 n 个 cell (n + 1 个就超出屏幕),那么 cell 的宽度为屏幕宽度减 n 个 cell 的宽度。以下是 cell 的大小
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch indexPath.item {
case numberOfItems:
let n = Int(UIScreen.main.bounds.width / cellWidth)
let d = UIScreen.main.bounds.width - cellWidth * CGFloat(n)
return CGSize(width: d, height: cellHeight)
default:
return CGSize(width: cellWidth, height: cellHeight)
}
}
现在滚动效果的问题是,从松开手到停止滚动的时间太长。加上一句代码就能解决
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
decelerationRate 是 UIScrollView 的属性,设置为 UIScrollViewDecelerationRateFast,表示滚动松开手后减速更快(加速度与速度方向相反,加速度的绝对值增大),因而滚动会很快减速并停止。
UIScrollView + UICollectionView 分页
如果一定要 UICollectionView 显示分页内容,并且完全有 isPagingEnabled 为 true 的分页效果,可以结合 UIScrollView 来实现。以下是大概思路。
把 UICollectionView 放在底部,正常显示内容。把上文自定义的 PageScrollView 放在顶部,响应触摸范围为 UICollectionView 的范围,设置 UIScrollView 的 contentSize。触摸发生在 scroll view 上。在 UIScrollViewDelegate 的 scrollViewDidScroll(_
UIScrollView 和 UICollectionView 分页效果的更多相关文章
- 原生JS实现分页效果2.0(新增了上一页和下一页,添加当前元素样式)
虽然写的很烂,但至少全部都是自己写的,因为这个没有固定的顺序,所以就没有封装,如果你技术好的话,可以你写的分享给我,谢谢. <!DOCTYPE html><html lang=&qu ...
- 原生JS实现分页效果1.0
不太完整,写的太急,等等加上完整注释,写起来还是有些难度的,写的有点水,后面再改进改进. <!DOCTYPE html><html lang="en">&l ...
- ViewPager+GridView实现首页导航栏布局分页效果
如图是效果图用ViewPager+GridView实现首页导航栏布局分页效果来实现的效果 Demo下载地址:http://download.csdn.net/detail/qq_29774291/96 ...
- 分享5种风格的 jQuery 分页效果【附代码】
jPaginate 是一款非常精致的分页插件,提供了五种不同风格的分页效果,支持鼠标悬停翻页,快速分页功能.这款插件还提供了丰富的配置选项,你可以根据需要进行设置. 效果演示 源码下载 各个 ...
- thinkphp自定义分页效果
TP自带了一个分页函数,挺方便使用的. 下面是我的使用方法: /*****************分页显示start*************************/ $arr_page=$this ...
- 5种风格的 jQuery 分页效果【附代码】
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xht ...
- thinkphp ajax 无刷新分页效果的实现
思路:先做出传统分页效果,然后重新复制一份Page.class.php类,对它进行修改,把js中的函数传到page类中,把上一页.下一页.首页.尾页.链接页中的url地址改成js控制的函数,模板页面中 ...
- PHP实现仿Google分页效果的分页函数
本文实例讲述了PHP实现仿Google分页效果的分页函数.分享给大家供大家参考.具体如下: /** * 分页函数 * @param int $total 总页数 * @param int $pages ...
- .NET中的repeater简介及分页效果
Repeater控件是一个数据绑定容器控件,它能够生成各个项的列表,并可以使用模板定义网页上各个项的布局.当该页运行时,该控件为数据源中的每个项重复此布局. 配合模板使用repeater控件 若要使 ...
随机推荐
- JavaScript特效制作经典精讲(案例入门详解、可直接粘贴拷贝运行、史上最牛案例)
技巧一.添加链接提示 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:// ...
- JDBC连接SQL Server 2005步骤详解
一.设置SQL Server服务器: 1.“开始” → “程序” → “Microsoft SQL Server 2005” → “配置工具” → “SQL Server Configurati ...
- LNAMP 中的PHP探针
<?php /* ----------------本探针基于YaHei.net探针------------------- */ error_reporting(0); //抑制所有错误信息 @h ...
- FMS+NGINX打造高带宽利用率的流媒体(音频+视频)环境
fms自身已经拥有了httpd,用来给客户端访问用,例如通过http的音频播放.众所周知,非专业的httpd自然有不专业之处,例如我遇到的情况就是经常http服务假死,或者在访问量庞大的时候会无缘无故 ...
- python中将两个list合并为字典
两个list合并为字典的代码如下: def Run(): list2 = [1, 2, 3, 4, 5 ]; list3 = ["a", "b", " ...
- Windows下Python读取GRIB数据
之前写了一篇<基于Python的GRIB数据可视化>的文章,好多博友在评论里问我Windows系统下如何读取GRIB数据,在这里我做一下说明. 一.在Windows下Python为什么无法 ...
- Backdoor CTF 2013: 电子取证 250
0x00 题目 h4x0r厌烦了你对他的城堡的所有攻击,所以他决定报复攻击你,他给你发来一封带有图片的邮件作为警告,希望你能找出他的警告消息:-) 消息的MD5值就是flag. 0x01 解题法1 给 ...
- Codeforces Round #257 (Div. 1)A~C(DIV.2-C~E)题解
今天老师(orz sansirowaltz)让我们做了很久之前的一场Codeforces Round #257 (Div. 1),这里给出A~C的题解,对应DIV2的C~E. A.Jzzhu and ...
- asp.net权限认证篇外:集成域账号登录
在之前的我们已经讲过asp.net权限认证:Windows认证,现在我们来讲讲域账号登录, 这不是同一件事哦,windows认证更多的是对资源访问的一种权限管控,而域账号登录更多的是针对用户登录的认证 ...
- Eclipse / Intellij Idea配置Git+Maven+Jetty开发环境
作者:鹿丸不会多项式 出处:http://www.cnblogs.com/hechao123 转载请先与我联系. 最近公司给加配了Mac,本想着花一个小时的时间搭好开发环境,最后全部弄好却用了一上午 ...