编程

当前位置:永利皇宫463登录 > 编程 > 特定图层

特定图层

来源:http://www.makebuLuo.com 作者:永利皇宫463登录 时间:2019-09-12 14:29

你可能从它们的名字中猜测这些方法给每个CALayer实例添加滚动方法,但事实上它们只是CAScrollLayer中的图层的实用方法。scrollPoint:方法向上搜索图层树来寻找第一个可用的CAScrollLayer,然后滚动它使的指定点可见。scrollRectToVisible:方法对矩形做同样的事情。visibleRect属性决定CAScrollLayer现在可见的图层部分。

表6.15 使用AVPlayerLayer播放视频import UIKitimport AVFoundationclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame.size if (screenSize.width > screenSize.height) { // 获得视频URL let URL = NSBundle.mainBundle().URLForResource("Ship", withExtension: "mp4") // 创建播放器及其图层 let player = AVPlayer let playerLayer = AVPlayerLayer(player: player) // 设置播放器图层帧并加到视图中 playerLayer.frame = self.containerView.bounds self.containerView.layer.addSublayer(playerLayer) // 播放视频 player.play() } }}

使用基于图层的视图结构而非由有组织的CALayer的优点是当你仍需要访问所有的底层CALayer特性时,你不会丢失由UIView类提供的高层的API(例如自动调整大小, 自动布局以及事件处理)。

在iOS上可高效绘制的图像尺寸是有上限的。所以在屏幕上显示的图像最终会转换为OpenGL纹理,OpenGL有个最大的纹理,OpenGL有一个最大的纹理大小(通常是20482048或40964096,这取决于设备模型)。如果你尝试展示一张大于最大纹理大小的图像,即使这张图像已经存在RAM中,你仍会看到一些糟糕的表现,因为Core Aniamtion被迫使用CPU而不是更快的GPU来加载图像。(看第12章“速度微调”和第13章“高效绘图”来获得关于软件绘制和硬件绘制的区别。)

最后我们将讲解AVPlayerLayer。尽管它不是Core Animaiton框架中的一部分,AVPlayerLayer是另一框架的例子(在这里是AVFoundation),它与Core Animation紧密结合,提供了一个CALayer子类来显示一个自定义内容类型。

但你可能还是想在一个现实中的应用里使用用有组织的CALayer而非基于图层的视力结构,其原因有:

在第2章,我们讲解了图层的contentsRect属性的使用,这是在一个图层中显示一张大图像的一小部分的好的解决方法。但当你的图层有子图层时,这不是一个好方法,因为每当你想滚动可视化区域时,你需要手动重新计算并更新子图层的位置。

图片 1图6.16 AVPlayerLayer中播放中视频的某一静态帧

这个章节讲述了图层树——这个平行存在于UIView层次结构下的CALayer对象的层次结构,用于构建iOS接口。我们也创建了我们自己的CALayer并将之添加到图层树中作为我们的实验。

表6.7 使用locations数组来偏移渐变import UIKitclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame if (screenSize.width > screenSize.height) { // 创建渐变图层并加入到容器视图中 let gradientLayer = CAGradientLayer() gradientLayer.frame = self.containerView.bounds self.containerView.layer.addSublayer(gradientLayer) // 设置渐变颜色 gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.yellowColor().CGColor, UIColor.greenColor().CGColor] // 设置位置 gradientLayer.locations = [0.0, 0.25, 0.5] // 设置渐变的起点和终点 gradientLayer.startPoint = CGPointMake gradientLayer.endPoint = CGPointMake } }}

当然,因为AVPlayerLayerCALayer的一个子类,它继承了它所有的特性。我们不一定要在一个简单的矩形中播放视频;通过6.16中的额外代码,我们可以3D旋转视频并添加圆角、彩色边框、遮罩、投影等。

接下来,我们可以直接在代码中引用CALayer以及它的属性、方法。在表1.1中,我们程序化创建了一个新的CALayer,设置它的backgroundColor属性,然后将它作为子图层添加到 layerView中的图层中。(这个代码假设我们通过Interface Buiilder创建了这个视图并且早已经链接到layerView出口。)图1.5显示了结果。

让我们尝试用CATextLayer显示一些文字。表6.2展示了设置和显示CATextLayer的代码,图6.2展示了结果。

尽管我们程序化地创建了AVPlayerLayer,我们仍将其加入一个容器视图而不是直接加到到控制器主视图中。这样我们就可以用普通的自动布局约束来居中图层;否则,我们不得不在设备旋转时程序化调整其位置,这是因为Core Animation不支持自动尺寸和自动布局(看第3章“图层几何”获得详细解释)。

表1.1 向视图添加一个蓝色的子图层import UIKitclass ViewController: UIViewController { @IBOutlet weak var layerView: UIView! override func viewDidLoad() { super.viewDidLoad() // 创建子图层 let blueLayer = CALayer() blueLayer.frame = CGRectMake(50.0, 50.0, 100.0, 100.0) blueLayer.backgroundColor = UIColor.blueColor().CGColor // 加入到白色的view中 self.layerView.layer.addSublayer(blueLayer) }}

我们将成一个相当大的图开始——在这个例子中,一个2048*2048的雪人。为了从CATiledLayer中受益,我们需要将它切成几个小图像。你可以程序化的做,但如果你想加载整张图像然后在运行时切割它,你会失去大多CATiledLayer设计提供的加载性能优势。理想状态下,你想用预处理来代替这一操作。

图片 2图6.17 3d旋转、有边框和圆角的AVPlayerLayer

让我们从创建一个用于操作图层属性的简单项目开始。在Xcode中使用Single View Application模板创建一个新的iOS项目。

在我们开始前,你需要在项目中添加GLKitOpenGLES框架。接下来你应该实现表6.14的代码,它使用OpenGL ES 2.0绘图上下文做了几乎最少的GAEAGLLayer设置,然后渲染了一个彩色三角形。

在我们开始前,我们需要向我们项目中添加AVFoundation框架,因为它并没有默认项目模板中引入。然后看表6.15中创建简单视频播放器的例子。图6.16展示了播放中的视频播放器。

CALayer backgroundColor属性是CGColorRef类型的,并不是像UIView类的backgroundColor一样是UIColor类型的,所以当设置颜色时我们需要使用UIColor对象的CGColor属性。如果你喜欢你也可以直接通过使用Core Graphics方法来直接创建一个CGColor,但使用UIColor可以避免当你不再需要使用这个颜色手动释放它的麻烦。

图片 3图6.3 设置contentsScale来适配屏幕的效果

AVPlayerLayer用来在iOS上播放视频。它是高层API如MPMoviePlayer的底层实现,提供视频显示的底层控制。AVPlayerLayer的使用也是相当直接的:你可以使用+playerLayerWithPlayer:方法创建一个早已绑定的视频图层的图层,或先创建一个图层然后使用player属性绑定AVPlayer实例。

每一个UIView都有一个layer属性,这个属性是一个CALayer的实例。视图负责创建和管理这个layer,并确保当子视图从视图层次结构中增加或移除时,它们背后的图层会在图层树中平行地联系起来。

为了高效使用Core Animaiton,你需要决定内你要绘制的内容类型(矢量图形、位图、粒子、文字等)然后选择一个适当的图层类型来展示内容。只有一部分内容类型在Core Animaiton中有优化;所以如果你要绘制的东西不能很好的匹配任何标准的图层类型,你将很难得到高性能的表现。

这一章提供了许多特定图层类型的总览,以及使用它们达到的效果。我们大多只讲解了基础知识;有些类如CATiledLayerCAEmitterLayer的知识可以作为独立的章节来讲。然而,关键要记住的是CALayer是万金油,并没有为所有绘制情况进行优化。为了获得Core Animation的最佳性能,你需要自行选择最适合的工具,希望你已经受到鼓舞来深入了解不同的CALayer子类及它们的能力。

在屏幕中央创建一个小视图(大概200*200像素)。你可以根据你喜爱的方式,用程序代码或Interface Builder来实现这一切。只要确保你在你的view controller中包含了这一属性来让我们可以直接访问这一视图。我们将之命名为layerView

图片 4图6.11 使用CAScrollLayer来创建凑合的滚动视图

表6.16 给视频增加变形、边框和圆角override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame.size if (screenSize.width > screenSize.height) { // 获得视频URL let URL = NSBundle.mainBundle().URLForResource("Ship", withExtension: "mp4") // 创建播放器及其图层 let player = AVPlayer let playerLayer = AVPlayerLayer(player: player) // 设置播放器图层帧并加到视图中 playerLayer.frame = self.containerView.bounds self.containerView.layer.addSublayer(playerLayer) // 变形图层 var transform = CATransform3DIdentity transform.m34 = -1.0 / 500.0 transform = CATransform3DRotate(transform, CGFloat, 1, 1, 0) playerLayer.transform = transform // 添加圆角和边框 playerLayer.masksToBounds = true playerLayer.cornerRadius = 20.0 playerLayer.borderColor = UIColor.redColor().CGColor playerLayer.borderWidth = 5.0 // 播放视频 player.play() }}

但这些情况都很少见,通常情况下,基于图层的视图往往更容易使用。

我们真正想要的是一个用CATextLayer作为主图层UILabel子类,然后会自动调整视图的大小,因此不用担心主图像被裁剪。

我们在这章的CAEmitterLayerAVPlayerLayer中接触到了一点动画。在第二部分中,我们从隐式动画开始深入了解动画属性。

  • 你可能要写一些同样需要运行于Mac中的跨平台的代码。
  • 你可能要使用许多CALayer的子类(第6章“特定图层”)而且不想创建新的UIView的子类去管理它们。
  • 你可能在做高性能的工作,而任何一点多余的UIView对象会导致不可忽略的差异(尽管通常在这种情况下,你可能会使用例如OpenGL的工具来绘制)。

我们说过CATextLayerUILabel表现性能更好,以及一些额外的布局选项和对iOS5中富文本的支持。但相对于正常标签,它使用起来十分不便。如果我们想用一个UILabel的好用的替代品,我们应该在Interface Builder中创建我们的标签,它们也应该尽可能表现的像正常的视图。

图片 5图1.1 一个典型的iOS屏幕和构成它的视图层次

CAGradientLayer用来产生两种或多种颜色之间的平滑渐变。可以通过Core Graphics来将等同于CAGradientLayer的效果于普通的图层主图像中,但使用CAGradientLayer的优点在于绘制是硬件加速的。

为什么iOS有这样两个基于UIViewCALayer的平行层次结构?为什么不是一个结构用于处理一切?原因是用于分离职责来防止重复性的代码。事件和用户接口在iOS和Mac OS上的工作相当不同。一个基于多点触控的用户接口从根本上有别于基于鼠标和键盘的用户接口。这就是为什么iOS有UIKitUIView而Mac OS有AppKitNSView。他们功能相似却在实现上有显著差异。

// 确定贴图坐标let bounds = CGContextGetClipBoundingBoxlet scale = UIScreen.mainScreen().scalelet x = floor(bounds.origin.x / layer.tileSize.width * scale)let y = floor(bounds.origin.y / layer.tileSize.height * scale)

那如果CALayer仅仅是UIView的内在工作的实现细节,为什么我们需要深入了解它呢?Apple确实恰当地提供了简单好用的UIView借口,但因此我们就不需要去直接深穷Core Animation本身的细节了吗?

你将很少需要手动设置一个CAEAGLLayer(而是用GLKView),但让我们花点时间了解一下旧时的目的。特别的,我们将设置一个OpenGL ES 2.0上下文,这是所有现代iOS设备的标准。

相比之下绘制、布局以及动画在如iPhone和iPad的触摸屏设备下和笔记本和台式机之中的概念几乎一致。通过分离出这些功能逻辑到独立的Core Animation框架中,Apple可以在iOS和Mac OS中共用这些代码,使得对于Apple自己的操作系统开发小组和第三方开发者们在开发针对这两种平台的应用时有更为相似的体验。

表6.8 使用CAReplicatorLayer重复图层import UIKitclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame if (screenSize.width > screenSize.height) { // 创建复制器图层并加入到容器视图中 let replicator = CAReplicatorLayer() replicator.frame = self.containerView.bounds self.containerView.layer.addSublayer(replicator) // 配置复制器 replicator.instanceCount = 10 // 给每个实例应用变形 var transform = CATransform3DIdentity transform = CATransform3DTranslate(transform, 0, 200, 0) transform = CATransform3DRotate(transform, CGFloat(M_PI / 5.0), 0, 0, 1) transform = CATransform3DTranslate(transform, 0, -200, 0) replicator.instanceTransform = transform // 给每个实例应用颜色偏移 replicator.instanceBlueOffset = -0.1 replicator.instanceGreenOffset = -0.1 // 创建子图层并将它放进复制器中 let layer = CALayer() layer.frame = CGRectMake(100.0, 100.0, 100.0, 100.0) layer.backgroundColor = UIColor.whiteColor().CGColor replicator.addSublayer } }}

如果你先前开发过一个iOS或Mac OS的应用,你可能会对视图的概念比较熟悉。一个视图是一个用于显示内容(如图象、文字或视频等)的矩形对象,并且处理用户鼠标点击或者触摸手势等的输入行为。视图可以构建于另一个视图中来形成一种层次的结构,在这种层次中每个视图控制它子视图的位置。图1.1展示了一个典型的视图层次。

你可能也注意到了贴图并不能在Retina分辨率下显示。为了在设备的相应分辨率下渲染CATiledLayer,我们需要设置图层的contentsScale来适配UIScreen缩放,如:

事实上,它们并不是两个,而是四个这样的层次,每一个用于实现不同的功能。除了视图层次和图层树之外还有表示树(presentation tree)和渲染树(render tree)两个。这将分别在第7章“隐式动画”和第12章“速度调整”中进行讲解。

表6.12 一个简单的滚动CATiledLayer的实现import UIKitclass ViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame if (screenSize.width > screenSize.height) { // 增加贴图图层 let tileLayer = CATiledLayer() tileLayer.frame = CGRectMake(0, 0, 2048, 2048) tileLayer.delegate = self self.scrollView.layer.addSublayer(tileLayer) // 配置滚动视图 self.scrollView.contentSize = tileLayer.frame.size // 绘制图层 tileLayer.setNeedsDisplay() } } override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) { let layer = layer as! CATiledLayer // 确定贴图坐标 let bounds = CGContextGetClipBoundingBox let x = floor(bounds.origin.x / layer.tileSize.width) let y = floor(bounds.origin.y / layer.tileSize.height) // 加载贴图 let imageName = NSString(format: "Snowman_%02i_%02i", x, y) let imagePath = NSBundle.mainBundle().pathForResource(imageName as String, ofType: "jpg")! let tileImage = UIImage(contentsOfFile: imagePath) // 绘制贴图 UIGraphicsPushContext tileImage?.drawInRect UIGraphicsPopContext() }}

就是这些背后的图层负责你在屏幕上看见的一切东西的显示和动画。UIView是一个简单的包装用于提供针对iOS的方法,诸如触摸处理以及相应Core Animation的底层方法的高层接口。

我们可以用有这一路径的CAShapeLayer来创建一个混有圆角和直角的视图。如果我们想要将视图内容裁剪成这个形状,我们可以使用我们的CAShapeLayer作为视图主图像的mask属性而不是添加为子图层。(见第4章“视觉特效”中对图层遮罩的完整解释。)

  1. 译者使用的是Xcode6.4和Swift,并不需要这一步骤。因为已经import UIKit无需再import QuartzCore。 ↩

并没有强制要求提供locations数组,但如果你这么做了,你必须保证位置数和颜色数一致,否则你将得到一个空白的渐变。

因此,让我们创建一个CALayer并把它做为我们白色视图的图层的子图层。尽管layer属性已经在UIView类接口中暴露,但标准的Xcode的iOS项目模板并没有饮食Core Animation的头文件,因此在我们给项目添加相应的框架之前,我们并不能高用任何方法或者访问这处个layer的任何属性。为了实现这一功能,首先添加QuartzCore框架在程序的target's Build Phases中,并且在代码中import QuartzCore在viewController.swift中。[1]

自己实现相应方法是十分粗暴的,但CAScrollLayer避免了你这个麻烦,几乎是在你图层滚动时会调整它的存在。

我们将在接下来的章节中分别介绍这些特性,但是现在让我们一起看看如何在一个应用中使用CALayer

CATransformLayer解决了这一问题。CATransformLayer不同于一般的CALayer,它不能显示自身的内容;它的存在只是为了管理一个可以应用于其子图层的变形。CATransformLayer并不会平面化其子图层,所以可以用来构造3D结构的层次,就像我们的手臂的例子一样。

图片 6图1.2 图层树结构反映的相应的视图层次

如果你想在图层内显示文字,你当然可以用Core Graphics使用图层委托直接向图层内容内绘制文字(这是UILabel工作原理的本质)。如果你直接操作图层是十分笨拙的方法,尽管不是使用基于图层的视图。你将需要创建一个类来作为每个图层显示文字的图层委托,然后写下决定哪个图层要显示字符串的逻辑代码,更不用说跟踪不同的字体、颜色等。

在iOS中,视图都继承自一个共同的基类——UIViewUIView用于处理触摸事件、支持基于Core Graphics的绘制、仿射变形以及如滑动、渐变等简单动画。

CATextLayer font属性并不是UIFont,它是CGTypeRef。这允许你根据需求用CGFontRefCTFontRef(一个Core Text font)来指定字体。字体大小也可以单独用fontSize属性设置,因为CTFontRefCGFontRef并不像UIFont一样有点大小。这个例子展示了如何将UIFont转换成CGFontRef

CALayer并无法察觉响应者链(iOS用于在视图层次间传递触摸事件的机制),因此尽管它确实提供方法用于确认是否一个确定的触摸点在某个图层之中(这将在第三章“图层几何学”中进一步讲述),CALayer并不能响应事件。

图片 7使用UIScrollView滚动CATiledLayer

Core Animation是一个有误导性的名字。你也许猜测它的主要目的是用于动画制作,然而事实上动画只是这个框架中的一方面,这个名字最初用于Layer Kit的动画核心的简称。

-  scrollPoint:  p;-  scrollRectToVisible:  r;@property CGRect visibleRect;

CALayer的概念与UIView非常类似。像视图一样,图层也是可以组成层次树结构的矩形对象,并同样包含如图象、文字或背景颜色等内容,控制着子图层的位置。它们有用于实现动画和变形的方法和属性,而UIView的主要特性中用户交互是唯一不归CALayer处理的。

在表6.8中,我们在屏幕中央创建一个小的白色方形图层,然后用CAReplicatorLayer将其转 为十个图层的环。instanceCount属性指定图层应该被重复多少次。instanceTransform应用一个CATransform3D(在这里,是位移并旋转使图层到达圆中的下一点)。

在Mac OS 10.8之前,一个显著的缺点就是使用基于图层的视图层次而不是每个视图拥有一个独立的CALayer树结构。但iOS中的轻量级UIView类在使用图层时几乎没有不好的影响。(在Mac OS 10.8中NSView的表现同样有了巨大的进步。)

表6.9 用CAReplicatorLayer自动绘制镜像import UIKitimport QuartzCoreclass ReflectionView: UIView { var layerClass: AnyClass { get { return CAReplicatorLayer.classForCoder() } } func setUp() { // 配置复制器 var layer: CAReplicatorLayer = CAReplicatorLayer(layer: self.layer) layer.instanceCount = 2 // 将镜像实例移到下面并且垂直翻转 var transform: CATransform3D = CATransform3DIdentity let verticalOffset: CGFloat = self.bounds.size.height + 2 transform = CATransform3DTranslate(transform, 0, verticalOffset, 0) transform = CATransform3DScale(transform, 1, -1, 0) layer.instanceTransform = transform // 减少镜像图层的透明度 layer.instanceAlphaOffset = -0.6 } override init(frame: CGRect) { super.init(frame: frame) self.setUp() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.setUp() } override func awakeFromNib() { super.awakeFromNib() // 当视图从nib中创建时会调用这个 self.setUp() }}

在我们开始讨论动画之前,我们将从图层树开始讲解Core Animation的静态构成和布局特征。

tileLayer.contentsScale = UIScreen.mainScreen().scale
  • 投射阴影、圆角和有色彩的边框
  • 3D变形和位移
  • 非矩形的边界
  • 内容的透明遮罩
  • 多步非线性动画

Reflections通过使用CAReplicatorLayer给单独一个复制图层应用一个负缩放因子的变形,你可以创建一个给定视图(或一个完整视图层次)的内容镜像,创建一个实时的“镜像”效果。

图片 8图1.3 一个白色的UIView在灰色背景中

表6.1 使用CAShapeLayer绘制火柴人图像import UIKitclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 创建路径 let path = UIBezierPath() path.moveToPoint(CGPointMake) path.addArcWithCenter(CGPointMake, radius: 25, startAngle: 0, endAngle: CGFloat, clockwise: true) path.moveToPoint(CGPointMake) path.addLineToPoint(CGPointMake) path.addLineToPoint(CGPointMake) path.moveToPoint(CGPointMake) path.addLineToPoint(CGPointMake) path.moveToPoint(CGPointMake) path.addLineToPoint(CGPointMake) // 创建形状图层 let shapeLayer = CAShapeLayer() shapeLayer.strokeColor = UIColor.redColor().CGColor shapeLayer.fillColor = UIColor.clearColor().CGColor shapeLayer.lineWidth = 5 shapeLayer.lineJoin = kCALineJoinRound shapeLayer.lineCap = kCALineCapRound shapeLayer.path = path.CGPath // 加入到容器视图中 self.containerView.layer.addSublayer(shapeLayer) }}

我们可以通过简单地使用另一个UIView并把它加为之前创建的视图的子视图来实现这一效果(通过代码或者Interface Builder),但这样并不会教给我们任何关于图层的知识。

尽快可以不用GLKit完成这一切,但会花费许多额外工作来设置顶点碎片阴影,这是用一门叫GLSL的类C语言写的自包含程序,将会在运行时载入图形硬件。编写GLSL代码与设置EAGLLayer并不直接想着,所以我们会用GLKBaseEffect来抽象阴影逻辑。此外,我们将用旧方法来做这一切。

Core Animation是一个组合引擎,他的主要职责在于在屏幕上尽可能快地将不同的视觉内容组合起来。这些内容被分成独立的层存储在叫图层树的层次结构中。图层树组成了整个UIKit的根基,以及你在一个iOS应用中所有在屏幕上的东西。

在一个真实OpenGL应用中,我们可能想使用NSTimerCADisplayLink(见第11章“基于定时器的动画”)来每秒调用-drawFrame方法60次,我们会将绘制和几何产生分离来防止每帧重新产生三角形(因此我们也可以绘制其它非三角形的东西),但这应该足够演示这一原则了。

图片 9图1.4 添加QuartzCore框架到项目中

图片 10图6.8 用CAReplicatorLayer创建的图层环

这在某种程度下是正确的,如果出于简单的目的,我们并不需要真的去深究CALayer,因为Apple使其可以通过用简单的高级API来间接作用于动画等的特性中。

可以在ReflectionView找到一个更完整的ReflectionView类的开源实现,它有一个可调的渐隐效果(用CAGradientLayer和图层遮罩实现)。

但是这种便利性也带来了灵活性的丧失。如果你想做一些有别于常的东西,或者使用一些Apple并没有在UIView的类接口中使用的特性,那你别无选择只能深入到Core Animation之中去调整那些底层一点的选项。

图片 11图6.9 在Interface Builder中使用ReflectionView图片 12图6.10 ReflectionView自动在运行时创造镜像

在第2章“主图像”中,我们将讲解CALayer的主图像以及Core Animation提供的操作其如何显示的属性。

差异大小是不同的(取决于使用的特定字体和字符),但一般是相当小的,但当你想用正常标签和CATextLayer实现完全相同的样子的时候,你需要记得这一点。

一个视图只有唯一个主图层但可以拥有无数的附加图层。正如表1.1所展示的,你可以显式地创建独立的图层并把它们作为子图层加入到这个视图的主图层中。尽管可以通过这种方式添加图层,但更多情况下你只需要使用到视图和它们的主图层,并不需要手动创建额外的图层。

表6.1展示一个简单的火柴人图像的绘制代码,它使用了一个CAShapeLayer来渲染。CAShapeLayer path属性被定义为CGPathRef,但我们使用UIBezierPath辅助类来创建路径,这避免了我们手动释放CGPath的烦恼。图6.1展示了结果。它并不真的是一个Rembrandt,但你知道方法就好!

怪物有好多层,洋葱有好多层,你知道吗?我们都有好多。--Shrek

如果你仔细看文字,你们发现有一点奇怪;文字是像素化的。这是因为它没有在Retina分辨率下渲染。第2章提到的contentsScale属性,这是用来决定图层渲染的分辨率。contentsScale属性默认为1.0而不是屏幕缩放因子。如果你想要Retina级别的文本,我们得用下列代码给CATextLayer设置contentsScale来匹配屏幕缩放。

我们说过图层并不能像UIView那样处理触摸事件,那么他们能做什么视图所不能做的?下面列举了一些不能通过UIView而仅能通过CALayer使用的特性:

到目前为止,我们使用过CALayer类,我们发现它有一些有用的图像绘制和变形能力。但Core Animation图层不仅是用来图像和颜色的。这一章将讲述其它你可以用来扩展Core Animation绘制能力的图层类。

如果你运行这个项目,应该可以看见一个白色方块在一个浅灰色的背景中。如果你并没有看到这个情形,你可能需要微调这个窗口或视图的背景颜色。

既然我们有了这些贴图图像,我们需要创建一个iOS应用来使用它们。CATiledLayer完美兼容UIScrollView,所以为了实现这一例子的目的,我们会将CATiledLayer放入UIScrollView中。除了设置图层和滚动视图边界来适应我们的总图像大小外,我们所有需要做的只剩下实现-drawLayer:inContext:方法,这将在CATiledLayer需要加载一张新的贴图时调用。

图片 13图1.5 一个小的蓝色CALayer构架于一个白色的UIView之中

表6.5 使用CATransformLayer组织3D图层层次import UIKitimport CoreTextclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! func faceWithTransform(transform: CATransform3D) -> CALayer { // 创建立方体面图层 let face = CALayer() face.frame = CGRectMake(-50, -50, 100, 100) // 添加随机颜色 let red: CGFloat = CGFloat / CGFloat let green: CGFloat = CGFloat / CGFloat let blue: CGFloat = CGFloat / CGFloat face.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).CGColor // 添加变形并返回 face.transform = transform return face } func cubeWithTransform(transform: CATransform3D) -> CALayer { // 创建立方体图层 let cube = CATransformLayer() // 添加面1 var ct = CATransform3DMakeTranslation cube.addSublayer(self.faceWithTransform // 添加面2 ct = CATransform3DMakeTranslation ct = CATransform3DRotate(ct, CGFloat, 0, 1, 0) cube.addSublayer(self.faceWithTransform // 添加面3 ct = CATransform3DMakeTranslation(0, -50, 0) ct = CATransform3DRotate(ct, CGFloat, 1, 0, 0) cube.addSublayer(self.faceWithTransform // 添加面4 ct = CATransform3DMakeTranslation ct = CATransform3DRotate(ct, CGFloat, 1, 0, 0) cube.addSublayer(self.faceWithTransform // 添加面5 ct = CATransform3DMakeTranslation(-50, 0, 0) ct = CATransform3DRotate(ct, CGFloat, 0, 1, 0) cube.addSublayer(self.faceWithTransform // 添加面6 ct = CATransform3DMakeTranslation(0, 0, -50) ct = CATransform3DRotate(ct, CGFloat, 0, 1, 0) cube.addSublayer(self.faceWithTransform // 在容器中居中立方体图层 let containerSize = self.containerView.bounds.size cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0) // 应用变形并返回 cube.transform = transform return cube } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame if (screenSize.width > screenSize.height) { // 设置透视变形 var pt = CATransform3DIdentity pt.m34 = -1.0 / 500.0 self.containerView.layer.sublayerTransform = pt // 设置立方体1的变形并添加它 var c1t = CATransform3DIdentity c1t = CATransform3DTranslate(c1t, -100, 0, 0) let cube1 = self.cubeWithTransform self.containerView.layer.addSublayer // 设置立方体2的变形并添加它 var c2t = CATransform3DIdentity c2t = CATransform3DTranslate(c2t, 100, 0, 0) c2t = CATransform3DRotate(c2t, CGFloat, 1, 0, 0) c2t = CATransform3DRotate(c2t, CGFloat, 0, 1, 0) let cube2 = self.cubeWithTransform self.containerView.layer.addSublayer } }}

这看起来并不是非常赞,因此让我们加一点色彩,我们将在这个白色方块里面放一个小的蓝色方块。

精明的你可能想知道到底什么时候需要使用CAScrollLayer,既然你可以简单的使用CALayer并且调整它的boudns源点。事实上并没有什么时候需要使用它。UIScrollView并不使用UIScrollLayer,它只是简单的通过直接操作图层bounds来实现的。

变形是逐渐增加的,每一个实例位置是相对于前一个实例的。这就是为什么复制品不都结束于同一个位置。图6.8展示了最终结果。

图片 14图6.15 用OpenGL在CAEAGLLayer中渲染的简单三角形

表6.12展示了代码,图6.12展示了结果。

  • preservesDepth,它控制是否一个3D粒子系统会扁平化进一个图层或是它容器图层的其它图层在3D空间中混合。
  • renderMode,它控制了粒子图像如何视觉化地混合。你可能注意到我们的例子将它设置成kCAEmitterLayerAdditive,其效果是混合粒子的光影。如果我们将其设为默认值kCAEmitterLayerUnordered,结果会变得不是很美观。图片 15图6.14 禁用增加混合的火焰粒子

colors数组可以容纳任意多的你想要的颜色,所以很容易创建如同彩虹的多级渐变。默认情况下,渐变中的颜色会平分,但我们可以使用locations属性来调整间距。

表6.11 一个将图像切成贴图的终端应用#import <Foundation/Foundation.h>#import <AppKit/AppKit.h>int main(int argc, const char * argv[]) { @autoreleasepool { // 处理不正确的参数 if (argc < 2) { NSLog(@"TileCutter arguments: inputfile"); return 0; } // 输入文件 NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding]; // 贴图大小 CGFloat tileSize = 256; // 输出路径 NSString *outputPath = [inputFile stringByDeletingPathExtension]; // 加载图像 NSImage *image = [[NSImage alloc] initWithContentsOfFile: inputFile]; NSSize size = [image size]; NSArray *representations = [image representations]; if ([representations count]) { NSBitmapImageRep *representation = representations[0]; size.width = [representation pixelsWide]; size.height = [representation pixelsHigh]; } NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height); CGImageRef imageRef = [image CGImageForProposedRect: &rect context: NULL hints: nil]; // 计算行列 NSInteger rows = ceil(size.height / tileSize); NSInteger cols = ceil(size.width / tileSize); // 创建贴图 for (int y = 0; y < rows; ++y) { for (int x = 0; x < cols; ++x) { // 取出贴图图像 CGRect tileRect = CGRectMake(x * tileSize, y * tileSize, tileSize, tileSize); CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect); // 转换成jpeg数据 NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage]; NSData *data = [imageRep representationUsingType: NSJPEGFileType properties: nil]; CGImageRelease(tileImage); // 存储文件 NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jp", x, y]; [data writeToFile: path atomically: NO]; } } } return 0;}

当提及iOS上的高性能图像时,最后要说的是OpenGL。它也可能是最后的办法,至少对非游戏应用是这样的,因为相比Core AnimationUIKit框架,它不是一般的复杂。

图片 16图6.1 一个使用`CAShapeLayer`绘制的简单火柴人

图6.4展示了结果。(注意红色的,有下划线的文字。)

译者实现的效果有点问题,还没有查明原因:

图片 17文字发生重叠

图片 18图6.7 使用locations数组偏移至左上角的三色渐变

> path/to/TileCutterApp path/to/Snowman.jpg

这样就解决了像素化问题。

Snowman_00_00.jpgSnowman_00_01.jpgSnowman_00_02.jpg...Snowman_07_07.jpg

在iOS 5中,Apple引入一个新的框架叫做GLKit,它通过提供一个叫GLKViewUIView子类来移除了一个设置OpenGL绘图上下文的复杂性,它帮你处理了大多数启动和绘制。在之前,你自己必须使用CAEAGLLayer来做不同OpenGL绘制缓存的所有底层配置,这是一个CALayer子类设计来直接显示OpenGL图像。

所以让我们尝试用CATransformLayer来代替。第一个要解决的问题是我们在第5章用的视图而不是独立的图层的构建立方体的。我们不能把一个视图后的图层放在另一个不是本身视图的图层里而不弄乱视图层次。我们可以创建UIVIew的一个基于CATransformLayer的子类(使用+layerClass方法),但为了在我们的例子中简化一切,让我们重新用独立的图层来创建立方体。这意味着我们不能像第5章一样在其上显示按钮和标签,但我们现在并不需要这样做。

译者实现效果如下:

图片 19还是有文字重叠

表6.5包含了相应的代码。我们用和第5章一样的基本逻辑来放置每一个立方体表面。但不同于我们之前直接向容器视图中添加立方体表面,我们将它们放入一个CATransformLayer中来创建一个独立的立方体,然后将这两个立方体放入我们的容器中。我们给这立方体表面随机涂色来方便我们不通过标签和光影得以区分它们。图6.5显示了结果。

textLayer.contentsScale = UIScreen.mainScreen().scale

对于一个未变形的图层,图层的bounds大小会匹配它的frame大小。frame是自动从boudns中计算的,所以改变任何属性都会更新。

让我们用CAScrollLayer来创建一个非常基础的UIScrollView替代品。我们将创建一个以CAScrollLayer作为主图层的自定义UIView,然后使用UIPanGestureRecognizer来实现触摸处理。代码展示在表6.10。图6.11展示ScrollView被用来在一个大于本身帧大小的UIImageView中四处拖动。

CAEmitterCell的属性通常有三类:

  • ——CAShapeLayer使用硬件加速绘制,比用Core Graphics绘制图像快很多。
  • 高效内存——CAShapeLayer并不需要像正常CALayer那样创建主图像,所以无论它多大都不需要消耗过多内存。
  • 不会被图层边界裁剪——CAShapeLayer可以自由地画在边界之外。它并不会像你使用Core Graphics绘制在一个普通CALayer中的路径一样被裁剪(正如你在第2章所见)。
  • 不会像素化——当你用变形放大一个CAShapeLayer或者用3D透视变形将其拉近镜头,它并不会像普通图层主图像一样像素化。
表6.10 使用CAScrollLayer实现滚动视图import UIKitclass ScrollView: UIView { var layerClass: AnyClass { get { return CAScrollLayer.classForCoder() } } func setUp() { // 允许裁剪 self.layer.masksToBounds = true // 添加拖动动作识别 let recognizer = UIPanGestureRecognizer(target: self, action: "pan:") self.addGestureRecognizer(recognizer) } override init(frame: CGRect) { super.init(frame: frame) self.setUp() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.setUp() } override func awakeFromNib() { super.awakeFromNib() self.setUp() } func pan(recognizer: UIPanGestureRecognizer) { // 通过当前bounds源点减去pan手势位移量获得偏移值 var offset = self.bounds.origin offset.x -= recognizer.translationInView.x offset.y -= recognizer.translationInView.y // 滚动图层 let layer = self.layer as! CAScrollLayer layer.scrollToPoint // 重置pan手势位移 recognizer.setTranslation(CGPointZero, inView: self) }}

CAReplicatorLayer类用来高效产生相似图层的集合的。它通过绘制一个或多个子图层的复制,并给每一个复制品应用不同的变形。演示起来可以比说的清楚,所以让我们来构建一个例子。

专业化是每一个复杂组织的特性。——Catharine R.Stimpson

表6.4 使用CATextLayer的UILabel的子类——LayerLabelimport UIKitclass LayerLabel: UILabel { var layerClass: AnyClass { get { return CATextLayer.classForCoder() } } var textLayer: CATextLayer { get { return self.layer as! CATextLayer } } func setUp() { // 设置UILabel中的默认设置 self.text = self.text self.textColor = self.textColor self.font = self.font // 我们也应该从UILabel的设置的继承它们 // 但这样就太麻烦了,所以我们就只核心(hard-core)它们 self.textLayer.alignmentMode = kCAAlignmentJustified self.textLayer.wrapped = true self.layer.display() } override init(frame: CGRect) { super.init(frame: frame) self.setUp() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.setUp() } override func awakeFromNib() { // 当用Interface Builder创建标签时调用 self.setUp() } func setText_(text: NSString) { super.text = text as? String // 设置图层文本 self.textLayer.string = text } func setTextColor_(textColor: UIColor) { super.textColor = textColor // 设置图层文本颜色 self.textLayer.foregroundColor = textColor.CGColor } func setFont_(font: UIFont) { super.font = font // 设置图层字体 let fontName: CFStringRef = font.fontName let fontRef: CGFontRef = CGFontCreateWithFontName self.textLayer.font = fontRef self.textLayer.fontSize = font.pointSize }}

CAShapeLayer是一个用矢量图而非位图绘制自身的图层子类。你指定如颜色和线条粗细等属性,使用CGPath指定想要的形状,然后CAShapeLayer会自动渲染它。当然,你也可以直接用Core Graphics在一个普通CALayer中直接向内容里绘制路径(在第2章“主图像”中讲解的),但用CAShapeLayer有如下一些优点:

图片 20火焰爆炸效果

这样就可以让你独立移动每一部分。转动肘可以移动小臂和手但不会移动肩膀。Core Animation图层便于在2D中实现这种层次结构,但不能在3D中实现。这是因为每个图层会将其孩子平面化成一个单独的平面(如第5章“变形”中讲解的一样)。

CAEmitterLayer属性本身控制了整个粒子系统的位置和基本形状。一些属性如birthRatelifetimevelocity复制值定义于CAEmitterCell上。它们像是乘数,因此你可以用一个值加速或增强整个粒子系统。其它值得关注的属性有:

表6.4展示的代码是创建UILabel子类LayerLabel用于使用CATextLayer而不是像正常UILabel样用慢速的-drawRect:方法来绘制文本的。LayerLabel既可以用程序实例化,也可以在Inteface Builder通过添加普通的标签视图然后将其类设置为LayerLabel来实例化。

因为OpenGL并不清楚你的内容,它可以是极快的。通过OpenGL,你可以绘制任何你想画的,只要你提供如何去画必须的几何和阴影逻辑。这使它成为游戏的流行选择(这时,Core Animation的有限的优化过的内容类型并不能总是符合需求),但对于一个通常的界面来说就是大材小用了。

让我们尝试用一个可复用的UIView子类ReflectionView来实现这一想法,它会自动产生内容的镜像。创建这个的代码是很简单的,实际上使用ReflectionView更简单;我们可以简单地向Interface Builder中拖入一个ReflectionView实例,它将会在运行时产生子视图的镜像而不需要向视图控制器中添加其它启动代码。

  • 粒子的某一特殊属性的起始值。例如,color属性指定了会与contents中图像颜色相混合的颜色。在我们的例子中,我们将它设为橘色。

  • 粒子间的值区间。例如,我们项目中的emitssionRange属性被设为2π,表示粒子可以360度无死角发射。通过设置一个较小值,我们会创建一个粒子的圆锥漏洞。

  • 特定值的改变时间。例如,在爆炸项目中我们设置alphaSpeed为-0.4,这意味着粒子的alpha值每秒减少0.4,这会产生了粒子离开发射器的渐出效果。

CAEmitterLayer表现的像是一系列CAEmitterCell实例的容器,它们定义了粒子效果。你可以用模板似的创建一个或多个不同的粒子类型,CAEmitterLayer负责用这些模板实例化粒子流。

这在个简单的例子中,我们只是实现了UILabel一些样式和布局属性,但只需要一点额外的工作,我们就可以创建一个LayerLabel类来支持完整的UILabel(你会发现网上的开源项目中早有了这样的类)。

CAScrollLayer并没有潜在的好用的特性,尽管如此,如果你看CAScrollLayer的头文件,你会注意到它引入了一些分类来扩展CALayer,这包含一些额外的方法和属性:

我们可以继承UILabel然后添加CATextLayer作为子图层并重写其显示文本的方法,但我们仍有由UILabel -drawRect:方法创建的空的主图像。因为CALayer不支持自动尺寸和自动布局,子图层并不会自动跟踪视图的bounds大小,所以每当视图大小改变时我们需要手动更新子图层的边界。

表6.14 用ACEAGLayer绘制三角形#import "ViewController.h"#import <QuartzCore/QuartzCore.h>#import <GLKit/GLkit.h>@interface ViewController ()@property (weak, nonatomic) IBOutlet UIView *glView;@property (strong, nonatomic) EAGLContext *glContext;@property (strong, nonatomic) CAEAGLLayer *glLayer;@property (assign, nonatomic) GLuint framebuffer;@property (assign, nonatomic) GLuint colorRenderbuffer;@property (assign, nonatomic) GLint framebufferWidth;@property (assign, nonatomic) GLint framebufferHeight;@property (strong, nonatomic) GLKBaseEffect *effect;@end@implementation ViewController- setUpBuffers { // 设置帧缓冲 glGenFramebuffers(1, &_framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); // 设置颜色渲染缓冲 glGenRenderbuffers(1, &_colorRenderbuffer); glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer); [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer]; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth); glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight); // 检查结果 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER)); }}- tearDownBuffers { if (_framebuffer) { // 删除帧缓冲 glDeleteFramebuffers(1, &_framebuffer); _framebuffer = 0; } if (_colorRenderbuffer) { // 删除颜色渲染缓冲 glDeleteRenderbuffers(1, &_colorRenderbuffer); _colorRenderbuffer = 0; }}- drawFrame { // 绑定帧缓冲、蛇者视窗 glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); glViewport(0, 0, _framebufferWidth, _framebufferHeight); // 绑定着色程序 [self.effect prepareToDraw]; // 清屏 glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0); // 设置顶点 GLfloat vertices[] = { -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f, }; // 设置颜色 GLfloat colors[] = { 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, }; // 绘制三角形 glEnableVertexAttribArray(GLKVertexAttribPosition); glEnableVertexAttribArray(GLKVertexAttribColor); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices); glVertexAttribPointer(GLKVertexAttribColor, 4, GL_FLOAT, GL_FALSE, 0, colors); glDrawArrays(GL_TRIANGLES, 0, 3); // 显示渲染缓冲 glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer); [self.glContext presentRenderbuffer:GL_RENDERBUFFER];}- viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // 判断横屏 CGSize screenSize = [[UIScreen mainScreen] applicationFrame].size; if (screenSize.width > screenSize.height) { // 设置上下文 self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; [EAGLContext setCurrentContext:self.glContext]; // 设置图层 self.glLayer = [CAEAGLLayer layer]; self.glLayer.frame = self.glView.bounds; [self.glView.layer addSublayer:self.glLayer]; self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking: @NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 }; // 设置基本效果 self.effect = [[GLKBaseEffect alloc] init]; // 设置缓冲 [self setUpBuffers]; // 绘制缓冲 [self drawFrame]; }}- dealloc { [self tearDownBuffers]; [EAGLContext setCurrentContext:nil];}@end

用户界面不能仅用图像构建。一个设计优秀的图标可以很好地传达按钮或控件的意图,但或早或晚你需要一个好的旧式文字标签。

CATransformLayer当构建复杂的3D物体时,如果可以控制独立的元素层次将是十分方便的。例如,假设你在制作一只手臂:你可能想手是手腕的孩子,手腕是前臂的孩子,前臂是手肘的孩子,手肘是上臂的孩子,上臂是肩膀的孩子等。

如果你运行案例代码,你会注意到即使我们没有设置contentsScale文本也不会像素化。另一个使用CATextLayer作为主图层的好处是,contentsScale会由视图自动设置。

如果你只是要支持iOS6及以上版本,基于CATextLayer的标签可能用处不大,但通常来说,使用+layerClasss来创建基于不同图层类型的视图是在你应用的利用CALayer子类的干净可重复利用的好方法。

在第4章“视觉特效”中你学会了如何用CGPath来直接创建一个阴影形状,而不是使用图像。如果你可以用同样的方式创建图层形状将是极好的。

CATiledLayer(不同于大多UIKitCore Animation方法)支持多线程绘制。-drawLayer: inContext:方法可能同时被多线程调用,所以确保你实现的所有绘制代码是线程安全的。

在iOS5中,Apple引入了一个新的CALayer的子类叫CAEmitterLayerCAEmitterLayer是一个被用来创建实时粒子动画如烟、火、雨等的高性能粒子引擎。

表6.6 一个简单的双色对象渐变import UIKitclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame if (screenSize.width > screenSize.height) { // 创建渐变图层并加入到容器视图中 let gradientLayer = CAGradientLayer() gradientLayer.frame = self.containerView.bounds self.containerView.layer.addSublayer(gradientLayer) // 设置渐变颜色 gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.blueColor().CGColor] // 设置渐变的起点和终点 gradientLayer.startPoint = CGPointMake gradientLayer.endPoint = CGPointMake } }}
表6.13 用CAEmitterLayer创建爆炸效果import UIKitclass ViewController: UIViewController { @IBOutlet weak var containerView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 判断横屏 let screenSize = UIScreen.mainScreen().applicationFrame if (screenSize.width > screenSize.height) { // 创建粒子发射器图层 let emitter = CAEmitterLayer() emitter.frame = self.containerView.bounds self.containerView.layer.addSublayer // 配置发射器 emitter.renderMode = kCAEmitterLayerAdditive emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0) // 创建粒子模板 let cell = CAEmitterCell() cell.contents = UIImage(named: "Spark.png")?.CGImage cell.birthRate = 150 cell.lifetime = 5.0 cell.color = UIColor(red: 1, green: 0.5, blue: 0.1, alpha: 1.0).CGColor cell.alphaSpeed = -0.4 cell.velocity = 50 cell.velocityRange = 50 cell.emissionRange = CGFloat * 2.0 // 向发射器添加粒子模板 emitter.emitterCells = NSArray(array: [cell]) as [AnyObject] } }}

CATextLayer也比UILabel渲染快。它在iOS6及之前是不为人知的事实,UIlabel实际上使用WebKit来进行文本绘制,这在你绘制大量文本时带来显著的性能负担。CATextLayer使用Core Text而且显著的快速。

表6.7展示了表6.6中的对角渐变的修改版。我们现在有一个红黄绿三色渐变。locations数组被指定为0.0、0.25和0.5,这会使渐变挤在视图左上角。

译者并没有实现相应效果,因此贴上原著图。

表6.11展示了一个简单的Mac OS命令行应用的代码,它会将图像切成贴图并将它们存储为独立文件供CATiledLayer使用。

在iOS6中,Apple增加了对UILabel和其它UIKit文本视图的属性字符串的直接支持。这是一个便于使用属性文本的有用恶性,但CATextLayer从iOS3.2引入以来都支持属性文本;所以如果你仍需要在你的应用中支持早期的iOS版本,CATextLayer是一个给你的用户界面添加富文本标签的简单方法,你无需处理复杂的Core Text或者陷入使用UIWebView的麻烦中。

表6.2 用CATextLayer实现文字标签import UIKitclass ViewController: UIViewController { @IBOutlet weak var labelView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 创建文字图层 let textLayer = CATextLayer() textLayer.frame = self.labelView.bounds self.labelView.layer.addSublayer(textLayer) // 设置文字属性 textLayer.foregroundColor = UIColor.blackColor().CGColor textLayer.alignmentMode = kCAAlignmentJustified textLayer.wrapped = true // 选择字体 let font = UIFont.systemFontOfSize // 设置图层字体 let fontName: CFStringRef = font.fontName let fontRef: CGFontRef = CGFontCreateWithFontName textLayer.font = fontRef textLayer.fontSize = font.pointSize // 选择文字 let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." // 设置图层文字 textLayer.string = text }}

CAShapeLayer可以用来将任何可以用CGPath表示的形状绘制出来。这个开着不需要是闭合的,因此路径不一定是连续的,所以你的确可以在一个单独的图层中绘制多个形状。你可以控制路径的strokeColorfillColor,以及其它的属性,例如lineWidthlineCap以及lineJoin;但你在图层级只能设置这些属性一次。如果你想用不同的颜色样式绘制多个形状,你不得不为每个形状使用一个单独的图层。

我们可以手动使用独立的直线和弧线来创建圆角矩形路径,但UIBezierPath的确有一些自动创建圆角矩形的简便方法。接下来的代码片断产生一个三圆角一直角的路径:

同样的,CATextLayer string属性并不是你想象中的一个NSString,但是id类型的。这允许你使用NSAttributedString而非NSString来指定文本(NSAttributedString并不是NSString的子类)。属性设置是iOS用于渲染风格文本的机制。它们指定运行风格(style runs),这是用来指定字符串的类型添加何种元数据例如字体、颜色、粗体、斜体等。

OpenGL提供Core Animation的加强。它是一个底层C的API接口来直接通过最小的抽象与iPhone和iPad上的图形硬件打交道。OpenGL并没有所谓的对象或图层的层次;它简单的处理三角形。在OpenGL中一切都是在3D空间中的三角形以及它们的颜色、纹理组成。这个方法非常灵活、有效,但使用OpenGL从头制作类似iOS用户界面的东西需要做很多事情。

这时候就可以使用CAScrollLayer了。CAScrollLayer有一个-scrollToPoint:方法用来自动调整bounds的源点,这样图层内容看起来就像是滚动。注意,这是它所做的全部。正如先前所说,Core Animation并不会处理用户输入,所以CAScrollLayer并不负责将触摸事件转换成滚动行为,它也不会渲染滚动条或者实现其它任何iOS的特定行为如滚动回弹(当一个视图滚动出界后回弹回正常位置)。

这个应用十分基础,但可以很容易地扩展成可支持额外参数的版本,例如贴图大小,或者导出非JPEG格式的图像。运行结果是产生64个新图像,名称如下:

但如何你只想显示一个大图层的一小部分怎么办?例如,你可能有一张大图像希望用户可以四处滚动浏览,或者一个长的数据或文本表。在典型的iOS应用中,你可能会使用UITableView或者UIScrollView,但当用独立的图层时有什么等同的东西吗?

图片 21图6.5 同一个透视但不同变形的立方体

让我们试一个例子:我们将在一个圆圈中发射不两只速度和透明度的粒子[1]来创建火焰爆炸效果。表6.13包含了产生爆炸的代码。你可以在图6.13看到效果。

图片 22图6.6 使用CAGradientLayer的双色对角渐变

图片 23图6.4 使用CATextLayer实现的富文本

// 创建路径参数let rect = CGRectMake(50, 50, 100, 100)let radii = CGSizeMakelet corners = UIRectCorner.TopRight | UIRectCorner.BottomRight | UIRectCorner.BottomLeft// 创建路径let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: radii)

图片 24图6.2 使用CATextLayer实现的纯文本

不同于UIScrollView,我们自定义的ScrollView并没有实现任何边界检查。很可能将图层内容移出视图边缘并可以无限拖动。CAScrollLayer并没有等同于UIScrollView contentSiez的属性,因此也没有总可滚动区域的概念,这些在你滚动CAScrollLayer时真正发生的是它调整它的bounds源点到你指定的值。它并不会调整bounds大小,因为它不需要这样做;内容可以无限制的超出边界。

很有必要说一下CATextLayer渲染的文本与UILabel渲染的文本在行距和字距上完全不同,这是由于它们分别使用了不同的绘制实现(分别是Core TextWebKit)。

这个复制效果看起来可能很酷炫,但它实际用处是什么呢?CAReplicatorLayer对于特殊效果十分有用,比如绘制游戏里面的子弹轨迹,或者粒子爆炸(尽管iOS5引入了CAEmitterLayer,这是更适合直接创建粒子效果的)。还有另一个更有用的用处:反射。

程序化地创建一只手臂需要相当多的代码,所以我们会用一些更简单的东西来代替:在第5章中的立方体例子中,我们使用旋转镜头而不是使用容器视图的sublayerTransform来解决图层平面化的问题。这是一个不错的方法,但只对单一物体生效。如果我们的场景有两个立方体,我们不能用这种方法单独地旋转每一个立方体。

幸运的是,这不是必须的。Core Animation提供了一个叫CATextLayerCALayer子类,它包含了UILabel中所有图层组成的字符串绘制特性,还增添了一些额外的特性。

有时你会发现你需要绘制一个相当大的图像。一个典型的例子可能是高像素镜头摄制的图片或地球表面的详尽地图。iOS应用通常运行于一个内存相当有限的设备上,所以将这样一个图像完整载入内存并不是一个好主意。加载大图也有可能非常慢,简便的方法(调用UIImage -imageName:-imageWithContentsOfFile:方法)是会使你的界面无响应一会儿,或者至少导致动画运行不畅。

每一个CAEmitterCell像是一个CALayer:它有contents属性可用CGImage设置,同样也有许多用来控制粒子样式和行为的配置属性。我们不会详细描述每个属性,但它们在CAEmitterCell头文件里都有详细说明。

CAGradientLayer也有startPointendPoint属性来定义渐变方向。它们用单元坐标指定,而非点,所以图层左上角是{0, 0}右下角是{1, 1}。图6.6展示最终的渐变结果。

通过这种方法来修正缩放也意味着我们的雪人图像在Retina设备上只会渲染成一半大小(变成总共10241024点而非先前的20482048)。图像的类型通常不会影响CATiledLayer的正常显示(除了图片和地图,它们是被设计来缩放显示在不同的比例的),它这点值得记住。

我们将用这个将我们的20482048雪人图像转换成64个独立的256256贴图。(256*256是CATiledLayer的默认大小,尽管这个可以用tileSize属性改变。)我们的应用需要接收输入图像文件的路径作为第一个命令行参数。我们可以在Xcode的构建模式中硬编码这个路径参数,但当我们将来想用一张不同的图像时并不会很有用。因此,我们会构建这个应用并机智的保存它,然后从终端中执行它,就像这样:

表6.3 用NSAttributedString实现富文本import UIKitimport CoreTextclass ViewController: UIViewController { @IBOutlet weak var labelView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 创建文字图层 let textLayer = CATextLayer() textLayer.frame = self.labelView.bounds textLayer.contentsScale = UIScreen.mainScreen().scale self.labelView.layer.addSublayer(textLayer) // 设置文字属性 textLayer.alignmentMode = kCAAlignmentJustified textLayer.wrapped = true // 选择字体 let font = UIFont.systemFontOfSize // 选择文字 let text: NSString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." // 创建属性字符串 let string = NSMutableAttributedString(string: text.description) // 将UIFont转为CTFont let fontName: CFStringRef = font.fontName let fontSize: CGFloat = font.pointSize let fontRef: CTFontRef = CTFontCreateWithName(fontName, fontSize, nil) // 设置文本属性 var attribs: Dictionary<NSObject, AnyObject> = [ kCTForegroundColorAttributeName: UIColor.blackColor().CGColor, kCTFontAttributeName: fontRef ] string.setAttributes(attribs, range: NSMakeRange(0, text.length)) attribs = [ kCTForegroundColorAttributeName: UIColor.redColor().CGColor, kCTUnderlineStyleAttributeName: NSNumber(int: CTUnderlineStyle.Single.rawValue), kCTFontAttributeName: fontRef ] string.setAttributes(attribs, range: NSMakeRange // 设置图层文本 textLayer.string = string }}

CATiledLayer提供一个解决方案,当加载大图时通过将它分割成多个小的贴片并按需要单纯加载它们来解决性能问题。让我们用一个例子测试一下。

有趣的是,tileSize是用像素而非点为单位的,所以增加contentsScale,我们自动平分了默认的贴图大小(它现在在屏幕上是128128个点而非256256)。因此,我们不需要手动更新贴图大小或都为Retina分辨率提供一套单独的贴图。我们只需要简单的修改贴图渲染代码来适应缩放的改变:

locations属性是一组浮点数(封装成NSNumber对象)。这组数用单元坐标定义了颜色数组里每一种颜色的位置,其中0.0代表渐变开始,1.0代表渐变结束。

正如我们在第1层“图层树”中讨论的,每个UIView后都有一个CALayer实例。这个图层是由视图自动创建并管理的,所以我们怎么用一个不同类型的图层来代替?一旦图层建立我们就不能替换它,但如果我们子类化UIView,我们可以重写+layerClass方法来在创建时返回一个不同的图层子类。UIView在初始化时调用+layerClass方法,然后用它返回的图层作为主图像。

  1. 文章中用到的Spark.png:

    图片 25Spark.png ↩

注意图层在重复是如何改变的:这是用instanceBlueOffsetinstanceGreenOffset属性实现的。通过每次重复时减少蓝和绿的色块,我们使图层偏移成红色。

我们将从一个简单的红蓝对角渐变例子开始。渐变颜色使用colors属性指定,这是一个数组。colors数组容纳CGColorRef类型的数据(这不是NSObject的派生),所以我们需要使用第2章看见的桥技术来使编译器顺利执行。

第2章提及的CAShapeLayer提供不同于使用CALayer cornerRadius属性外的一个可选的方法来创建带圆角的视图。尽管使用CAShapeLayer需要一些额外的工作,但它能够让我们独立指定每个角的圆角。

当你四处转动图像时,你会注意到当CATiledLayer加载贴图时,它们会渐入。这是CATiledLayer的默认表现。(你可能在iOS6之前的Apple地图应用上看见过这一效果。)你可以使用fadeDuration来改变渐变时间或禁用它。

让我们用NSAttributedString修改这个例子。在iOS6及之后的版本中,我们可以用新的NSTextAttributeName常量来设置我们的字符属性,但因为练习的点在于演示这一特性在iOS5及以下版本也同样适用,我们用Core Text来代替。这意味着你需要在项目中引入Core Text项目;否则,编译器不会识别属性常量。

本文由永利皇宫463登录发布于编程,转载请注明出处:特定图层

关键词:

上一篇:没有了

下一篇:特定图层