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 设置为需要的分页大小,在视觉上就基本达到分页效果了。然而,这样会出现的问题是:

  1. 滚动条只在 bounds 以内显示(所以分页效果只是视觉上“基本达到”)
  2. UIScrollView 显示的内容会超出所在 UIViewController 的 view 所在范围,当 UINavigationController 发生 push 或 pop 时,可能会看到超出部分,不美观
  3. 触摸 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 systemoverride init(frame: CGRect) {super.init(frame: frame)clipsToBounds = falseisPagingEnabled = trueshowsVerticalScrollIndicator = falseshowsHorizontalScrollIndicator = 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 boundslet bounds = self.boundslet areas = interactionAreaNotInBounds.map { (rect) -> CGRect inreturn CGRect(x: bounds.minX + rect.minX,y: bounds.minY + rect.minY,width: rect.width,height: rect.height)}// Find area contains pointfor area in areas where area.contains(point) {// Check subviewfor subview in subviews {// Convert point from current coordinate system to that of subviewlet convertedPoint = convert(point, to: subview)// Hit-test subview and return it if it is hitif let view = subview.hitTest(convertedPoint, with: event) {return view}}// Return self if no subview is hitreturn self}// No area contains point// Do super hit-testreturn 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 = 100func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {// Destination xlet x = targetContentOffset.pointee.x// Page width equals to cell widthlet pageWidth = cellWidth// Check which way to movelet movedX = x - pageWidth * CGFloat(selectedIndex)if movedX < -pageWidth * 0.5 {// Move leftselectedIndex -= 1} else if movedX > pageWidth * 0.5 {// Move rightselectedIndex += 1}if abs(velocity.x) >= 2 {targetContentOffset.pointee.x = pageWidth * CGFloat(selectedIndex)} else {// If velocity is too slow, stop and move with default velocitytargetContentOffset.pointee.x = scrollView.contentOffset.xscrollView.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 = 100func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {// Destination xlet x = targetContentOffset.pointee.x// Page width equals to cell widthlet pageWidth = cellWidth// Destination page indexvar index = Int(x / pageWidth)// Check whether to move to next pagelet divideX = CGFloat(index) * pageWidth + pageWidth * 0.5if x > divideX {// Should move to next pageindex += 1}// Move to destinationtargetContentOffset.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(_:) 方法中,让 collection view 跟着 scroll view 滚动。如果要 collection view 响应选中 cell 等操作,需要写其他的代码。

这个方法比较麻烦,要把对 scroll view 的手势传给 collection view,每次刷新数据都要重新设置 scroll view 的 contentSize。具体见 GitHub:https://github.com/Silence-GitHub/PageScrollViewDemo

转载请注明出处:http://www.cnblogs.com/silence-cnblogs/p/6529728.html

转载于:https://www.cnblogs.com/silence-cnblogs/p/6529728.html

UIScrollView 和 UICollectionView 分页效果相关推荐

  1. 如何用php实现分页效果

    分页效果在网页中是常见的,可是怎样才能实现分页呢,今天做了两种方法来实现一下分页的效果 首先,我们需要准备在数据库里面准备一个表,并且插入数据,这些都是必需的前提工作了,不多说,如图所示(库名为jer ...

  2. 给UIScrollView添加category实现UIScrollView的轮播效果

    给UIScrollView添加category实现UIScrollView的轮播效果 大家都知道,要给category添加属性是必须通过runtime来实现的,本教程中给UIScrollView添加c ...

  3. php+mysql 大容量数据高效分页效果(弃用limit)

    发现当表中有很多上万条数据时,越后的数据用limit分页显示就越慢(>2秒),可能是mysql的特性所致.所以花了点时间总结实现了更优解决方案,最终实现毫秒级响应.若网友有更优的,请留言,谢谢! ...

  4. 原生JS实现分页效果1.0

    不太完整,写的太急,等等加上完整注释,写起来还是有些难度的,写的有点水,后面再改进改进. <!DOCTYPE html> <html lang="en"> ...

  5. android 之ListView分页效果以及从网络上加载数据一系列的综合运用

    数据分页策略: <1>:用多少查多少 <2>:全部查询出来,再进行分页处理 数据分页的有关算法: (1):起始索引值 = (当前页-1)*每页显示的记录数 (2):结束索引值 ...

  6. pdo mysql分页_php运用PDO连接数据库,实现分页效果

    PDO是一个"数据库访问抽象层",作用是统一各种数据库的访问接口,与mysql和mysqli的函数库相比,PDO让跨数据库的使用更具有亲和力:与ADODB和MDB2相比,PDO更高 ...

  7. element 修改分页样式_如何给wordpress网站的文章列表,添加分页效果?可以通过2种方式...

    在上一章节中,我们为wordpress网站的首页添加了全站文章列表.一个wordpress网站不可能只有几篇文章,可能会有成千上万的文章,作为一个wordpress博客主题模板,一般情况下,不可能让所 ...

  8. 【MyBatis】Mybatis实现分页效果

    源代码:GitHub链接 环境: Eclipse项目 web 3.0 Tomcat 8.5 用到的技术: MyBatis Log4J JSTL 基本的-Java Servlet,JSP,- 使用方式: ...

  9. react ajax 分页,React实现分页效果

    本文实例为大家分享了React实现分页效果的具体代码,供大家参考,具体内容如下 首先确保已经安装了antd,axios jsx文件: import React, { useState, useEffe ...

  10. rax+react hook 实现分页效果

    'use strict'; import { createElement, useState, useEffect, memo } from 'rax'; import View from 'rax- ...

最新文章

  1. Exchange2007 中Send as 与Send on behalf of 讲解
  2. [unreal4入门系列之十四] 在UE4中添加碰撞触发事件
  3. Jetty 开发指南:嵌入式开发示例
  4. 欧洲顶级云数据中心着火,损失惨重!筑牢数据中心“防火墙”,可靠才是王道!...
  5. 快速幂模板(java)
  6. iOS之深入解析静态库和动态库
  7. Codeforces Round #757 (Div. 2)
  8. 【转】Azure DevOps —— Azure Board 之 长篇故事、特性、用户情景(故事)的用法应用场景
  9. NodeJs学习笔记002--npm常用命令详解
  10. Springboot启动报错Error handling failed
  11. html5跳转页面接收参数,HTML页面跳转及参数传递问题
  12. 一个文科毕业生在德国 IT 行业的漫漫求职路
  13. ubuntu 命令卡住_安装Win10和Ubuntu双系统
  14. css 设置背景图片模糊效果
  15. 2016最新的旅游网站程序CMS系统优点和缺点对比分析
  16. Python 汉字转化成拼音
  17. 轻松创建FB和Ins故事广告,提升内容曝光率
  18. 屏下指纹版iPhone终于稳了!效果很好很强大
  19. yolov5 【v4.0】用自有数据集训练结果大比拼【5s,5m,5l,5x,5shpy】
  20. Android 实现GIF播放的几种方式

热门文章

  1. latex 伪代码 return怎么写 不换行怎么办
  2. 树分解 tree decomposition
  3. DevOps使用教程 华为云(8)代码托管 代码仓库 git协作开发
  4. JDBC06 其他操作及批处理Batch
  5. 2021-03-08
  6. mysql 慢查询及深入调优
  7. 开局崩盘!IDEA 2020 无法启动的解决办法|赠送 IDEA 2020 新功能
  8. K3s(Kubernetes)环境使用Let‘s Encrypt证书的部署及自动配置https域名-阿里云域名解析管理
  9. Linux Ubuntu/Centos7 定时备份mysql数据库
  10. CentOS 7.5 重置 root 密码