热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

理解ScrollViews

可能你很难相信?UIScrollView?和一个标准的?UIView?差异并不大,scrollview确实会多出一些方法,但这些方法只是和UIView的属性很好的结合到一起了。因此

可能你很难相信 UIScrollView 和一个标准的 UIView 差异并不大,scroll view 确实会多出一些方法,但这些方法只是和 UIView 的属性很好的结合到一起了。因此,在要想弄懂 UIScrollView 是怎么工作之前,你需要先了解一下 UIView,特别是视图渲染的两步过程。

光栅化和组合

渲染过程的第一部分是众所周知的光栅化(rasterization),光栅化简单的说就是产生一组绘图指令并且生成一张图片。比如绘制一个圆角矩形、带图片、标题居中的 UIButtons。这些图片并没有被绘制到屏幕上去;取而代之的是,他们被自己的视图保持着留到下一个步骤使用。

一旦每个视图都产生了自己的光栅化图片,这些图片便被一个接一个的绘制,并产生一个屏幕大小的图片,这便是上文所说的组合。视图层级(view hierarchy)对于组合如何进行扮演了很重要的角色:一个视图的图片被组合在它父视图的图片上面。然后,组合好的图片被组合到父视图的父视图图片上面。视图层级最顶端是窗口(window),它组合好的图片便是我们看到的东西了。

概念上,依次在每个视图上放置独立分层的图片并最终产生一个图片,单调的图像更容易被理解,特别是如果你以前使用过像 Photoshop 这样的工具。我们还有另外一篇文章详细解释了像素是如何绘制到屏幕上去的。

现在,回想一下,每个视图都有一个 bounds 和 frame。当布局一个界面时,我们需要处理视图的 frame。这允许我们放置并设置视图的大小。视图的 frame 和 bounds 的大小总是一样的,但是他们的 origin 有可能不同。弄懂这两个工作原理是理解 UIScrollView 的关键。

在光栅化步骤中,视图并不关心即将发生的组合步骤。也就是说,它并不关心自己的 frame (这是用来放置视图的图像)或自己在视图层级中的位置(这是决定组合的顺序)。这时视图只关心一件事就是绘制它自己的 content。这个绘制发生在每个视图的 drawRect:方法中。

在 drawRect: 方法被调用前,会为视图创建一个空白的图片来绘制 content。这个图片的坐标系统是视图的 bounds。几乎每个视图 bounds 的 origin 都是 {0,0}。因此,当在光栅化图片左上角绘制一些东西的时候,你都会在 bounds 的 origin {x:0, y:0} 处绘制。在一个图片右下角的地方绘制东西的时候,你都会绘制在 {x:width, y:height} 处。如果你的绘制超出了视图的 bounds,那么超出的部分就不属于光栅化图片的部分了,并且会被丢弃。

技术分享

在组合的步骤中,每个视图将自己光栅化图片组合到自己父视图的光栅化图片上面。视图的 frame 决定了自己在父视图中绘制的位置,frame 的 origin 表明了视图光栅化图片左上角相对父视图光栅化图片左上角的偏移量。所以,一个 origin 为 {x:20, y:15} 的 frame 所绘制的图片左边距其父视图 20 点,上边距父视图 15 点。因为视图的 frame 和 bounds 矩形的大小总是一样的,所以光栅化图片组合的时候是像素对齐的。这确保了光栅化图片不会被拉伸或缩小。

技术分享

记住,我们才仅仅讨论了一个视图和它父视图之间的组合操作。一旦这两个视图被组合到一起,组合的结果图片将会和父视图的父视图进行组合,这是一个雪球效应。

考虑一下组合图片背后的公式。视图图片的左上角会根据它 frame 的 origin 进行偏移,并绘制到父视图的图片上:

CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;

CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;

正如之前所说的,如果一个视图 bounds 的 origin 是 {0,0}。那么,我们得到这个公式:

CompositedPosition.x = View.frame.origin.x;

CompositedPosition.y = View.frame.origin.y;

我们可以通过几个不同的 frames 看一下:

技术分享

这样做是有道理的,我们改变 button 的 frame.origin后,它会改变自己相对紫色父视图的位置。注意,如果我们移动 button 直到它的一部分已经在紫色父视图 bounds 的外面,当光栅化图片被截去时这部分也将会通过同样的绘制方式被截去。然而,技术上讲,因为 iOS 处理组合方法的原因,你可以将一个子视图渲染在其父视图的 bounds 之外,但是光栅化期间的绘制不可能超出一个视图的 bounds。

Scroll View的Content Offset

现在我们所讲的跟 UIScrollView 有什么关系呢?一切都和它有关!考虑一种我们可以实现的滚动:我们有一个拖动时 frame 不断改变的视图。这达到了相同的效果,对吗?如果我拖动我的手指到右边,那么拖动的同时我增大视图的 origin.x ,瞧,这货就是 scroll view。

当然,在 scroll view 中有很多具有代表性的视图。为了实现这个平移功能,当用户移动手指时,你需要时刻改变每个视图的 frames。当我们提到组合一个 view 的光栅化图片到它父视图什么地方时,记住这个公式:

CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;

CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;

我们减少 Superview.bounds.origin 的值(因为他们总是0)。但是如果他们不为0呢?我们用和前一个图例相同的 frames,但是我们改变了紫色视图 bounds 的 origin 为 {-30, -30}。得到下图:

技术分享

现在,巧妙的是通过改变这个紫色视图的 bounds,它每一个单独的子视图都被移动了。事实上,这正是 scroll view 工作的原理。当你设置它的 contentOffset 属性时它改变 scroll view.bounds 的 origin。事实上,contentOffset 甚至不是实际存在的。代码看起来像这样:

- (void)setContentOffset:(CGPoint)offset
{    CGRect bounds = [self bounds];
    bounds.origin = offset;
    [self setBounds:bounds];
}

注意前一个图例,只要足够的改变 bounds 的 origin,button 将会超出紫色视图和 button 组合成的图片的范围。这也是当你足够的移动 scroll view 时,一个视图会消失!

世界之窗:Content Size

现在,最难的部分已经过去了,我们再看看 UIScrollView 另一个属性:contentSize。 scroll view 的 content size 并不会改变其 bounds 的任何东西,所以这并不会影响 scroll view 如何组合自己的子视图。反而,content size 定义了可滚动区域。scroll view 的默认 content size 为 {w:0, h:0}。既然没有可滚动区域,用户是不可以滚动的,但是 scroll view 仍然会显示其 bounds 范围内所有的子视图。 
当 content size 设置为比 bounds 大的时候,用户就可以滚动视图了。你可以认为 scroll view 的 bounds 为可滚动区域上的一个窗口:

技术分享

当 content offset 为 {x:0, y:0} 时,可见窗口的左上角在可滚动区域的左上角处。这也是 content offset 的最小值;用户不能再往可滚动区域的左边或上边移动了。那儿没啥,别滚了!

content offset 的最大值是 content size 和 scroll view size 的差(不同于 content size 和scroll view的 bounds 大小)。这也在情理之中:从左上角一直滚动到右下角,用户停止时,滚动区域右下角边缘和滚动视图 bounds 的右下角边缘是齐平的。你可以像这样记下 content offset 的最大值:

contentOffset.x = contentSize.width - bounds.size.width;

contentOffset.y = contentSize.height - bounds.size.height;

用Content Insets对窗口稍作调整

contentInset 属性可以改变 content offset 的最大和最小值,这样便可以滚动出可滚动区域。它的类型为 UIEdgeInsets,包含四个值:{top,left,bottom,right}。当你引进一个 inset 时,你改变了 content offset 的范围。比如,设置 content inset 顶部值为 10,则允许 content offset 的 y 值达到 10。这介绍了可滚动区域周围的填充。

技术分享

这咋一看好像没什么用。实际上,为什么不仅仅增加 content size 呢?除非没办法,否则你需要避免改变scroll view 的 content size。想要知道为什么?想想一个 table view(UItableView是UIScrollView 的子类,所以它有所有相同的属性),table view 为了适应每一个cell,它的可滚动区域是通过精心计算的。当你滚动经过 table view 的第一个或最后一个 cell 的边界时,table view将 content offset 弹回并复位,所以 cells 又一次恰到好处的紧贴 scroll view 的 bounds。

当你想要使用 UIRefreshControl 实现拉动刷新时发生了什么?你不能在 table view 的可滚动区域内放置 UIRefreshControl,否则,table view 将会允许用户通过 refresh control 中途停止滚动,并且将 refresh control 的顶部弹回到视图的顶部。因此,你必须将 refresh control 放在可滚动区域上方。这将允许首先将 content offset 弹回第一行,而不是 refresh control。

但是等等,如果你通过滚动足够多的距离初始化 pull-to-refresh 机制,因为 table view 设置了 content inset,这将允许 content offset 将 refresh control 弹回到可滚动区域。当刷新动作被初始化时,content inset 已经被校正过,所以 content offset 的最小值包含了完整的 refresh control。当刷新完成后,content inset 恢复正常,content offset 也跟着适应大小,这里并不需要为content size 做数学计算。(这里可能比较难理解,建议看看 EGOTableViewPullRefresh 这样的类库就应该明白了)

如何在自己的代码中使用 content inset?当键盘在屏幕上时,有一个很好的用途:你想要设置一个紧贴屏幕的用户界面。当键盘出现在屏幕上时,你损失了几百个像素的空间,键盘下面的东西全都被挡住了。

现在,scroll view 的 bounds 并没有改变,content size 也并没有改变(也不需要改变)。但是用户不能滚动 scroll view。考虑一下之前一个公式:content offset 的最大值是 content size 和 bounds 的差。如果他们相等,现在 content offset 的最大值是 {x:0, y:0}.

现在开始出绝招,将界面放入一个 scroll view。scroll view 的 content size 仍然和 scroll view 的 bounds 一样大。当键盘出现在屏幕上时,你设置 content inset 的底部等于键盘的高度。

技术分享

这允许在 content offset 的最大值下显示滚动区域外的区域。可视区域的顶部在 scroll view bounds 的外面,因此被截取了(虽然它在屏幕之外了,但这并没有什么)。

但愿这能让你理解一些滚动视图内部工作的原理,你对缩放感兴趣?好吧,我们今天不会谈论它,但是这儿有一个有趣的小窍门:检查 viewForZoomingInScrollView: 方法返回视图的 transform 属性。你将再次发现 scroll view 只是聪明的利用了 UIView 已经存在的属性。

相关链接(强烈推荐):

计算机图形渲染的流程


理解Scroll Views


推荐阅读
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 解决VS写C#项目导入MySQL数据源报错“You have a usable connection already”问题的正确方法
    本文介绍了在VS写C#项目导入MySQL数据源时出现报错“You have a usable connection already”的问题,并给出了正确的解决方法。详细描述了问题的出现情况和报错信息,并提供了解决该问题的步骤和注意事项。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
author-avatar
吴柏盈4477
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有