原文引用:http://objccn.io/issue-3-2/。真的是受益匪浅,记录一下,以便不断学习。
UIView - frame 和 bounds
每个视图都有bounds和frame属性;用于决定视图的位置和大小。视图的frame和bounds的大小总是一样的,但是他们的origin有可能不同。
视图在绘制自己的时候,并不会关心frame,即不关心自己所处的位置。它只关心绘制自己的内容,这个绘制发生在每个视图的drawRect: 方法中。
在drawRect: 方法被调用之前,会为视图创建一个画布(视图的上下文环境)用来绘制内容。这个画布的坐标系统就是视图的bounds。只会绘制bounds中的内容,超出视图bounds的部分,会被摒弃。
在合成阶段,每个视图都会将自己的图层组合到自己父视图的图层上去,视图的frame决定了其在父视图中的位置,frame中的origin决定了其相对于父视图左上角的偏移量。所以,一个origin为{x:20,y:15}的frame所绘制的图片 左边距其父视图20点,上边距父视图15点。
视图相对于其父视图的位置,组合公式如下:
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
视图的左上角会根据它frame的origin进行偏移,并绘制到父视图上:
ScrollView - Content Offset
我们前面提到的组合公式:
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
从上面这个组合公式,我们可以看出通过改变子View的frame.origin,或者父view的bounds.origin,来移动子View. 这正是scrollview的工作原理。当你设置它的contentOffset属性时,它改变的是scrollview的bounds.origin。设置代码看起来像这样:
- (void)setContentOffset:(CGPoint)offset
{
CGRect bounds = [self bounds];
bounds.origin = offset;
[self setBounds:bounds];
}
ScrollView - Content Size
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 为可滚动区域上的一个窗口
ScrollView - Content Insets
contentInset 属性可以改变 content offset 的最大和最小值,这样便可以滚动出可滚动区域。它的类型为 UIEdgeInsets,包含四个值:{top,left,bottom,right}。当你引进一个 inset 时,你改变了 content offset 的范围。比如,设置 content inset 顶部值为 10,则允许 content offset 的 y 值达到 -10.
contentInset很好的两个例子:
tableview的下拉刷新
想想一个 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 这样的类库就应该明白了)
键盘弹出后的遮挡问题
当键盘在屏幕上时,有一个很好的用途:你想要设置一个紧贴屏幕的用户界面。当键盘出现在屏幕上时,你损失了几百个像素的空间,键盘下面的东西全都被挡住了。
现在,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 的底部等于键盘的高度。