I.6 特定图层(上)
专业化是每一个复杂组织的特性。——Catharine R.Stimpson
到目前为止,我们使用过CALayer
类,我们发现它有一些有用的图像绘制和变形能力。但Core Animation
图层不仅是用来图像和颜色的。这一章将讲述其它你可以用来扩展Core Animation
绘制能力的图层类。
CAShapeLayer
在第4章“视觉特效”中你学会了如何用CGPath
来直接创建一个阴影形状,而不是使用图像。如果你可以用同样的方式创建图层形状将是极好的。
CAShapeLayer
是一个用矢量图而非位图绘制自身的图层子类。你指定如颜色和线条粗细等属性,使用CGPath
指定想要的形状,然后CAShapeLayer
会自动渲染它。当然,你也可以直接用Core Graphics
在一个普通CALayer
中直接向内容里绘制路径(在第2章“主图像”中讲解的),但用CAShapeLayer
有如下一些优点:
-
快——
CAShapeLayer
使用硬件加速绘制,比用Core Graphics
绘制图像快很多。 -
高效内存——
CAShapeLayer
并不需要像正常CALayer
那样创建主图像,所以无论它多大都不需要消耗过多内存。 -
不会被图层边界裁剪——
CAShapeLayer
可以自由地画在边界之外。它并不会像你使用Core Graphics
绘制在一个普通CALayer
中的路径一样被裁剪(正如你在第2章所见)。 -
不会像素化——当你用变形放大一个
CAShapeLayer
或者用3D透视变形将其拉近镜头,它并不会像普通图层主图像一样像素化。
创建CGPath
CAShapeLayer
可以用来将任何可以用CGPath
表示的形状绘制出来。这个开着不需要是闭合的,因此路径不一定是连续的,所以你的确可以在一个单独的图层中绘制多个形状。你可以控制路径的strokeColor
和fillColor
,以及其它的属性,例如lineWidth
(线的宽度,以点计)、lineCap
(线的两端的样子)以及lineJoin
(线的连接处的样子);但你在图层级只能设置这些属性一次。如果你想用不同的颜色样式绘制多个形状,你不得不为每个形状使用一个单独的图层。
表6.1展示一个简单的火柴人图像的绘制代码,它使用了一个CAShapeLayer
来渲染。CAShapeLayer path
属性被定义为CGPathRef
,但我们使用UIBezierPath
辅助类来创建路径,这避免了我们手动释放CGPath
的烦恼。图6.1展示了结果。它并不真的是一个Rembrandt,但你知道方法就好!
表6.1 使用CAShapeLayer绘制火柴人图像
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 创建路径
let path = UIBezierPath()
path.moveToPoint(CGPointMake(175, 100))
path.addArcWithCenter(CGPointMake(150, 100), radius: 25, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.moveToPoint(CGPointMake(150, 125))
path.addLineToPoint(CGPointMake(150, 175))
path.addLineToPoint(CGPointMake(125, 225))
path.moveToPoint(CGPointMake(150, 175))
path.addLineToPoint(CGPointMake(175, 225))
path.moveToPoint(CGPointMake(100, 150))
path.addLineToPoint(CGPointMake(200, 150))
// 创建形状图层
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)
}
}
图6.1 一个使用`CAShapeLayer`绘制的简单火柴人
终级版圆角
第2章提及的CAShapeLayer
提供不同于使用CALayer cornerRadius
属性外的一个可选的方法来创建带圆角的视图。尽管使用CAShapeLayer
需要一些额外的工作,但它能够让我们独立指定每个角的圆角。
我们可以手动使用独立的直线和弧线来创建圆角矩形路径,但UIBezierPath
的确有一些自动创建圆角矩形的简便方法。接下来的代码片断产生一个三圆角一直角的路径:
// 创建路径参数
let rect = CGRectMake(50, 50, 100, 100)
let radii = CGSizeMake(20, 20)
let corners = UIRectCorner.TopRight | UIRectCorner.BottomRight | UIRectCorner.BottomLeft
// 创建路径
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: radii)
我们可以用有这一路径的CAShapeLayer
来创建一个混有圆角和直角的视图。如果我们想要将视图内容裁剪成这个形状,我们可以使用我们的CAShapeLayer
作为视图主图像的mask
属性而不是添加为子图层。(见第4章“视觉特效”中对图层遮罩的完整解释。)
CATextLayer
用户界面不能仅用图像构建。一个设计优秀的图标可以很好地传达按钮或控件的意图,但或早或晚你需要一个好的旧式文字标签。
如果你想在图层内显示文字,你当然可以用Core Graphics
使用图层委托直接向图层内容内绘制文字(这是UILabel
工作原理的本质)。如果你直接操作图层是十分笨拙的方法,尽管不是使用基于图层的视图。你将需要创建一个类来作为每个图层显示文字的图层委托,然后写下决定哪个图层要显示字符串的逻辑代码,更不用说跟踪不同的字体、颜色等。
幸运的是,这不是必须的。Core Animation
提供了一个叫CATextLayer
的CALayer
子类,它包含了UILabel
中所有图层组成的字符串绘制特性,还增添了一些额外的特性。
CATextLayer
也比UILabel
渲染快。它在iOS6及之前是不为人知的事实,UIlabel
实际上使用WebKit
来进行文本绘制,这在你绘制大量文本时带来显著的性能负担。CATextLayer
使用Core Text
而且显著的快速。
让我们尝试用CATextLayer
显示一些文字。表6.2展示了设置和显示CATextLayer
的代码,图6.2展示了结果。
表6.2 用CATextLayer实现文字标签
import UIKit
class 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(15)
// 设置图层字体
let fontName: CFStringRef = font.fontName
let fontRef: CGFontRef = CGFontCreateWithFontName(fontName)
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
}
}
图6.2 使用CATextLayer实现的纯文本
译者实现的效果有点问题,还没有查明原因:
文字发生重叠
如果你仔细看文字,你们发现有一点奇怪;文字是像素化的。这是因为它没有在Retina分辨率下渲染。第2章提到的contentsScale
属性,这是用来决定图层渲染的分辨率。contentsScale
属性默认为1.0而不是屏幕缩放因子。如果你想要Retina级别的文本,我们得用下列代码给CATextLayer
设置contentsScale
来匹配屏幕缩放。
textLayer.contentsScale = UIScreen.mainScreen().scale
这样就解决了像素化问题(如图6.3)。
图6.3 设置contentsScale来适配屏幕的效果CATextLayer font
属性并不是UIFont
,它是CGTypeRef
。这允许你根据需求用CGFontRef
或CTFontRef
(一个Core Text font
)来指定字体。字体大小也可以单独用fontSize
属性设置,因为CTFontRef
和CGFontRef
并不像UIFont
一样有点大小。这个例子展示了如何将UIFont
转换成CGFontRef
。
同样的,CATextLayer string
属性并不是你想象中的一个NSString
,但是id
类型的。这允许你使用NSAttributedString
而非NSString
来指定文本(NSAttributedString
并不是NSString
的子类)。属性设置是iOS用于渲染风格文本的机制。它们指定运行风格(style runs),这是用来指定字符串的类型添加何种元数据例如字体、颜色、粗体、斜体等。
富文本
在iOS6中,Apple增加了对UILabel
和其它UIKit
文本视图的属性字符串的直接支持。这是一个便于使用属性文本的有用恶性,但CATextLayer
从iOS3.2引入以来都支持属性文本;所以如果你仍需要在你的应用中支持早期的iOS版本,CATextLayer
是一个给你的用户界面添加富文本标签的简单方法,你无需处理复杂的Core Text
或者陷入使用UIWebView
的麻烦中。
让我们用NSAttributedString
修改这个例子(如表6.3)。在iOS6及之后的版本中,我们可以用新的NSTextAttributeName
常量来设置我们的字符属性,但因为练习的点在于演示这一特性在iOS5及以下版本也同样适用,我们用Core Text
来代替。这意味着你需要在项目中引入Core Text
项目;否则,编译器不会识别属性常量。
图6.4展示了结果。(注意红色的,有下划线的文字。)
表6.3 用NSAttributedString实现富文本
import UIKit
import CoreText
class 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(15)
// 选择文字
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(6, 5))
// 设置图层文本
textLayer.string = string
}
}
图6.4 使用CATextLayer实现的富文本
译者实现效果如下:
还是有文字重叠
行距和字距
很有必要说一下CATextLayer
渲染的文本与UILabel
渲染的文本在行距和字距上完全不同,这是由于它们分别使用了不同的绘制实现(分别是Core Text
和WebKit
)。
差异大小是不同的(取决于使用的特定字体和字符),但一般是相当小的,但当你想用正常标签和CATextLayer
实现完全相同的样子的时候,你需要记得这一点。
UILabel的代替品
我们说过CATextLayer
比UILabel
表现性能更好,以及一些额外的布局选项和对iOS5中富文本的支持。但相对于正常标签,它使用起来十分不便。如果我们想用一个UILabel
的好用的替代品,我们应该在Interface Builder中创建我们的标签,它们也应该尽可能表现的像正常的视图。
我们可以继承UILabel
然后添加CATextLayer
作为子图层并重写其显示文本的方法,但我们仍有由UILabel -drawRect:
方法创建的空的主图像。因为CALayer
不支持自动尺寸和自动布局,子图层并不会自动跟踪视图的bounds
大小,所以每当视图大小改变时我们需要手动更新子图层的边界。
我们真正想要的是一个用CATextLayer
作为主图层的UILabel
子类,然后会自动调整视图的大小,因此不用担心主图像被裁剪。
正如我们在第1层“图层树”中讨论的,每个UIView
后都有一个CALayer
实例。这个图层是由视图自动创建并管理的,所以我们怎么用一个不同类型的图层来代替?一旦图层建立我们就不能替换它,但如果我们子类化UIView
,我们可以重写+layerClass
方法来在创建时返回一个不同的图层子类。UIView
在初始化时调用+layerClass
方法,然后用它返回的图层作为主图像。
表6.4展示的代码是创建UILabel
子类LayerLabel
用于使用CATextLayer
而不是像正常UILabel
样用慢速的-drawRect:
方法来绘制文本的。LayerLabel
既可以用程序实例化,也可以在Inteface Builder通过添加普通的标签视图然后将其类设置为LayerLabel
来实例化。
表6.4 使用CATextLayer的UILabel的子类——LayerLabel
import UIKit
class 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(fontName)
self.textLayer.font = fontRef
self.textLayer.fontSize = font.pointSize
}
}
如果你运行案例代码,你会注意到即使我们没有设置contentsScale
文本也不会像素化。另一个使用CATextLayer
作为主图层的好处是,contentsScale
会由视图自动设置。
这在个简单的例子中,我们只是实现了UILabel
一些样式和布局属性,但只需要一点额外的工作,我们就可以创建一个LayerLabel
类来支持完整的UILabel
(你会发现网上的开源项目中早有了这样的类)。
如果你只是要支持iOS6及以上版本,基于CATextLayer
的标签可能用处不大,但通常来说,使用+layerClasss
来创建基于不同图层类型的视图是在你应用的利用CALayer
子类的干净可重复利用的好方法。
CATransformLayer
当构建复杂的3D物体时,如果可以控制独立的元素层次将是十分方便的。例如,假设你在制作一只手臂:你可能想手是手腕的孩子,手腕是前臂的孩子,前臂是手肘的孩子,手肘是上臂的孩子,上臂是肩膀的孩子等。
这样就可以让你独立移动每一部分。转动肘可以移动小臂和手但不会移动肩膀。Core Animation
图层便于在2D中实现这种层次结构,但不能在3D中实现。这是因为每个图层会将其孩子平面化成一个单独的平面(如第5章“变形”中讲解的一样)。
CATransformLayer
解决了这一问题。CATransformLayer
不同于一般的CALayer
,它不能显示自身的内容;它的存在只是为了管理一个可以应用于其子图层的变形。CATransformLayer
并不会平面化其子图层,所以可以用来构造3D结构的层次,就像我们的手臂的例子一样。
程序化地创建一只手臂需要相当多的代码,所以我们会用一些更简单的东西来代替:在第5章中的立方体例子中,我们使用旋转镜头而不是使用容器视图的sublayerTransform
来解决图层平面化的问题。这是一个不错的方法,但只对单一物体生效。如果我们的场景有两个立方体,我们不能用这种方法单独地旋转每一个立方体。
所以让我们尝试用CATransformLayer
来代替。第一个要解决的问题是我们在第5章用的视图而不是独立的图层的构建立方体的。我们不能把一个视图后的图层放在另一个不是本身视图的图层里而不弄乱视图层次。我们可以创建UIVIew
的一个基于CATransformLayer
的子类(使用+layerClass
方法),但为了在我们的例子中简化一切,让我们重新用独立的图层来创建立方体。这意味着我们不能像第5章一样在其上显示按钮和标签,但我们现在并不需要这样做。
表6.5包含了相应的代码。我们用和第5章一样的基本逻辑来放置每一个立方体表面。但不同于我们之前直接向容器视图中添加立方体表面,我们将它们放入一个CATransformLayer
中来创建一个独立的立方体,然后将这两个立方体放入我们的容器中。我们给这立方体表面随机涂色来方便我们不通过标签和光影得以区分它们。图6.5显示了结果。
表6.5 使用CATransformLayer组织3D图层层次
import UIKit
import CoreText
class 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(rand()) / CGFloat(INT_MAX)
let green: CGFloat = CGFloat(rand()) / CGFloat(INT_MAX)
let blue: CGFloat = CGFloat(rand()) / CGFloat(INT_MAX)
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(0, 0, 50)
cube.addSublayer(self.faceWithTransform(ct))
// 添加面2
ct = CATransform3DMakeTranslation(50, 0, 0)
ct = CATransform3DRotate(ct, CGFloat(M_PI_2), 0, 1, 0)
cube.addSublayer(self.faceWithTransform(ct))
// 添加面3
ct = CATransform3DMakeTranslation(0, -50, 0)
ct = CATransform3DRotate(ct, CGFloat(M_PI_2), 1, 0, 0)
cube.addSublayer(self.faceWithTransform(ct))
// 添加面4
ct = CATransform3DMakeTranslation(0, 50, 0)
ct = CATransform3DRotate(ct, CGFloat(-M_PI_2), 1, 0, 0)
cube.addSublayer(self.faceWithTransform(ct))
// 添加面5
ct = CATransform3DMakeTranslation(-50, 0, 0)
ct = CATransform3DRotate(ct, CGFloat(-M_PI_2), 0, 1, 0)
cube.addSublayer(self.faceWithTransform(ct))
// 添加面6
ct = CATransform3DMakeTranslation(0, 0, -50)
ct = CATransform3DRotate(ct, CGFloat(M_PI), 0, 1, 0)
cube.addSublayer(self.faceWithTransform(ct))
// 在容器中居中立方体图层
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(c1t)
self.containerView.layer.addSublayer(cube1)
// 设置立方体2的变形并添加它
var c2t = CATransform3DIdentity
c2t = CATransform3DTranslate(c2t, 100, 0, 0)
c2t = CATransform3DRotate(c2t, CGFloat(-M_PI_4), 1, 0, 0)
c2t = CATransform3DRotate(c2t, CGFloat(-M_PI_4), 0, 1, 0)
let cube2 = self.cubeWithTransform(c2t)
self.containerView.layer.addSublayer(cube2)
}
}
}
图6.5 同一个透视但不同变形的立方体
CAGradientLayer
CAGradientLayer
用来产生两种或多种颜色之间的平滑渐变。可以通过Core Graphics
来将等同于CAGradientLayer
的效果于普通的图层主图像中,但使用CAGradientLayer
的优点在于绘制是硬件加速的。
基本渐变
我们将从一个简单的红蓝对角渐变例子开始(如表6.6)。渐变颜色使用colors
属性指定,这是一个数组。colors
数组容纳CGColorRef
类型的数据(这不是NSObject
的派生),所以我们需要使用第2章看见的桥技术来使编译器顺利执行。
CAGradientLayer
也有startPoint
和endPoint
属性来定义渐变方向。它们用单元坐标指定,而非点,所以图层左上角是{0, 0}
右下角是{1, 1}
。图6.6展示最终的渐变结果。
表6.6 一个简单的双色对象渐变
import UIKit
class 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(0, 0)
gradientLayer.endPoint = CGPointMake(1, 1)
}
}
}
图6.6 使用CAGradientLayer的双色对角渐变
多级渐变
colors
数组可以容纳任意多的你想要的颜色,所以很容易创建如同彩虹的多级渐变。默认情况下,渐变中的颜色会平分,但我们可以使用locations
属性来调整间距。
locations
属性是一组浮点数(封装成NSNumber
对象)。这组数用单元坐标定义了颜色数组里每一种颜色的位置,其中0.0代表渐变开始,1.0代表渐变结束。
并没有强制要求提供locations
数组,但如果你这么做了,你必须保证位置数和颜色数一致,否则你将得到一个空白的渐变。
表6.7展示了表6.6中的对角渐变的修改版。我们现在有一个红黄绿三色渐变。locations
数组被指定为0.0、0.25和0.5,这会使渐变挤在视图左上角(如图6.7)。
表6.7 使用locations数组来偏移渐变
import UIKit
class 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(0, 0)
gradientLayer.endPoint = CGPointMake(1, 1)
}
}
}
图6.7 使用locations数组偏移至左上角的三色渐变
CAReplicatorLayer
CAReplicatorLayer
类用来高效产生相似图层的集合的。它通过绘制一个或多个子图层的复制,并给每一个复制品应用不同的变形。演示起来可以比说的清楚,所以让我们来构建一个例子。
重复图层
在表6.8中,我们在屏幕中央创建一个小的白色方形图层,然后用CAReplicatorLayer
将其转 为十个图层的环。instanceCount
属性指定图层应该被重复多少次。instanceTransform
应用一个CATransform3D
(在这里,是位移并旋转使图层到达圆中的下一点)。
变形是逐渐增加的,每一个实例位置是相对于前一个实例的。这就是为什么复制品不都结束于同一个位置。图6.8展示了最终结果。
表6.8 使用CAReplicatorLayer重复图层
import UIKit
class 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(layer)
}
}
}
图6.8 用CAReplicatorLayer创建的图层环
注意图层在重复是如何改变的:这是用instanceBlueOffset
和instanceGreenOffset
属性实现的。通过每次重复时减少蓝和绿的色块,我们使图层偏移成红色。
这个复制效果看起来可能很酷炫,但它实际用处是什么呢?CAReplicatorLayer
对于特殊效果十分有用,比如绘制游戏里面的子弹轨迹,或者粒子爆炸(尽管iOS5引入了CAEmitterLayer
,这是更适合直接创建粒子效果的)。还有另一个更有用的用处:反射。
Reflections
通过使用CAReplicatorLayer
给单独一个复制图层应用一个负缩放因子的变形,你可以创建一个给定视图(或一个完整视图层次)的内容镜像,创建一个实时的“镜像”效果。
让我们尝试用一个可复用的UIView
子类ReflectionView
来实现这一想法,它会自动产生内容的镜像。创建这个的代码是很简单的(如表6.9),实际上使用ReflectionView
更简单;我们可以简单地向Interface Builder中拖入一个ReflectionView
实例(如图6.9),它将会在运行时产生子视图的镜像而不需要向视图控制器中添加其它启动代码(如图6.10)。
表6.9 用CAReplicatorLayer自动绘制镜像
import UIKit
import QuartzCore
class 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()
}
}
图6.9 在Interface Builder中使用ReflectionView 图6.10 ReflectionView自动在运行时创造镜像译者并没有实现相应效果,因此贴上原著图。
可以在ReflectionView找到一个更完整的ReflectionView
类的开源实现,它有一个可调的渐隐效果(用CAGradientLayer
和图层遮罩实现)。
CAScrollLayer
对于一个未变形的图层,图层的bounds
大小会匹配它的frame
大小。frame
是自动从boudns
中计算的,所以改变任何属性都会更新。
但如何你只想显示一个大图层的一小部分怎么办?例如,你可能有一张大图像希望用户可以四处滚动浏览,或者一个长的数据或文本表。在典型的iOS应用中,你可能会使用UITableView
或者UIScrollView
,但当用独立的图层时有什么等同的东西吗?
在第2章,我们讲解了图层的contentsRect
属性的使用,这是在一个图层中显示一张大图像的一小部分的好的解决方法。但当你的图层有子图层时,这不是一个好方法,因为每当你想滚动可视化区域时,你需要手动重新计算并更新子图层的位置。
这时候就可以使用CAScrollLayer
了。CAScrollLayer
有一个-scrollToPoint:
方法用来自动调整bounds
的源点,这样图层内容看起来就像是滚动。注意,这是它所做的全部。正如先前所说,Core Animation
并不会处理用户输入,所以CAScrollLayer
并不负责将触摸事件转换成滚动行为,它也不会渲染滚动条或者实现其它任何iOS的特定行为如滚动回弹(当一个视图滚动出界后回弹回正常位置)。
让我们用CAScrollLayer
来创建一个非常基础的UIScrollView
替代品。我们将创建一个以CAScrollLayer
作为主图层的自定义UIView
,然后使用UIPanGestureRecognizer
来实现触摸处理。代码展示在表6.10。图6.11展示ScrollView
被用来在一个大于本身帧大小的UIImageView
中四处拖动。
表6.10 使用CAScrollLayer实现滚动视图
import UIKit
class 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(self).x
offset.y -= recognizer.translationInView(self).y
// 滚动图层
let layer = self.layer as! CAScrollLayer
layer.scrollToPoint(offset)
// 重置pan手势位移
recognizer.setTranslation(CGPointZero, inView: self)
}
}
图6.11 使用CAScrollLayer来创建凑合的滚动视图
不同于UIScrollView
,我们自定义的ScrollView
并没有实现任何边界检查。很可能将图层内容移出视图边缘并可以无限拖动。CAScrollLayer
并没有等同于UIScrollView contentSiez
的属性,因此也没有总可滚动区域的概念,这些在你滚动CAScrollLayer
时真正发生的是它调整它的bounds
源点到你指定的值。它并不会调整bounds
大小,因为它不需要这样做;内容可以无限制的超出边界。
精明的你可能想知道到底什么时候需要使用CAScrollLayer
,既然你可以简单的使用CALayer
并且调整它的boudns
源点。事实上并没有什么时候需要使用它。UIScrollView
并不使用UIScrollLayer
,它只是简单的通过直接操作图层bounds
来实现的。
CAScrollLayer
并没有潜在的好用的特性,尽管如此,如果你看CAScrollLayer
的头文件,你会注意到它引入了一些分类来扩展CALayer
,这包含一些额外的方法和属性:
- (void) scrollPoint: (CGPoint) p;
- (void) scrollRectToVisible: (CGRect) r;
@property(readonly) CGRect visibleRect;
你可能从它们的名字中猜测这些方法给每个CALayer
实例添加滚动方法,但事实上它们只是CAScrollLayer
中的图层的实用方法。scrollPoint:
方法向上搜索图层树来寻找第一个可用的CAScrollLayer
,然后滚动它使的指定点可见。scrollRectToVisible:
方法对矩形做同样的事情。visibleRect
属性决定CAScrollLayer
现在可见的图层部分(如果有的话)。
自己实现相应方法是十分粗暴的,但CAScrollLayer
避免了你这个麻烦,几乎是在你图层滚动时会调整它的存在。
CATiledLayer
有时你会发现你需要绘制一个相当大的图像。一个典型的例子可能是高像素镜头摄制的图片或地球表面的详尽地图。iOS应用通常运行于一个内存相当有限的设备上,所以将这样一个图像完整载入内存并不是一个好主意。加载大图也有可能非常慢,简便的方法(调用UIImage -imageName:
或-imageWithContentsOfFile:
方法)是会使你的界面无响应一会儿,或者至少导致动画运行不畅。
在iOS上可高效绘制的图像尺寸是有上限的。所以在屏幕上显示的图像最终会转换为OpenGL纹理,OpenGL有个最大的纹理,OpenGL有一个最大的纹理大小(通常是20482048或40964096,这取决于设备模型)。如果你尝试展示一张大于最大纹理大小的图像,即使这张图像已经存在RAM中,你仍会看到一些糟糕的表现,因为Core Aniamtion
被迫使用CPU而不是更快的GPU来加载图像。(看第12章“速度微调”和第13章“高效绘图”来获得关于软件绘制和硬件绘制的区别。)
CATiledLayer
提供一个解决方案,当加载大图时通过将它分割成多个小的贴片并按需要单纯加载它们来解决性能问题。让我们用一个例子测试一下。
贴图切割
我们将成一个相当大的图开始——在这个例子中,一个2048*2048的雪人。为了从CATiledLayer
中受益,我们需要将它切成几个小图像。你可以程序化的做,但如果你想加载整张图像然后在运行时切割它,你会失去大多CATiledLayer
设计提供的加载性能优势。理想状态下,你想用预处理来代替这一操作。
表6.11展示了一个简单的Mac OS命令行应用的代码,它会将图像切成贴图并将它们存储为独立文件供CATiledLayer
使用。
表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;
}
我们将用这个将我们的20482048雪人图像转换成64个独立的256256贴图。(256*256是CATiledLayer
的默认大小,尽管这个可以用tileSize
属性改变。)我们的应用需要接收输入图像文件的路径作为第一个命令行参数。我们可以在Xcode的构建模式中硬编码这个路径参数,但当我们将来想用一张不同的图像时并不会很有用。因此,我们会构建这个应用并机智的保存它,然后从终端中执行它,就像这样:
> path/to/TileCutterApp path/to/Snowman.jpg
这个应用十分基础,但可以很容易地扩展成可支持额外参数的版本,例如贴图大小,或者导出非JPEG格式的图像。运行结果是产生64个新图像,名称如下:
Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg
既然我们有了这些贴图图像,我们需要创建一个iOS应用来使用它们。CATiledLayer
完美兼容UIScrollView
,所以为了实现这一例子的目的,我们会将CATiledLayer
放入UIScrollView
中。除了设置图层和滚动视图边界来适应我们的总图像大小外,我们所有需要做的只剩下实现-drawLayer:inContext:
方法,这将在CATiledLayer
需要加载一张新的贴图时调用。
表6.12展示了代码,图6.12展示了结果。
表6.12 一个简单的滚动CATiledLayer的实现
import UIKit
class 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(ctx)
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(ctx)
tileImage?.drawInRect(bounds)
UIGraphicsPopContext()
}
}
使用UIScrollView滚动CATiledLayer
当你四处转动图像时,你会注意到当CATiledLayer
加载贴图时,它们会渐入。这是CATiledLayer
的默认表现。(你可能在iOS6之前的Apple地图应用上看见过这一效果。)你可以使用fadeDuration
来改变渐变时间或禁用它。
CATiledLayer
(不同于大多UIKit
和Core Animation
方法)支持多线程绘制。-drawLayer: inContext:
方法可能同时被多线程调用,所以确保你实现的所有绘制代码是线程安全的。
Retina贴图
你可能也注意到了贴图并不能在Retina分辨率下显示。为了在设备的相应分辨率下渲染CATiledLayer
,我们需要设置图层的contentsScale
来适配UIScreen
缩放,如:
tileLayer.contentsScale = UIScreen.mainScreen().scale
有趣的是,tileSize
是用像素而非点为单位的,所以增加contentsScale
,我们自动平分了默认的贴图大小(它现在在屏幕上是128128个点而非256256)。因此,我们不需要手动更新贴图大小或都为Retina分辨率提供一套单独的贴图。我们只需要简单的修改贴图渲染代码来适应缩放的改变:
// 确定贴图坐标
let bounds = CGContextGetClipBoundingBox(ctx)
let scale = UIScreen.mainScreen().scale
let x = floor(bounds.origin.x / layer.tileSize.width * scale)
let y = floor(bounds.origin.y / layer.tileSize.height * scale)
通过这种方法来修正缩放也意味着我们的雪人图像在Retina设备上只会渲染成一半大小(变成总共10241024点而非先前的20482048)。图像的类型通常不会影响CATiledLayer
的正常显示(除了图片和地图,它们是被设计来缩放显示在不同的比例的),它这点值得记住。
CAEmitterLayer
在iOS5中,Apple引入了一个新的CALayer
的子类叫CAEmitterLayer
。CAEmitterLayer
是一个被用来创建实时粒子动画如烟、火、雨等的高性能粒子引擎。
CAEmitterLayer
表现的像是一系列CAEmitterCell
实例的容器,它们定义了粒子效果。你可以用模板似的创建一个或多个不同的粒子类型,CAEmitterLayer
负责用这些模板实例化粒子流。
每一个CAEmitterCell
像是一个CALayer
:它有contents
属性可用CGImage
设置,同样也有许多用来控制粒子样式和行为的配置属性。我们不会详细描述每个属性,但它们在CAEmitterCell
头文件里都有详细说明。
让我们试一个例子:我们将在一个圆圈中发射不两只速度和透明度的粒子[1]来创建火焰爆炸效果。表6.13包含了产生爆炸的代码。你可以在图6.13看到效果。
表6.13 用CAEmitterLayer创建爆炸效果
import UIKit
class 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)
// 配置发射器
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(M_PI) * 2.0
// 向发射器添加粒子模板
emitter.emitterCells = NSArray(array: [cell]) as [AnyObject]
}
}
}
火焰爆炸效果
CAEmitterCell
的属性通常有三类:
-
粒子的某一特殊属性的起始值。例如,
color
属性指定了会与contents
中图像颜色相混合的颜色。在我们的例子中,我们将它设为橘色。 -
粒子间的值区间。例如,我们项目中的
emitssionRange
属性被设为2π,表示粒子可以360度无死角发射。通过设置一个较小值,我们会创建一个粒子的圆锥漏洞。 -
特定值的改变时间。例如,在爆炸项目中我们设置
alphaSpeed
为-0.4,这意味着粒子的alpha
值每秒减少0.4,这会产生了粒子离开发射器的渐出效果。
CAEmitterLayer
属性本身控制了整个粒子系统的位置和基本形状。一些属性如birthRate
、lifetime
和velocity
复制值定义于CAEmitterCell
上。它们像是乘数,因此你可以用一个值加速或增强整个粒子系统。其它值得关注的属性有:
-
preservesDepth
,它控制是否一个3D粒子系统会扁平化进一个图层(默认)或是它容器图层的其它图层在3D空间中混合。 -
renderMode
,它控制了粒子图像如何视觉化地混合。你可能注意到我们的例子将它设置成kCAEmitterLayerAdditive
,其效果是混合粒子的光影。如果我们将其设为默认值kCAEmitterLayerUnordered
,结果会变得不是很美观(如图6.14)。
图6.14 禁用增加混合的火焰粒子
CAEAGLLayer
当提及iOS上的高性能图像时,最后要说的是OpenGL。它也可能是最后的办法,至少对非游戏应用是这样的,因为相比Core Animation
和UIKit
框架,它不是一般的复杂。
OpenGL提供Core Animation
的加强。它是一个底层C的API接口来直接通过最小的抽象与iPhone和iPad上的图形硬件打交道。OpenGL并没有所谓的对象或图层的层次;它简单的处理三角形。在OpenGL中一切都是在3D空间中的三角形以及它们的颜色、纹理组成。这个方法非常灵活、有效,但使用OpenGL从头制作类似iOS用户界面的东西需要做很多事情。
为了高效使用Core Animaiton
,你需要决定内你要绘制的内容类型(矢量图形、位图、粒子、文字等)然后选择一个适当的图层类型来展示内容。只有一部分内容类型在Core Animaiton
中有优化;所以如果你要绘制的东西不能很好的匹配任何标准的图层类型,你将很难得到高性能的表现。
因为OpenGL并不清楚你的内容,它可以是极快的。通过OpenGL,你可以绘制任何你想画的,只要你提供如何去画必须的几何和阴影逻辑。这使它成为游戏的流行选择(这时,Core Animation
的有限的优化过的内容类型并不能总是符合需求),但对于一个通常的界面来说就是大材小用了。
在iOS 5中,Apple引入一个新的框架叫做GLKit
,它通过提供一个叫GLKView
的UIView
子类来移除了一个设置OpenGL绘图上下文的复杂性,它帮你处理了大多数启动和绘制。在之前,你自己必须使用CAEAGLLayer
来做不同OpenGL绘制缓存的所有底层配置,这是一个CALayer
子类设计来直接显示OpenGL图像。
你将很少需要手动设置一个CAEAGLLayer
(而是用GLKView
),但让我们花点时间了解一下旧时的目的。特别的,我们将设置一个OpenGL ES 2.0上下文,这是所有现代iOS设备的标准。
尽快可以不用GLKit
完成这一切,但会花费许多额外工作来设置顶点和碎片阴影,这是用一门叫GLSL的类C语言写的自包含程序,将会在运行时载入图形硬件。编写GLSL代码与设置EAGLLayer
并不直接想着,所以我们会用GLKBaseEffect
来抽象阴影逻辑。此外,我们将用旧方法来做这一切。
在我们开始前,你需要在项目中添加GLKit
和OpenGLES
框架。接下来你应该实现表6.14的代码,它使用OpenGL ES 2.0绘图上下文做了几乎最少的GAEAGLLayer
设置,然后渲染了一个彩色三角形(如图6.15)。
表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
- (void)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));
}
}
- (void)tearDownBuffers {
if (_framebuffer) {
// 删除帧缓冲
glDeleteFramebuffers(1, &_framebuffer);
_framebuffer = 0;
}
if (_colorRenderbuffer) {
// 删除颜色渲染缓冲
glDeleteRenderbuffers(1, &_colorRenderbuffer);
_colorRenderbuffer = 0;
}
}
- (void)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];
}
- (void)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];
}
}
- (void)dealloc {
[self tearDownBuffers];
[EAGLContext setCurrentContext:nil];
}
@end
图6.15 用OpenGL在CAEAGLLayer中渲染的简单三角形
在一个真实OpenGL应用中,我们可能想使用NSTimer
或CADisplayLink
(见第11章“基于定时器的动画”)来每秒调用-drawFrame
方法60次,我们会将绘制和几何产生分离来防止每帧重新产生三角形(因此我们也可以绘制其它非三角形的东西),但这应该足够演示这一原则了。
-
文章中用到的Spark.png:
Spark.png ↩