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 就也往左上角去了。解释参考

参考资料

相关推荐
鱼跃鹰飞7 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
程序员清风10 小时前
浅析Web实时通信技术!
java·后端·面试
测试199810 小时前
外包干了2年,快要废了。。。
自动化测试·软件测试·python·面试·职场和发展·单元测试·压力测试
mingzhi6111 小时前
渗透测试-快速获取目标中存在的漏洞(小白版)
安全·web安全·面试·职场和发展
嚣张农民11 小时前
一文简单看懂Promise实现原理
前端·javascript·面试
Liknana13 小时前
Android 网易游戏面经
android·面试
威哥爱编程17 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
程序猿进阶17 小时前
Redis 基础数据改造
java·开发语言·数据库·redis·后端·面试·架构
刘艳兵的学习博客19 小时前
刘艳兵-DBA027-在Oracle数据库,通常可以使用如下方法来得到目标SQL的执行计划,那么通过下列哪些方法得到的执行计划有可能是不准确的?
数据库·oracle·面试·database·刘艳兵