今天看啥  ›  专栏  ›  Jani

Core Animation 之 View

Jani  · 掘金  ·  · 2018-04-03 02:17

Core Animation 之 View

最近切实的感受到了自己在界面这一块基础原理的匮乏,因此决定深入学习了解一下Core Animation, 但是在学习Core Animation的开端,不禁又怀疑自己,UIWindow是什么?它的作用你真的明白吗?为什么要有UIWindow呢,如果没有的话行不行呢?一系列问题层出不穷,因此我决定在研究学习Core Animation之前去学习学习苹果的官方文档View Programming Guide for iOS, 那么在开始的这一篇我只会去写一写我对于UIView的学习,总结,应该还会有一些问题,希望和大家一起交流交流。


可能在文中会用到不同的词,但是他们是一个含义:
view == view对象 == 视图对象 == UIView对象
window == window对象 == 视窗对象 == UIWindow对象

(注:UIWindow的基类是UIView)

View and Window

View到底是什么?View对象在屏幕上定义了一块矩形的区域并且处理这个区域内部的事件。在UIKIT中,每一个UIView对象都有一个CALayer对象的layer属性,layer对象负责视图内容的绘制,处理视图动画相关的事宜。视图由layer绘制,动画也由layer处理,那么UIView对象做什么呢?其实UIView对象可以理解为是对layer的封装,提供了处理触摸事件的具体功能以及Core Animation底层方法的接口。

什么是Core Animation呢?

image

Core Animation 是是iOS和OSX上可用的图形渲染和动画基础结构,用于为应用的视图和其他视觉元素设置动画。它把大量的绘图工作交给GPU去加速渲染,所以可以实现高帧率高流畅度的动画效果,并不会去负载CPU而使得手机卡顿。

Core Animation 并不是一个绘图系统,而是使用硬件去合成和处理视图内容的基础框架,而这个基础框架的核心就是layer对象,那么layer又是如何对视图内容进行处理的呢?它会将其转化为GPU便宜处理的bitmap,然后GPU再去执行相应的操作,大多数APP中layer都是用来管理视图内容的,但是也可以去单独的创建独立的layer。

那么现在可以对Core Animation可以进行一些底层的了解了:

Core Animation是一套包含图形绘制、投影、动画的Objective-C类集合,该框架包含在QuartzCore.framework中.

image

上图的OpenGL ES是一个C语言写的非常底层的图形处理框架。是个移动设备上绘制2D和3D计算机图形的标准的开源库,广泛地被用在游戏的图形的绘制上负责直接驱动GPU,效率很高,但是使用起来很复杂。

Core Animation的核心就是对于OpenGL ES的抽象,它并不做渲染工作,而是使用OpenGL ES的功能,让GPU去处理渲染。从上图的结构可以知道Core Graphic 的绘制是需要消耗CPU的,而Core Animation渲染消耗的是GPU。Core Animation最繁重的任务是去判断出哪些layer需要被重新绘制,而OpenGL ES要做的就是将layer合并、显示在屏幕上。

工作的流程图

image

Core Graphic, Core Animation, Core Image是三个不同的核心库,它们之间可能会有交互,但是不存在一个包含的关系。上图是它们的工作流程图

当你设置一个 layer 的内容为 CGImageRef 时,Core Animation 会创建一个 OpenGL 纹理,并确保在这个图层中的位图被上传到对应的纹理中。以及当你重写 -drawInContext 方法时,Core Animation 会请求分配一个纹理,同时确保 Core Graphics 会将你所做的(即你在drawInContext中绘制的东西)放入到纹理的位图数据中。一个layer的属性和 CALayer 的子类会影响到 OpenGL 的渲染结果,许多底层的 OpenGL ES 行为被简单易懂地封装到 CALayer 概念中。

回到主题 -> UIView

好了,走偏了我们还是继续聊一聊UIView吧。

视图的层次结构

每一个父视图都会维持一个子视图数组,在数组最后一个子视图就是最上层的子视图,如果子视图重叠了,数组中靠后的子视图位于上方,父视图和子视图的关系会影响子视图的显示,在代码中常见的有两个场景

//1. 要求子视图不透明,父视图透明
layerView.backgroundColor = UIColor.black.withAlphaComponent(0.1)

//2. scrollView自动调整子视图size如何解决?
let scrollView = UIScrollView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))
scrollView.autoresizesSubviews = false
视图的响应事件

当谈及UIView和CALayer的主要区别时,最重要的区别就是UIView是可以响应事件的,那么View是如何响应事件的呢?简单来讲分为两步:

  1. 寻找发生触摸事件的视图(从上往下)
  • 发生触摸事件之后,系统会将该事件加入到一个由UIApplocation管理的事件队列中
  • UIApplication会从事件队列中取出最靠前的事件向下分发,一般先给当前的KeyWindow
  • KeyWindow根据视图结构依次向下分发,直到找到最合适事件处理的view
  1. 找到事件的第一响应者(从下往上)
    事件响应
  • 事件响应由最底层的View开始,如果View处理不了,就向上找父View,一直找到当前Window,如果Window处理不了,会传给UIApplication处理,如果UIApplication处理不了就由UIApplecation的delegate处理。
视图的绘制周期

当view第一次在屏幕上显示的时候,系统会绘制它的内容,然后系统会截取内容的快照,并且将快照作为视图的可见外观,如果你永远不改变视图的内容,那么视图的绘制代码永远不会改变。如果更改了视图的内容,不用直接重新绘制,而是使用setNeedsDisplay或者setNeedsDisplayInRect方法是视图无效。这些方法会告知视图内容以及改变并且需要在下一次进行重绘。如果需要立马重绘,那么需要使用layoutIfNeeded方法。

  • 更改了Frame,或者Bounds的宽和高
  • 分配了包含缩放系数的transform属性
    contendMode
视图的伸缩

有时候只需要可视视图的一部份可以被伸缩,这个在设置按钮背景图片的场景是非常常见的,那么如何让这个常见的需求实现呢?使用resizableImageWithCapInsets:方法,可以确定可缩放的视图的内容,如下:

var image = UIImage.init(named: "image.png")
image = image?.resizableImage(withCapInsets: UIEdgeInsetsMake(10, 10, 10, 10), resizingMode: UIImageResizingMode.stretch)
let imageview = UIImageView.init(image: image)

伸缩背景的效果如下:(使四个角不变,中心进行缩放)

stretch

视图可以实现的动画

视图可以实现的动画效果的属性如下:

  • frame
  • bounds
  • center
  • transform
  • alpha
  • background
坐标形状和坐标系统

UIKit的坐标系以屏幕的左上角为原点

坐标
如果是OpenGL ES或者是Core Graphic的话是以屏幕的左下角为坐标原点的一个笛卡尔坐标系

视图需要注意的几点
  • 在修改视图的transform的时候,每一种变换都是针对视图中心而言的
  • 尽量少的使用drawRact绘制
  • 只要有可能就显示声明视图为Opaque

回到主题之Window

那么window的职责是什么,为什么要有Window的存在呢?没有Window行不行呢? 下面试官网的原文

* It contains your application’s visible content.
* It plays a key role in the delivery of touch events to your views and other application objects.
* It works with your application’s view controllers to facilitate orientation changes.

如果把viewController拿出来看,我们知道viewController一般对应着相应的应用内部模块,如果要和其他的应用交互呢?这个就需要靠Window了;当然还有一点是屏幕的方向,这个也是通过Window来控制的!

涉及窗口的任务

在大多数时候,我们只需要在didFinishLaunchingWithOptions:中初始化window之后就不会去涉及其他的window操作了,如果使用IB的话,连window的初始化也没有必要了;但是在实际的代码过程中仍然可能会涉及两个任务

  • 使用窗口对象将点和矩形转换为窗口的本地坐标系统或从窗口的本地坐标系统转换点和矩形
  • 通过窗口的通知来追踪窗口的变化
    • UIWindowDidBecomeVisibleNotification
    • UIWindowDidBecomeHiddenNotification
    • UIWindowDidBecomeKeyNotification
    • UIWindowDidResignKeyNotification
关于不同的屏幕

其实吧,分屏是一个非常常见的功能,如果接触过分屏的话就需要去仔细的研究研究分屏相关的代码,这一块也是和UIWindow息息相关的。

回到主题之再谈View

关于视图的层次结构
  • 使用tag 使用viewWithTag:搜索对应的视图要比从父视图的子视图结构中搜索要快
  • 调整视图的层级结构(使用调整层级的方法比移除子视图然后重新插入更快) bringSubviewToFront: sendSubviewToBack: exchangeSubviewAtIndex:withSubviewAtIndex:
自动调整尺寸自动化调整布局变更

在布局中有一个很讨厌的事情,就是明明你的frame都已经设定好了,但是依然在运行的时候就会发生变化,其实很多这种情况的原因就是被添加的子视图的frame会随着父视图的frame的变化而发生变化;其二就是有时候子视图需要保持和父视图一致的大小变换,或者维持原有的尺寸不变,这个时候就需要视图的自动化调整布局了。

父视图的 autoresizesSubviews 属性决定所有子视图是否需要调整。如果属性设置为 YES,视图会使用每个子视图的 autoresizingMask 属性决定子视图的位置和尺寸。每个子视图的尺寸变更会对它们的子视图触发同样的布局调整。每一个子视图的autoresizingMask属性如何决定呢?

  • UIViewAutoresizingFlexibleHeight 当父视图高度变更时视图的高度也变更。如果这个常量没被包含,视图的高度将不会变更。
  • UIViewAutoresizingFlexibleWidth 当父视图宽度变更时视图的宽度也变更。如果这个常量没被包含,视图的宽度将不会变更。
  • UIVIewAutoresizingFlexibleLeftMargin 视图的左边缘和父视图的左边缘之间的距离根据需要增长或缩短。如果这个常量没有设置,视图的左边缘与父视图的左边缘之间的距离保持固定。
  • UIViewAutoresizingFlexibleRightMargin 视图的右边缘和父视图的右边缘之间的距离根据需要增长或缩短。如果这个常量没有设置,视图的右边缘与父视图的右边缘之间的距离保持固定。
  • UIViewAutoresizingFlexibleBottomMargin 视图的底部边缘和父视图的底部边缘之间的距离根据需要增长或缩短。如果这个常量没有设置,视图的底部边缘与父视图的底部边缘之间的距离保持固定。
  • UIViewAutoresizingFlexibleTopMargin 视图的顶部边缘和父视图的顶部边缘之间的距离根据需要增长或缩短。如果这个常量没有设置,视图的顶部边缘与父视图的顶部边缘之间的距离保持固定。
视图初始化

在使用code时: initWithFrame: 在使用nib时: initWithCoder: -> awakeFromNib

视图的绘制

在实际的操作中,要尽量少的使用drawRect:方法,如果要使用此方法,明确它的职责:绘制内容。使用的时候要配置环境,绘制内容,尽可能快结束绘制。 提高绘制性能的两个小tips:

  1. 如果你清楚你的视图绘制代码总是以不透明的内容覆盖视图的整个表面,你可以设置视图的 opaque 属性为 YES 提升系统性能。当你标记了视图为不透明时,UIKit 会避开在位于视图背后的绘制代码。这不是唯一缩减绘制花费的时间的方式但也最大限度减少视图和其他内容之间的合成工作。因此,如果你的视图的内容是完全不透明的那么应该设置这个属性为 YES。如果你的视图不能保证内容总是不透明的,你应该设置这个属性为 NO。
  2. 另一个提升绘制性能的方式,尤其在滚动过程中,是设置视图的 clearsContextBeforeDrawing 属性为 NO。当这个属性设置为 YSE 时,在你的 drawRect: 方法调用之前 UIKit 把将要被 drawRect: 方法更新的区域自动填充为半透明的黑色。设置这个属性为 NO 会消除填充操作的开销但会加重你的应用程序通过 drawRect: 方法填充更新矩形的内容的负担。
视图的动画

这个就非常常见了,大多使用的是UIView自带的Block动画,这个就不在细聊了,有一点想提一提,就是普通的动画只能是从A -> B两种状态之间进行切换,如果加上交互的话,动画就会变得不流畅了,所以Apple在iOS10添加了一个新的特性:UIViewPropertyAnimator ,这个在后面再聊吧。

总结

也不知道为什么写了这么多,其实写的时候,主要还是很多东西都是有关联的,一旦深入其实还会有非常多的东西在里面,以上是我的学习和一些总结。如果写的不好,欢迎指出问题,这样我就可以慢慢改进了,下一篇,我会用比较短的篇幅聊一聊UIViewController,然后就正式的进入我的Core Animation的学习过程中了。




原文地址:访问原文地址
快照地址: 访问文章快照