39.layoutSubviews和drawRect调用时机的探究

来源:互联网 发布:php pack函数 编辑:程序博客网 时间:2024/06/05 10:29

探究前了解

我在整理自己曾经收藏的技术文章时发现这么一个文章, 是关于layoutSubviews和drawRect的触发时机的。今天,就根据这篇文章来做一些测试,证实一些文章中的说法。
请先以了解为主阅读下面的这个文章,如下:

一、layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、改变一个UIView的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView引发UIView的重新布局会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、直接调用setNeedsLayout 或者 layoutIfNeeded。

二、drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡

三、drawRect方法使用注意点:
1、 若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate 的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或 者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来调用setNeedsDisplay实时刷新屏幕

探究

layoutSubviews调用时机测试

#import <UIKit/UIKit.h>@interface TestView : UIView@end
#import "TestView.h"@implementation TestView- (id)initWithFrame:(CGRect)frame{  self = [super initWithFrame:frame];  if (self)  {    NSLog(@"initWithFrame:%@" ,NSStringFromCGRect(frame));  }  return self;}- (void)layoutSubviews{  NSLog(@"layoutSubviews %@", self);  [super layoutSubviews];}@end
#import "RootViewController.h"#import "TestView.h"@interface RootViewController ()@property (nonatomic, strong) NSTimer   *timer;@property (nonatomic, strong) TestView  *largeView;@property (nonatomic, strong) TestView  *smallView;@end@implementation RootViewController- (void)viewDidLoad{  [super viewDidLoad];  // 1、init初始化不会触发layoutSubviews [正确的]  // 2、addSubview会触发layoutSubviews [不完全正确,当frame为0时是不会触发的]  // 3、设置view的Frame的 size 发生变化会触发layoutSubviews [正确]//  [self test_1];//  [self test_2];//  [self test_3];  // 4、滚动一个UIScrollView会触发layoutSubviews[错误,不用滚动就会触发]//  [self test_4];  // 5、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件  [self test_5];}- (void)test_1{  /*    解释:   走了initWithFrame:方法,但是又有frame值为{{0, 0}, {0, 0}},并不需要绘制任何的东西,   所以即使添加了test,也没必要绘制它,同时也验证了addSubview会触发layoutSubviews是错   误的,只有当被添加的view有着尺寸的时候才会触发layoutSubviews   */  TestView *test = [TestView new];  [self.view addSubview:test];}- (void)test_2{  TestView *test = [TestView new];  test.frame = CGRectMake(0, 0, 100, 100);  [self.view addSubview:test];}- (void)test_3{  /*   解释:   layoutSubviews这个方法自身无法调用,是被父类添加的时候才执行的方法   */  TestView *test = [TestView new];  test.frame = CGRectMake(0, 0, 50, 50);  UIView *showView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];  [test addSubview:showView];}- (void)test_4{  CGRect rect   = self.view.bounds;  CGFloat height = rect.size.height;  CGFloat width  = rect.size.width;  UIScrollView *rootScroll = [[UIScrollView alloc] initWithFrame:self.view.bounds];  NSArray *data         = @[@"", @"", @"", @""];  [data enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {    TestView *tmp       = [[TestView alloc] initWithFrame:CGRectMake(width*idx, 0,                                      width, height)];    [rootScroll addSubview:tmp];  }];  rootScroll.contentSize   = CGSizeMake(width * data.count, height);  [self.view addSubview:rootScroll];}- (void)test_5{  _timer = [NSTimer scheduledTimerWithTimeInterval:1.f                        target:self                      selector:@selector(timerEvent:)                      userInfo:nil                       repeats:YES];  _largeView = [[TestView alloc] initWithFrame:self.view.bounds];  [self.view addSubview:_largeView];  _smallView = [[TestView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];  [_largeView addSubview:_smallView];}- (void)timerEvent:(id)sender{  _smallView.frame = CGRectMake(arc4random()%100 + 20,                  arc4random()%100 + 20,                  arc4random()%100 + 20,                  arc4random()%100 + 20);  NSLog(@"_smallView %@", _smallView);  NSLog(@"_smallView %@", _largeView);}@end

官方文档中介绍:

You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.

layoutSubviews 测试后的结论:

1.一个view是不能够自己调用layoutSubviews,如果要调用,需要调用 setNeedsLayout或者 layoutIfNeeded
2.如果view的frame值为0,即使被addSubview也不会调用layoutSubviews
3.如果一个view的frame中的 size 值前后发生了改变,那么layoutSubviews也会被触发。 重新设置 frame 但 size 不变的话,是不会触发的

drawRect调用时机测试

#import <UIKit/UIKit.h>@interface AView : UIView@end
#import "AView.h"@implementation AView- (void)drawRect:(CGRect)rect {    // Drawing code    NSLog(@"drawRect");}-(void)layoutSubviews{    NSLog(@"layoutSubviews");}@end
#import "ViewController.h"#import "AView.h"@interface ViewController ()@property (nonatomic, strong)AView *a;@end@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];    // Do any additional setup after loading the view, typically from a nib.    self.a = [[AView alloc]initWithFrame:CGRectMake(0, 0, 300, 200)];    self.a.backgroundColor = [UIColor greenColor];    [self.view addSubview:_a];}- (void)viewWillAppear:(BOOL)animated{}-(void)viewDidAppear:(BOOL)animated{}- (IBAction)click:(id)sender {//    self.a.backgroundColor = [UIColor redColor];    self.a.frame = CGRectMake(50, 10, 200, 200);//    [self.a setNeedsLayout];}@end

drawRect 测试后的结论:

1.如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 调用是在Controller->viewWillAppear, Controller->viewDidAppear 两方法之间调用的.所以这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2.直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是frame的 size 不能为0
3.当 addSubview 时, frame 中的 size 为 (0,0)时, 不会触发 drawRect 方法
4.改变 view 的 size 的时候, 不会触发 drawRect, 但是更改其颜色变化的时候, 会触发 drawRect
5.当一个view第一次显示或当一个事件发生,该事件导致view的可视部分无效时,才会被调用。

drawRect官方文档:

The default implementation of this method does nothing. Subclasses that use technologies such as Core Graphics and UIKit to draw their view’s content should override this method and implement their drawing code there. You do not need to override this method if your view sets its content in other ways. For example, you do not need to override this method if your view just displays a background color or if your view sets its content directly using the underlying layer object.

该方法的默认实现并不会做任何事情。子类使用诸如Core Graphics和UIKit技术绘制其控件的内容应该重写该方法,并且把实现的代码写在该方法中。如果你控件的内容是用其他方式设置的,那么你就不需要重写该方法。例如,如果你的控件仅仅只是展示背景颜色,则不需要重写该方法或者你的控件内容是直接使用layer对象设置的,也不需要调用该方法。

By the time this method is called, UIKit has configured the drawing environment appropriately for your view and you can simply call whatever drawing methods and functions you need to render your content. Specifically, UIKit creates and configures a graphics context for drawing and adjusts the transform of that context so that its origin matches the origin of your view’s bounds rectangle. You can get a reference to the graphics context using the UIGraphicsGetCurrentContext function, but do not establish a strong reference to the graphics context because it can change between calls to the drawRect: method.

当调用该方法时,UIKit框架已经为你的控件配置好合适的绘制环境,你可以轻松的调用任何绘制方法和函数来渲染你的控件内容。特别地,UIKit会创建并配置一个图形上下文,然后调整该上下文的形变,以使该图形上下文的原点与你控件的bounds的原点相匹配。你可以通过调用UIGraphicsGetCurrentContext函数获得图形上下文的引用,但是不要对该图形上下文建立强引用,因为多次调用drawRect:方法期间,图形上下文会改变。

If you subclass UIView directly, your implementation of this method does not need to call super. However, if you are subclassing a different view class, you should call super at some point in your implementation.

如果你直接创建UIView的子类,该方法的实现不需要调用super。但是,如果你创建的是不同的view类,你需要在实现代码的某个时候调用super。

This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.

当一个view第一次显示或当一个事件发生,该事件导致view的可视部分无效时,才会被调用。永远不要手动调用该方法。为了使控件的某个部分失效,并且因此导致某个部分重绘,应该调用setNeedsDisplay或者setNeedsDisplayInRect:而不是drawRect:方法。

参考资料: 解析LayoutSubviews

1 0