iOS - 布局重绘机制相关方法的研究

时间:2022-03-13 18:15:20

iOS View布局重绘机制相关方法

布局

- (void)layoutSubviews 
- (void)layoutIfNeeded
- (void)setNeedsLayout

——————————————————————————————

重绘

- (void)drawRect

- (void)setNeedsDisplay

-  (void)setNeedsDisplayInRect:(CGRect)invalidRect

——————————————————————————————

- (CGSize)sizeThatFits:(CGSize)size
- (void)sizeToFit

 

一、布局

刷新子控件的布局

-layoutSubviews方法: 这个方法不能直接调用这个方法 ,由系统调用,默认没有做任何事情,一般进行重写调用super 之后可以在这里设置子控件的frame
-setNeedsLayout方法: 标记上一个需要被重新布局标记,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
-layoutIfNeeded方法:如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

- (void)layoutSubviews;

这个方法不能直接调用这个方法 ,由系统调用,默认没有做任何事情,一般进行重写调用super 之后可以在这里设置子控件的frame 

以下情况会触发系统调用 layoutSubviews方法

  • 1、init初始化不会触发layoutSubviews
  • 2、addSubview会触发layoutSubviews
  • 3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
  • 4、滚动一个UIScrollView会触发layoutSubviews
  • 5、旋转Screen会触发父UIView上的layoutSubviews事件
  • 6、改变一个UIView大小的时候也会触发其父类的UIView上的layoutSubviews事件
  • 7、直接调用setLayoutSubviews 或者 layoutIfNeeded
- (void)layoutSubviews{
[super layoutSubviews];
//可以在这里设置子控件的frame等 }

layoutSubviews官方讲解

Lays out subviews.

The default implementation of this method does nothing on iOS 5.1 and earlier. Otherwise, the default implementation uses any constraints you have set to determine the size and position of any subviews.

在iOS5.1或之前的版本中,这个方法什么也没干.这个方法的默认实现是用参数来设定subviews的尺寸和位置的.

Subclasses can override this method as needed to perform more precise layout of their subviews. You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.

如果你需要更加精确的布局,可以在子类里面重写这个方法.仅仅在以下情况下:自动布局达不到你想要效果时你才有必要重写这个方法.你可以直接设置subviews的尺寸.

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.

你不能直接调用这个方法.这个方法是系统调用的 如果你需要强制layout刷新,调用setNeedsLayout来代替.如果你想要立即刷新你的view,调用layoutIfNeeded

调用-setNeedsLayout方法

就是我们主动为这个视图设置一个标记(flag)标记为需要重新布局,告诉系统这个视图再下一个时机到来时要重新渲染.,异步调用layoutIfNeeded刷新布局,(不立即刷新,因为有标记所以 layoutSubviews一定会被调用)

在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews

调用-layoutIfNeeded方法

告诉系统,如果设置了flag那么不用等待时机到来了,直接渲染吧 .立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

结合上面的细细品味下面的内容总结

在我们没有任何干预的情况下,一个view的frame或bounds发生变化时,系统会设置一个flag给这个view,当下一个渲染时机到来时系统会重新按新的布局来渲染视图(系统会调用layoutSubViews)。setNeedLayout就是我们主动为这个视图设置一个flag,告诉系统这个视图再下一个时机到来时要重新渲染,而layoutIfNeed则是告诉系统,如果设置了flag那么不用等待时机到来了,直接渲染吧。而layoutSubviews这个方法是系统调用的,我们不需要主动调用,我们只需要调用layoutIfNeed就可以了,让系统判断是否在当前时机下立即渲染。

⚠️值得注意的是:设置标记的时机

  • 1、一个view的frame或bounds发生变化时,系统会设置一个flag给这个view
  • 2、主动调用setNeedLayout设置标记
  • 3、xib 中的约束值变了系统也会设置flag  self.blueViewW.constant = 80;  系统底层会转换为frame 所以会设置一个flag

二、重绘

-drawRect:(CGRect)rect方法:不能直接调用这个方法 ,由系统调用,重写此方法,执行重绘任务
-setNeedsDisplay方法:标上一个需要被重新绘图的标记,异步调用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘

- (void)drawRect:(CGRect)rect;

不能直接调用这个方法 ,由系统调用,重写此方法,执行重绘任务

 以下情况会触发系统调用 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。

drawRect方法使用注意点:

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

-setNeedsDisplay

在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘

-setNeedsDisplayInRect:(CGRect)invalidRect:

标记为需要局部重绘

⚠️注意

layoutSubviews对subviews重新布局

layoutSubviews方法调用先于drawRect

setNeedsLayout在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews

layoutIfNeeded方法如其名,UIKit会判断该receiver是否需要layout.根据Apple官方文档,layoutIfNeeded方法应该是这样的

layoutIfNeeded遍历的不是superview链,应该是subviews链

drawRect是对receiver的重绘,能获得context

setNeedDisplay/setNeedsDisplayInRect 在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘

- (void)sizeToFit; //手动调用,不能重写,系统会根据内容的帮我布局一个它认为最合适的 最优的 size 而且会改变自己的size

- (CGSize)sizeThatFits:(CGSize)size; //手动调用,可以在子类中重写,会计算出最优的 size 并返回 不会改变自己的size

- (CGSize)sizeThatFits:(CGSize)size;     // return 'best' size to fit given size. does not actually resize view. Default is return existing view size

- (void)sizeToFit;                       // calls sizeThatFits: with current view bounds and changes bounds size.

- (void)sizeToFit

  • sizeToFit会自动调用sizeThatFits方法;
  • sizeToFit不应该在子类中被重写,应该重写sizeThatFits
  • 调用sizeToFit 系统会根据内容的帮我布局一个它认为最合适的 最优的 size 而且会改变当前view自己的size

sizeToFit应用场合

另外sizeToFit声明在UIView中,所以我们的所有的视图控件,都可以调用这个方法,但是实际的开发中,我们好像也不经常使用它,来做一些布局什么的?

原因:我们一般在不方便手动布局的时候才调用sizeToFit方法,以下场合不适合手动布局可以用sizeToFit:

  • 1. navigationBar中对navigationItem的设置,(添加两个视图以上的控件到Item)
  • 2. toolBar中的对UIBarButtonItem的设置(一般我们还要添加弹簧控件)
    上述两种场合就可以用sizeToFit这个方法,来让系统给我们做自动布局。(注意:如果就添加一个控件的话,我们直接设置frame也是可以的)
  • 3.在tabBar中我们不能手动的添加的子控件,因为tabBar是根据控制器系统默认自动添加的tabBarItem。(猜想系统可能也会自动调用了这个方法)
  • 4.UILabel中添加文字,然后让调整label的大小来适应文字,我们也调用sizeToFit的方法。

- (CGSize)sizeThatFits:(CGSize)size;

返回最合适的大小,实际上不调整视图(即不会改变自己的size)。默认值是返回现有的视图大小

以上两个放的应用:使用这两个方法之前,必须要给uilabel赋值,否则不会显示内容的。

UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 50, 0, 0)];  

    testLabel.backgroundColor = [UIColor whiteColor];  

    testLabel.text = @"我们都有一个家啊,名字叫中国,家里攀着两条龙";  

    testLabel.font = [UIFont systemFontOfSize:20];  

    testLabel.textColor = [UIColor blackColor];  

    [testLabel sizeThatFits:CGSizeMake(20, 20)];//会计算出最优的 size 并返回 不会改变label 的size

    NSLog(@"testLabel sizeThatFits frame = %@", NSStringFromCGRect(testLabel.frame));  

    NSLog(@"best size = %@",NSStringFromCGSize([testLabel sizeThatFits:CGSizeMake(20, 20)]));  

    [testLabel sizeToFit];//会计算出最优的 size 而且会改变自己的size  内部会调用sizeThatFits去计算

    NSLog(@"testLabel sizeToFit frame = %@",NSStringFromCGRect(testLabel.frame));  

    [self.view  addSubview:testLabel];  

layoutIfNeeded方法的应用:

1. NSLayoutConstraint动画 需要在 对NSLayoutConstraint的对象赋值之后调用layoutIfNeeded方法
 
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.blueViewW.constant = 80; //frame /bound 发生改变(系统会对布局进行渲染 但是如果想做动画的话 需要如下自己强制执行渲染) [UIView animateWithDuration:2.0 animations:^{
//对布局进行渲染
[self.view layoutIfNeeded]; //layoutIfNeeded方法只会刷新子控件,因此要使用必须通过它的父类 }]; }

2.tableView cell 的高度的获取