从 frame、bounds 到 ScrollView 和手势
从 frame、bounds 到 ScrollView 和手势文章目录从 frame、bounds 到 ScrollView 和手势frame和boundsScrollView手势frame和bounds“The frame rectangle, which describes the view’s location and size in its superview’s coordinate system.”frame描述的是这个视图在父视图坐标系中的位置和大小 ------来自Apple官方文档“The bounds rectangle, which describes the view’s location and size in its own coordinate system.”bounds描述的是 view 在自身坐标系中的矩形范围它决定了 view 如何解释自己的内部 内容坐标“The center point of the view’s frame rectangle.”center描述的是view的中心位置也就是说frame关心的是这个 view 在父视图里放在哪里、多大bounds关心的是这个 view 自己内部的坐标系如何定义、内部可见区域有多大Apple 官方还补充了一个非常关键的点默认情况下bounds的 origin 为(0, 0)其 size 通常与frame的 size 一致对于绘图和子视图布局来说bounds定义了 view 的本地坐标范围也影响当前哪些内部内容会出现在可见区域中如果你改变bounds的 origin新的矩形范围内绘制的内容会成为 view 的可见内容对于绘图而言UIKit 使用的是 view 的本地坐标系因此bounds比frame更直接地决定了内容如何被绘制和显示当bounds.origin改变时变化的不是 view 在父视图中的摆放位置而是 view 在自身内容坐标系中“从哪里开始显示内容”所以最后其实就是一句话frame 管对外摆放bounds 管对内坐标在这里我们可以看一个示范#importViewController.hinterfaceViewController()endimplementationViewController-(void)viewDidLoad{[superviewDidLoad];self.view.backgroundColor[UIColor whiteColor];// 父视图有子视图时修改 bounds 对内部内容的影响UIView*vi1[[UIView alloc]initWithFrame:CGRectMake(100,150,150,100)];vi1.boundsCGRectMake(0,0,100,50);vi1.backgroundColor[UIColor redColor];vi1.tag101;[self.view addSubview:vi1];UIView*sub[[UIView alloc]initWithFrame:CGRectMake(0,0,30,30)];sub.backgroundColor[UIColor yellowColor];[vi1 addSubview:sub];[selfperformSelector:selector(delayPerform)withObject:nil afterDelay:2.0];// 没有子视图时比较修改 frame 和 bounds 的现象差异UIView*vi2[[UIView alloc]initWithFrame:CGRectMake(100,300,200,100)];vi2.backgroundColor[UIColor redColor];vi2.tag102;[self.view addSubview:vi2];[selfperformSelector:selector(delayPerformTwo)withObject:nil afterDelay:2.0];}-(void)delayPerform{UIView*temp[self.view viewWithTag:101];temp.backgroundColor[UIColor blueColor];temp.boundsCGRectMake(50,50,100,50);UIView*subtemp.subviews.firstObject;// 获取黄色小块NSLog(修改 bounds 后黄色小块的 frame: %,NSStringFromCGRect(sub.frame));NSLog(修改 bounds 后黄色小块在屏幕上的实际位置: %,NSStringFromCGRect([sub convertRect:sub.bounds toView:nil]));}// 修改frame会移动//- (void)delayPerformTwo {// UIView *view [self.view viewWithTag:102];// view.frame CGRectMake(150, 400, 200, 100);//}// 修改bounds不会移动-(void)delayPerformTwo{UIView*view[self.view viewWithTag:102];view.boundsCGRectMake(150,400,200,100);}end这里上面是2秒之前没有修改坐标的图片下面是2秒之后修改坐标的图片在没有子视图的示范里如果修改frame红色矩形是会移动的只是这里我把它注释掉了这里我们可能有几个问题在没有子视图的示范里改bounds以后如果只是“视野移动”那原来没显示过的区域为什么不空白难道系统会重新渲染还是背景色在第一次渲染的时候不是只画bounds那一小块原因在于backgroundColor属于 view/layer 自身的基础外观它不是第一次只在初始bounds区域里画一次就结束的静态结果当bounds改变后系统会按照当前几何状态重新合成这个 view 的显示内容因此背景色仍然会填满 view 当前显示的矩形区域而不会出现“新露出的区域是空白”的现象那我们要怎么证明bounds的修改会改变父视图内部坐标系甚至影响子视图显示的位置了呢这里我们就可以看有子视图的示范对于背景颜色只要 view 本体还在那个frame位置显示它的背景就会被填充出来但是对于这个黄色子视图是一个内部内容对象当我们修改bounds.origin黄色小块就会看起来偏移、裁掉甚至消失这也证明了bounds 主要影响内部内容不直接影响 view 本身的 frame 位置那为什么黄色小块不是被裁掉而是看起来飞出去了 因为clipsToBounds NO所以UIView 默认是不会裁剪子视图的你也可以将它改为 YES 就会看到非常明 显的变化小结frame主要决定 view 在父视图中的外部位置和大小bounds主要决定 view 如何解释自身内部坐标修改bounds不会直接改变 view 的frame位置bounds的变化会明显影响子视图和内部绘图的显示backgroundColor这类基础外观会按当前几何状态重新合成因此通常不会因为bounds改变而露出空白ScrollView滚动视图 的关键在于它的可视区域是有限的但它内部承载的内容区域可以比可视区域大得多也就是说UIScrollView不是自己在父视图里“跑来跑去”而是它在自己的内容区域中显示不同的位置理解滚动视图的关键在于理解下面这个属性bounds.size表示 scrollView 当前“窗口”的大小也就是用户一次能看到多大的区域contentSize表示 scrollView 内部“内容”的总大小contentOffset表示当前这个可视窗口是从内容区域的哪个位置开始显示的假设有一个scrollView它在父视图中的显示区域大小是200 × 300同时它当前可视窗口的大小bounds.size也是200 × 300但它内部的内容大小contentSize有300 × 800这个时候的contentOffset {0,0}就类似外面有一个固定大小的窗户bounds.size里面有一张很长的海报contentSize你现在一次只能通过这个窗户看到海报的一部分假设用户往上拖内容看起来内容往上走了但对于开发者来说最直接看到的是contentOffset.y增大了从几何理解上可以把它类比成这个可视窗口的观察起点向下偏移了也就是和bounds.origin.y的变化很接近为什么UIScrollView的滚动和bounds有关? 前面我们说过bounds决定的是 view 自己内部坐标系的范围 对于UIScrollView来说可以把滚动理解为当前这个 view 的可视区域在自己的内部内容坐标系里发 生了偏移UIScrollView则在bounds的思想上进一步引入了更大的内容区域contentSize当前查看位置contentOffset所以如果想滚动就必须至少有某个方向上contentSize大于bounds.size为什么普通 UIView 不能像 scrollView 一样滚动? 普通UIView也有frame和bounds甚至修改bounds.origin也会影响内部内容显示的位置 但普通UIView没有 scrollView 那样完整的滚动系统它缺少这些:contentSizecontentOffset 拖动 手势驱动滚动的机制 惯性滚动 回弹效果 滚动条显示 普通 UIView 有内部坐标系但没有“滚动能力”UIScrollView 则是在内部坐标系的基础上专门增加了一套 滚动机制scrollView 本质上不是“自己在移动”而是在自己的内容坐标系中切换可见区域我们可以来看一个简单的事示范implementationViewController-(void)viewDidLoad{[superviewDidLoad];self.view.backgroundColor[UIColor whiteColor];// 创建 scrollView在父视图中的位置和大小是 200 × 300self.scrollView[[UIScrollView alloc]initWithFrame:CGRectMake(80,100,200,300)];self.scrollView.backgroundColor[UIColor lightGrayColor];// 设置内部内容总大小宽 200高 800self.scrollView.contentSizeCGSizeMake(200,800);[self.view addSubview:self.scrollView];// 顶部红色视图UIView*redView[[UIView alloc]initWithFrame:CGRectMake(0,0,200,150)];redView.backgroundColor[UIColor redColor];[self.scrollView addSubview:redView];// 底部蓝色视图UIView*blueView[[UIView alloc]initWithFrame:CGRectMake(0,300,200,150)];blueView.backgroundColor[UIColor blueColor];[self.scrollView addSubview:blueView];}手势在前面两部分里我们已经讨论了frame和bounds说明了一个 view 如何在父视图中摆放以及如何定义自己的内部坐标系UIScrollView则进一步说明了“可视区域”和“内容区域”是可以分开的接下来就有一个问题手势拿到的点到底是相对于谁的坐标不是独立存在的它最重要的点就在于它是相对性的总是依附于某个坐标系CGPoint p[tap locationInView:self.view];这段代码的意思就是获取当前手势点在self.view坐标系中的位置当我们把self.view换成别的 view就会得到不一样的结果因为同一个点击位置在不同坐标系中的表示是不一样的所以locationInView:不是一个可有可无的参数而是在明确这个点最终应该以谁作为参考系平移手势中的translationInView:也是同样的道理它表示的并不是“绝对位移”而是手势在某个指定 view 坐标系中的位移量到这里就可以发现前面几部分其实都在讲同一个问题frameview 相对于父视图boundsview 相对于自己scrollView.contentOffset当前窗口相对于内容区域locationInView:触摸点相对于某个 view所以它们背后的共同点就是UIKit 中很多几何问题本质上都在问谁相对于谁