iOS 界面开发 1—— 画 UI 时你应该记在心里的知识

前言

众所周知,我们作为 iOS 开发者,平日的工作里做得最多的就是画 UI,写页面。既然要写页面,自然要知道视图显示的原理,这样才能写出更多功能的、性能更好的页面,所以这篇文章我会讲讲视图的显示和绘制,以及它的坐标系属性。

UIView 与CALayer

UIView

UIView 是视图编程的基础,ta 指的是在屏幕上一块矩形的、管理"内容展示"的东西(比如图片、文字、按钮和视频),大多数情况下iOS 开发工程师都是跟 ta 打交道。它的工作职责是:

  1. Drawing and animation绘制和动画:可以用 UIKit 或者 Core Graphics 绘制内容,也可以用简单的动画展示某个属性的变化

    Core Graphics是一个绘图引擎,是 UIKit 的底层框架,它的 API 都是 C 语言的。常用于自定义绘制

  2. Layout and subview management布局和管理子视图

  3. Event handling处理触摸事件:因为 UIView 是UIResponder的子类,同时它也能通过设置gesture recognizer来响应特定的手势动作。

UIView 其实不是实际负责显示的那个,它有一个非常重要的属性------ CALayer

objc 复制代码
@interface UIView : UIResponder <procotols>
@property(nonatomic,readonly,strong) CALayer  *layer;           
// returns view's layer. Will always return a non-nil value. view is layer's delegate

CALayer

CALayer 不属于 UIKit,它属于 Core Animation,它负责"可视内容的展示"。也就是说,UIView 如果没有CALayer,那么它是不能够展示内容的。反过来说,如果你需要显示一个东西,同时又不需要 ta 响应用户操作,那其实你可以只用 CALayer就能实现该功能。它是轻量级的 UIView。


一个 CALayer要展示可视内容,所以它有一个属性contents,这指向一块缓存区,称之为backing store,backing store里放的是位图bitmap。

bitmap就是要显示到屏幕上的东西

手机屏幕刷新时,会取这个 bitmap,呈现到屏幕上。


CALayer 是个强大的工具,它能设置显示内容 content 的属性(contentGravity、背景颜色、边框、圆角),能播放视频,能设置滚动效果,能设置文字,能设置渐变背景颜色,能实现粒子喷射效果。。。总之就是公鸡中的战斗机。它的具体功能展示可以看看老外Ron Kliffer写的文章:CALayer Tutorial for iOS: Getting Started。不慌,我为您找到了中文翻译

但CALayer 不处理触摸事件,前面说了,触摸事件是 UIView 的任务。

两者的关系

CALayer 是 UIView 的一个属性,UIView 是 CALayer 的 delegate。UIView 是老板,它管理CALayer,它要负责创建 CALayer、添加 CALayer,必要时销毁 CALayer

之所以要用两个东西来负责视图显示和响应操作,很大的一个原因是「代码复用 」。因为Apple 不仅开发了 iOS,还开发了 macOS,这两个系统在「视图怎么显示」这个问题上的解决方案很相似,但是「视图如何响应用户操作」这个问题的解决上就有很大的差异,于是 Apple把前者的解决方案抽出来,做成 Core Animation,后者则是分别做成 UIKit 和 AppKit。这样一来,代码既能复用、在出现问题时也可以减少解决成本。

一个View显示在屏幕上,需要走过怎样的流程?绘制与布局

我简单阐述下步骤,其实只有前两个步骤是 iOS 开发者能够插手的地方:

  1. 布局:CPU 对一个 UIView进行 Frame 布局,设置它的layer 属性的属性(位置、背景色、边框等)。
  2. 显示 (绘制):layer 的可视内容开始绘制。有两种方法可以设置这个显示内容:一是设置 layer.contents。二是重写 UIView 的drawRect: 方法或者重写CALayerDelegatedrawLayer:inContext: 方法。重写这两个方法,我们称之为自定义绘制了。注意,自定义绘制会额外消耗 CPU 的性能,所以Apple 会建议我们尽量不要自定义绘制。
  3. 解码:Core Animation 框架准备发送数据到渲染服务。对图片进行解码,转换图片格式,图片要显示出来,必须从压缩状态转换到未解压状态,出于节省内存的目的,一般都是在绘制前才去解码图片。
  4. 提交:把UIView 和 CALayer的层级关系进行打包,提交给渲染服务
  5. 生成帧缓存:根据图层数据生成前后帧缓存,参照VSync信号和CADisplayLink,切换前后帧缓存
  6. 渲染:将后帧缓存交给 GPU,最终显示到屏幕上。

我们来关注一下能插手的地方

布局

layoutSubviews() 方法------布局子视图

根据已有的约束,决定子视图的大小和位置,它的触发时机如下:

  1. 修改 view 的大小(调用view 的layoutSubviews
  2. 调用addSubview添加子视图时(调用子视图的layoutSubviews
  3. 在 UIScrollView 上滚动(调用 UISCrollView 和ta 的父视图的layoutSubviews 方法)
  4. 更新view 的约束时,(调用view 的layoutSubviews

在什么时候需要重写这个方法?

当autoresizing 和基于约束的布局不满足需求时,就重写这个方法。注意,重写这个方法是有性能损耗的,所以要尽量避免重写。


setNeedsLayout() 方法------标记是否需要重新布局

对某个 view 打上「脏标记」告诉系统这个 view 需要重新布局,系统会在下一次runloop中调用layoutSubviews 进行布局。


layoutIfNeeded() 方法------马上重新布局

告诉系统马上执行打了「脏标记」的layoutSubviews,不必等下一次 runloop。

显示

drawRect: 方法------自定义绘制

自定义绘制,能够实现标准控件以外的显示效果。我们平时用的都是系统自带的标准组件,比如 UILabel,UIImage等,可以设置他们的一些属性来实现需求,但是有的需求压根就不能通过这种方法来做,于是我们需要把目光投向自定义绘制,这里是一个具体例子

开发者不能手动调用这个方法。应该是在 runloop 的某个时间节点,由系统调用。


setNeedsDisplay() 方法------标记谁谁谁需要自定义绘制

打上「脏标记」,告诉系统这个 view 需要重新绘制。后续系统会调用这个 view 的drawRect函数。

UIView 的系统绘制和异步绘制

当调用setNeedsDisplay 函数时,绘制就开始了,它有两条路径,如下:

  • UIView 调用 setNeedsDisplay后,调用自身 layer属性的 setNeedsDisplay ,最终会调用 CALayer 的 display 函数,开始判断 layer 的 delegate 是否实现了displayLayer: 方法。

  • 系统绘制 指的是默认的绘制流程,在主线程生成 bitmap,把 bitmap 设置给 layer.contents。

  • 异步绘制指的是bitmap在子线程生成,生成了 bitmap 以后再回到主线程,把 bitmap 设置给 layer.contents。异步绘制的好处是能减轻主线程的负担,提高界面的流畅性
markdown 复制代码
> Facebook 的[Texture](https://github.com/TextureGroup/Texture)和 ibireme 的[YYAsyncLayer](https://github.com/ibireme/YYAsyncLayer)都是按照这个思想实现的

总结:

  1. 如果想自定义布局 ,就重写layoutSubviews 方法
  2. 如果想自定义绘制 ,就重写 drawRect 方法

视图的坐标系以及属性frame, bounds 和 center

在坐标系中,有几个结构我们经常用到:

结构 作用
CGPoint 表示位置,包含 x,y 两个属性
CGSize 表示尺寸,包含 width 和 height 两个属性
CGRect 表示屏幕上的一块矩形区域,包含 origin 属性(CGPoint 类型,表示矩形左上角)和size 属性(CGSize 类型)

UIView 中的 frame、bounds、center属性都能在CALayer 找到对应的属性,如下

UIView CALayer 作用
frame frame 指定了view大小,和view在父视图 坐标系中的位置。一般用于修改 view 的大小和位置
center position view 中心点在父视图坐标系中的位置,用于修改 view 的位置
bounds bounds 指定了view大小,和在自身坐标系中的位置。常用于绘制,修改 bounds 会影响子视图的位置。
transform affineTransform 用于仿射变换,也就是对 View 进行旋转、缩放、移动等操作
anchorPoint 锚点,CGPoint类型,表示一个相对位置,左上角为(0,0), 右下角为(1,1), anchorPoint 的默认值为(0.5, 0.5)。一般用作 transform 仿射变换时的原点

只改位置的时候,最好用 center 属性,因为 center 属性永远是有效的,即便view 缩放和或者旋转了。而 frame 就不一定了,当视图的变换不等于单位的变换时(if the view's transform is not equal to the identity transform),它就是无效的。详见View Programming Guide for iOS

一个反直觉的设置是,当我们把父视图的 bounds 从(0,0, 200, 200) 修改为(50, 50 , 200 , 200)时,它的子视图 的位置会往左上角移动,代码如下:

ini 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self.view setBackgroundColor:UIColor.whiteColor];
    
    UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    [blueView setBackgroundColor:UIColor.blueColor];
    [self.view addSubview:blueView];
    UITapGestureRecognizer *ge = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onClick)];
    [blueView addGestureRecognizer:ge];
    
    UIView *yellowView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
    [yellowView setBackgroundColor:UIColor.yellowColor];
    
    [blueView addSubview:yellowView];
    
    self.blueView = blueView;
    self.yellowView = yellowView;
}
​
- (void)onClick {
    self.blueView.bounds = CGRectMake(50, 50, 200, 200);
}

这里的解释是:修改后,对于 yellowView,父视图的原点坐标就是在左上角,再往上往左 50 的位置,所以yellowView 就也往左上角去了。解释参考

参考资料

相关推荐
陪学40 分钟前
百度遭初创企业指控抄袭,维权还是碰瓷?
人工智能·百度·面试·职场和发展·产品运营
大数据编程之光2 小时前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
ifanatic4 小时前
[面试]-golang基础面试题总结
面试·职场和发展·golang
程序猿进阶5 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
长风清留扬6 小时前
一篇文章了解何为 “大数据治理“ 理论与实践
大数据·数据库·面试·数据治理
周三有雨18 小时前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
爱米的前端小笔记18 小时前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
好学近乎知o18 小时前
解决sql字符串
面试
我明天再来学Web渗透1 天前
【SQL50】day 2
开发语言·数据结构·leetcode·面试
程序员奇奥1 天前
京东面试题目分享
面试·职场和发展