##背景

网上有很多使用Storyboard完成UIScrollview的例子,但是纯代码的例子却不多。有限的一些例子大多也是外国开发者用VFL写的。而这篇文章基于swift语言和SnapKit分析了如何用纯代码加Autolayout写UIScrollview,完整代码已经上传到我的github。

在正文中,我会分析其中的关键代码。对于Autolayout,绝对不可取的态度是不停的试几个约束,一旦发现好用,也不管其原理,就放手不管了。事实上,我们写的每一个约束,都要明白它存在的价值是什么,要做到不写一个无用的约束,不漏一个必要的约束,明白为什么某种写法有效,而另一种写法就无效。

废话不多说,估计大家用UIScrollView时,都有过被Autolayout坑的经历,要么是布局不对,要么不能滑动,以及其他匪夷所思的bug。这与Autolayout和UIScrollView各自的特性有关。

理论分析

首先,我们知道Autolayout改变了传统的以frame为主的布局思想。它其实是一种相对布局,核心思想是视图与视图之间的位置关系。比如,我们可以根据矩形的起始横坐标、纵坐标、长和宽这四个变量确定它的位置。或者,如果已经确定矩形A的位置,只要知道矩形B每条边的和A对应边之间的距离,也能确定B的位置。前者就是frame的思想,它基于绝对数值,而后者是Autolayout的思想,它基于偏移量的概念。

其次,UIScrollView有自己的frame也就是我们在屏幕上能看到的区域。它还有一个contentSize的概念。在使用frame布局的时候,我们一般先设置好子视图的位置,最后再设置contentSize,它会将所有的子视图包含在内。于是通过滑动,我们就可以在有限的布局中,看到所有的内容了。

但是在Autolayout时代,为了简化布局,我们希望contentSize能够自动设置。比如有一个scrollView,它有两个子视图。frame分别为(x: 0, y: 0, width: 10, height: 10)和(x: 10, y: 0, width: 10, height: 10),那么我们自然会认为这两个视图左右并排排列,contentSize为(x: 0, y: 0, width: 20, height: 10):

这种把若干个子视图合并,得出contentSize的能力,人类是天生具备的,但是计算机却不是这样。仅凭以上信息,程序无法推断出真正的contentSize。原因在于,我们没有明确的告诉系统,在这两个子视图拼接而成的区域以外,还有没有区域应该被contentSize包含。

也就是说,contentSize也有可能是下图中的阴影部分:

如果需要指定contentSize就是两个正方形拼接而成的区域,我们还需要提供四个信息:

  1. 左边的正方形的左侧的边,距离contentSize左边的距离为0
  2. 右边的正方形的右侧的边,距离contentSize右边的距离为0

……

通过以上的分析,我们可以看到,其实contentSize是依赖于子视图自身的大小,和上下左右四个方向的留白大小计算出的。而UIScrollView的leading/trailing/top/bottom是相对于它的contentSize而不是bounds来确定的。所以如果你写这样的代码,布局是肯定不会生效的:

subview.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(scrollView).offset(5)
}
复制代码

因为我们其实是在根据UIScrollView的leading/trailing/top/bottom来确定子视图的位置,而我们已经分析过,UIScrollView的leading/trailing/top/bottom是相对于自己的contentSize而言的。而contentSize又是根据子视图位置决定的。这就变成了一种你依赖我,我又依赖你的情况。

为了打破这种循环依赖,为子视图添加约束的两个要求是:

  1. 它不依赖于任何与scrollview有关布局,也就是不能参考scrollview的位置和大小。
  2. 它不仅要确定过自己的大小,还要确定自己与contentSize四周的距离。

第二个要求意思是说,正常使用autolayout时,我们确定一个矩形在水平方向上的范围,只要知道它的左边距离它左边的矩形有多远,以及它有多宽即可。但是在UIScrollView中布局时,还需要告诉UIScrollView,它的右边距离右边的视图有多远。这样contentSize才能确定。否则UIScrollView就不知道contentSize向右可以延伸多少。在竖直方向上也是同理。

**这两大要求一定要牢记!**接下来我们的代码都将围绕如何满足这两大要求展开。

动手实践

明白了问题的理论背景后,我们通过一个具体的需求,来看看正确的代码怎么写,以下面这个效果为例:

如图所示,中间是一个UIScrollView,它的背景颜色是黄色。红色部分我们称之为box,它是一个普通的,红色背景的UIView。也就是说我们向UIScrollView中添加了多个box,每个子box之间间隔一定距离。我们分步实现这个功能

使用container

首先我们介绍一种使用Container的方法。

###第一步:为scrollView添加约束

let scrollView = UIScrollView()
view.addSubview(scrollView)
scrollView.snp_makeConstraints { (make) -> Void in
make.centerY.equalTo(view.snp_centerY)
make.left.right.equalTo(view)
make.height.equalTo(topScrollHeight)
}
复制代码

我们之前说过,使用Autolayout时,不用考虑frame布局。所以直接创建一个scrollView对象。需要先把scrollView添加到父视图上才能添加约束。

scrollView添加约束没有什么难点,就像我们给其他视图添加约束一样。这里表示scrollView和父视图左右对齐,居中显示。

###第二步:为container添加约束

scrollView.addSubview(containerView)
containerView.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(scrollView)
make.height.equalTo(topScrollHeight)
}
复制代码

这里对container的约束非常重要,第一个约束表示自己上、下、左、右和contentSize的距离为0,因此只要container的大小确定,contentSize也就可以确定了,因为此时它和container大小、位置完全相同。

第二个约束直接通过一个数值,确定container的高度。避免了依赖scrollview布局。这样一来,scrollview就变成水平的了。container的宽度直接决定了scrollview的宽度。

第三步:添加box

for i in 0...5 {
let box = UIView()
containerView.addSubview(box)box.snp_makeConstraints(closure: { (make) -> Void in
make.top.height.equalTo(containerView)  // 确定top和height之后,box在竖直方向上完全确定
make.width.equalTo(boxWidth)        //确定width后,只要再确定left,就可以在水平方向上完全确定
if i == 0 {
make.left.equalTo(containerView).offset(boxGap / 2)  //第一个box的left单独处理
}
else if let previousBox = containerView.subviews[i - 1] as? UIView{
make.left.equalTo(previousBox.snp_right).offset(boxGap)  // 在前一个box右侧15个距离
}
if i == 5 {
containerView.snp_makeConstraints(closure: { (make) -> Void in
make.right.equalTo(box)  // 确定container的右侧边界。
})
}
})
}
复制代码

box的约束看似复杂,其实非常简单。因为scrollview在Autolayout下的布局,难点就在于子视图布局时约束比较多。但现在,我们通过一个container已经隔离了,也就说我们又回归了常规的Autolayout布局。以水平方向为例,我们只要确定leftwidth即可。

在最后一个if语句中,我们为container添加了右侧的约束。这样就确定了container的宽度。由于container封装了所有的box,所以对于scrollview来说,它的子视图只有一个,就是container,而container自身的大小,上下左右四个方向和contentSize距离在之前的约束中已经被定义为0,contentSize也就可以确定了。

##使用外部视图

除了使用container以外,我们还可以使用外部的视图确定子视图的位置。这种方法,步骤较少,和之前一样,第一步是创建scrollView并添加约束。接下来我们直接添加子视图:

box.snp_makeConstraints(closure: { (make) -> Void in
make.top.equalTo(0)
make.bottom.equalTo(view).offset(-(ScreenHeight - topScrollHeight) / 2)  // This bottom can be incorret when device is rotated
make.height.equalTo(topScrollHeight)make.width.equalTo(boxWidth)
if i == 0 {
make.left.equalTo(boxGap / 2)
}
else if let previousBox = scrollView.subviews[i - 1] as? UIView{
make.left.equalTo(previousBox.snp_right).offset(boxGap)
}if i == 5 {
make.right.equalTo(scrollView)
}
})
复制代码

这时候,box是直接add到scrollView上的。我们直接指定它的top为0。前三个约束分别指定了box的顶部、底部和高度。这样就在竖直方向上满足了两大要求中的第二个要求。对于bottom的约束,它的参考物是view,这就是所谓的外部视图。

接下来我们分别为widthleft添加了约束。而且只要对最后一个box添加right约束即可在水平方向上满足第二个要求。由于我们的布局依赖于外部的视图,所以自然满足第一个要求,因此这种写法也是可以的。

Container与外部视图的优缺点

container相比,使用外部视图出了代码量可能略少以外,我实在想不到它还有什么优点。

首先,一旦我们使用了container,首先它天然满足第一个要求,因为它并没有进行布局,只是让contentSize与自己等大,然后设置自己的大小。而且它几乎已经满足了第二个要求。只要我们最后确定它的宽度或高度即可。其次,在container内部,子视图布局不用考虑满足第二个要求,因为container已经隔离了这一切,我们要做的只是按照习惯,确定子视图的位置,这样container的位置也会随着子视图确定。

其次,我发现的使用外部视图布局的缺点就至少有三个:

  1. 它依赖外部视图进行定位,这样的写法不够优雅
  2. 观察代码中对于bottom属性的约束,它不能完美适配旋转屏幕后的视图。因为此时的屏幕长和宽会对调。而且目测没有什么好的解决方案。
  3. 布局过程中容易踩到坑,比如对于left属性的约束,如果你的代码是这样的:
make.left.equalTo(view).offset(boxGap / 2)
复制代码

它和原来的写法几乎是等价的。但你仔细分析,或者试着滑动scrollView时,一定会大吃一惊。如果你不能一眼看出来这种写法的问题所在,那我建议你运行代码体验一下,并且以后尽量避免这种写法。

最后重复一下,代码地址在github.com/bestswifter…,可以下载下来把玩研究一番,如果觉得对你有帮助,请给一个star。

史上最简单的UIScrollView+Autolayout出坑指南相关推荐

  1. 玩转直播+短视频 京东打造“史上最简单618”

    疫情之下,直播+短视频成为新时代的"弄潮儿",也成为本届"618"的最大看点之一! 与往年不同,本届"618"购物节,京东在站内站外联动布置 ...

  2. 史上最简单的spark教程第十三章-SparkSQL编程Java案例实践(终章)

    Spark-SQL的Java实践案例(五) 本章核心:JDBC 连接外部数据库,sparkSQL优化,故障监测 史上最简单的spark教程 所有代码示例地址:https://github.com/My ...

  3. 史上最简单MySQL教程详解(进阶篇)之视图

    史上最简单MySQL教程详解(进阶篇)之视图 为什么要用视图 视图的本质 视图的作用 如何使用视图 创建视图 修改视图 删除视图 查看视图 使用视图检索 变更视图数据 WITH CHECK OPTIO ...

  4. 2010年史上最简单的做母盘教程

    2010年史上最简单的做母盘教程 辛苦了两个小时才把教程写完....写得不好大家多多包涵 其实做母盘是一件十分简单的事,只要大家敢去试就能成功的,这教程只给小白看的,老鸟路过指点一下. 本人是珠海信佑 ...

  5. 史上最简单Robotium跨进程操作实践——基于ADB框架

    楼主原创,分享不易,转载请注明出处,谢谢. 2015年2月3日更新: 有些朋友在用真机尝试本方法时,抛出了InputStream cannot be null的异常.该异常是由于adb运行在robot ...

  6. 史上最简单的 SpringCloud 教程 | 第一篇: 服务的注册与发现Eureka(Finchley版本)

    转载请标明出处: http://blog.csdn.net/forezp/article/details/81040925 本文出自方志朋的博客 个人博客纯净版:https://www.fangzhi ...

  7. 史上最简单的spark教程第十七章-快速开发部署第一个sparkStreaming+Java流处理程序

    第一个流处理程序sparkStreaming+Java 史上最简单的spark教程 所有代码示例地址:https://github.com/Mydreamandreality/sparkResearc ...

  8. 查找(一)史上最简单清晰的红黑树讲解 http://blog.csdn.net/yang_yulei/article/details/26066409

    查找(一)史上最简单清晰的红黑树讲解 2014-05-18 00:05 4037人阅读 评论(6) 收藏 举报 分类: 数据结构(7) 算法(4) 版权声明:本文为博主原创文章,未经博主允许不得转载. ...

  9. 史上最简单的 Spring MVC 教程(九)

    1 前言 在史上最简单的 Spring MVC 教程(五.六.七.八)等四篇博文中,咱们已经分别实现了"人员列表"的显示.添加.修改和删除等常见的增.删.改.查功能.接下来,也就是 ...

最新文章

  1. java控制台输入的数据存放在数据库表中_JDBC完成修改(使用控制台输入)
  2. html json 访问工程,SpringBoot:Web项目中如何优雅的同时处理Json和Html请求的异常...
  3. 厉害了!这支获得国家级荣誉的智能车队
  4. Spring中@Autowired和@Resource区别
  5. java反射克隆对象_Java反射 - 2(对象复制,父类域,内省)
  6. 用C#实现仿Ruby的XML Builder
  7. Mobile RDA 同步数据库的类--转
  8. vue 声明周期函数_vue-router路由守卫-上
  9. Silverlight 4+RIA Services–搜索引擎优化(SEO)
  10. C++之printf格式
  11. ubuntu 安装docker_Docker学习笔记1 虚拟化历史及 Ubuntu环境下体验安装
  12. 松本行弘:Ruby之父
  13. java类和对象的详解
  14. 基于国产全志A40I的机器人示教器解决方案
  15. Java使用BufferedImage裁剪图片
  16. NW.JS 客户端开发入坑指南
  17. 使用Number Insight和Java创建呼叫者ID
  18. Word去除目录主页页码
  19. 细胞培养常见问题分析
  20. php显示图片的广告,CBA各队第三阶段转会交易评级:辽宁北京拿A级,广东位列倒数...

热门文章

  1. 渗透测试实践(工具使用总结)
  2. 重置密码解决MySQL for Linux错误 ERROR 1045 (28000):
  3. echarts js 删除框选数据_ECharts进行区域选择
  4. 素材诊断分析助手_资深优化师告诉你广告投放素材都在哪找?(国内篇)
  5. 量子计算机张庆瑞讲座报告,燕山大学彭秋明、张庆瑞教授来我校开展学术交流...
  6. django mysql 初始化_Django初始化基础(1)
  7. 大学计算机和英语社团加那个,大学里哪些社团值得加入
  8. debian dhcp服务启动不了_网刻批量装系统pxe启动教程全自动分区装系统
  9. php打造自己的喜马拉雅,打造自己的私人知识宝库利器——mybase 7.3.5
  10. python h5s文件 压缩_如何用python解压zip压缩文件