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表示的形状绘制出来。这个开着不需要是闭合的,因此路径不一定是连续的,所以你的确可以在一个单独的图层中绘制多个形状。你可以控制路径的strokeColorfillColor,以及其它的属性,例如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提供了一个叫CATextLayerCALayer子类,它包含了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。这允许你根据需求用CGFontRefCTFontRef(一个Core Text font)来指定字体。字体大小也可以单独用fontSize属性设置,因为CTFontRefCGFontRef并不像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 TextWebKit)。

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

UILabel的代替品

我们说过CATextLayerUILabel表现性能更好,以及一些额外的布局选项和对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也有startPointendPoint属性来定义渐变方向。它们用单元坐标指定,而非点,所以图层左上角是{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创建的图层环

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

这个复制效果看起来可能很酷炫,但它实际用处是什么呢?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(不同于大多UIKitCore 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的子类叫CAEmitterLayerCAEmitterLayer是一个被用来创建实时粒子动画如烟、火、雨等的高性能粒子引擎。

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

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

CAEAGLLayer

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

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

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

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

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

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

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

在我们开始前,你需要在项目中添加GLKitOpenGLES框架。接下来你应该实现表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应用中,我们可能想使用NSTimerCADisplayLink(见第11章“基于定时器的动画”)来每秒调用-drawFrame方法60次,我们会将绘制和几何产生分离来防止每帧重新产生三角形(因此我们也可以绘制其它非三角形的东西),但这应该足够演示这一原则了。


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


    Spark.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容