深入理解UIScrollView
前言
这篇文章是关于UIScrollView
的工作原理,不涉及基础的语法和功能实现,文章主要是我的总结和实战,主要介绍了Bounds
和Frame
属性的区别,只有理解了这两个属性才能更好的理解UIScrollView
的工作方式,文章为原创,转载请注明出处
参考文章 ObjC期刊 和 Bounds和Frame的区别
- 了解
UIScrollView
是怎么工作之前,需要先了解UIView
,特别是视图渲染的两步
光栅化和组合
- 光栅化:光栅化是产生一组绘图指令并且生成一张图片。比如绘制一个圆角矩形,带图片,文字居中显示的UIButton。这些图片没有马上被绘制到屏幕上,而是被自己的视图留到下一个步骤使用
- 组合:每个图片都会产生一个光栅化图片,这些图片被一个接一个的绘制,并产生一个屏幕大小的图片
- 图片被一个接一个的绘制到父视图上面,视图最顶层的是UIWindow,它组合好的便是我们看到的
- 现在回想一下每个视图都有一个
Bounds
和Frame
,视图的Frame和Bounds的大小通常是一样的(但也可以不一样),但它们的origin
通常是不同的,弄懂这两个是理解UIScrollView
的关键,在光栅化的过程中,视图不关心自己的Frame或在图层层级中的位置,它只关心绘制自己的Content,这个绘制被发生在每个视图-drawRect
方法中 - 在-drawRect:调用前,视图会创建一个空的图片来绘制Content。这个图片的坐标系统是视图的Bounds,几乎每个视图的Bounds的Origin都是{0, 0},因此当在左上角绘制时会发生在{0, 0},在右下角绘制时会出现在{width, height},当超出了光栅化的部分将会被丢弃。
但事实上显示时只要不设置属性clipsToBounds
属性为true
,绘制在外面的视图还是会被显示出来
- 在组合的过程中,每个视图将自己光栅化图片组合到自己的父视图的光栅化的图片上。
Frame
决定了自己在父视图的位置,Frame的Origin
属性决定了视图的光栅化图片对于父坐标的偏移量。因为Bounds和Frame的大小是一样的,所以光栅化的图片是像素对齐的,这保证了光栅化的图片不会被缩小或拉伸
CompositedPositionX = View.frame.origin.x - SuperView.bounds.origin.x
CompositedPositionY = View.frame.origin.y - SuperView.bounds.origin.y
大多数视图的Bounds的Origin都是{0, 0}
CompositedPositionX = View.frame.origin.x
CompositedPositionY = View.frame.origin.y
- 这样做是有道理的,当改变视图的Frame.origin时它会相对父坐标而移动,在父坐标外的视图会在光栅化过程时被截取,但技术上讲还是会显示出来,但那是光栅化过程之后的事了。
关于Bounds和Frame的区别
CGRect rectForFirstView = CGRectMake(50.0, 100.0, 200.0, 200.0);
UIView *firstView = [[UIView alloc]initWithFrame:rectForFirstView];
firstView.backgroundColor = [UIColor blueColor];
// [firstView setBounds:CGRectMake(20.0, 80.0, 200.0, 200.0)];
CGRect rectForSecondView = CGRectMake(100.0, 100.0, 100.0, 100.0);
UIView *secondView = [[UIView alloc]initWithFrame:rectForSecondView];
secondView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:firstView];
[firstView addSubview:secondView];
- 首先注释掉改变第一个视图Bounds的代码,将第二个视图作为第一个视图的子视图,第一个视图作为View的子视图,这时两个视图的位置如上
此时添加如下代码块
[firstView setBounds:CGRectMake(20.0, 80.0, 200.0, 200.0)];
- 视图此时相对父视图的位置是这样的
父视图的Frame.origin并没有改变,但是子视图的位置却被改变了,改变父视图Bounds.origin改变后,整个视图的变化是这样的
- 再增加如下代码
[firstView setBounds:CGRectMake(0.0, 0.0, 300.0, 300.0)];
NSLog(@"firstView.Bounds:%@", NSStringFromCGRect(firstView.frame));
控制台打印消息如下
firstView.Bounds: {{0, 50}, {300, 300}}
- 可见当Bounds的Size改变的时候,整个Frame变成和Bounds同样的大小,并且改变了原点坐标
changedFrame.origin.x = (frame.origin.x - changedBounds.size.width) / 2.0
changedFrame.origin.y = (frame.origin.y - changedBounds.size.height) / 2.0
UIScrollView的Content Offset
现在所讲的跟UIScrollView有什么关系呢,考虑实现一种滚动,有一个拖动Frame不断改变的视图。这达到了同样的效果,当拖动在右边,那么改变的是Frame.origin.x,你看,这就是UIScrollView
- UIScrollView中有很多代表性的视图,当实现平移这个功能的时候,用户移动手指,你需要时刻改变每个视图的Frames。当组合光栅化到了它父视图的什么位置,记住这个公式
CompositedPosition.x = View.frame.origin.x - SuperView.bounds.origin.x
CompositedPosition.y = View.frame.origin.y - SuperView.bounds.origin.y
- Bounds.origin.x的值总是0,但当它不为0呢(如上文),当改变父视图的Bounds为{-30, -30}时,子视图也会随之移动。
这是UIScrollView的工作原理,当设置它的ContentOffset属性时,它实际上改变了Bounds.origin属性,实际上,ContentOffset甚至是不存在的,它的代码实现大致如下
-(void)setContentOffset:(CGPoint)offset{
CGRect Bounds = [self bounds];
bounds.origin = offset;
[self setBounds: bounds];
}
当改变UIScrollView的Bounds的Origin到一定大小时,超过组合
范围内的视图会消失