Swift语言iOS开发:CALayer十则示例
作者:Scott Gardner 译者:TurtleFromMars 如你所知,我们在iOS应用中看到的都是视图(view),包括按钮视图、表视图、滑动条视图,还有可以容纳其他视图的父视图等。 但你或许不知道在iOS中支撑起每个视图的是一个叫做"图层(layer)"的类,确切地说是CALayer。 本文中您会了解CALayer及其工作原理,还有应用CALayer打造酷炫效果的十则示例,比如绘制矢量图形、渐变色,甚至是粒子系统。 本文要求读者熟悉iOS应用开发和Swift语言的基础知识,包括利用Storyboard构建用户界面。 注:如果您尚未掌握这些基础,不必担心,我们有不少相关教程,例如使用Swift语言编写iOS应用和iOS学徒。 准备开始 要理解图层是什么,最简便的方式就是"实地考察"。我们这就创建一个简单的项目,从头开始玩转图层。 准备好写代码了吗?好!启动Xcode,然后: 1.选择FileNewProject菜单项。 2.在对话框中选择iOSApplicationSingle View Application。 3.点击Next,Product Name填写CALayerPlayground,然后输入你自己的Organization Name和Identifier。 4.Language选Swift,Devices选Universal。 5.取消选择Core Data,点击Next。 6.把项目保存到合适的位置(个人习惯把项目放在用户目录下建立的Source文件夹),点击Create。 好,文件准备就绪,接下来就是创建视图了: 7.在项目导航栏(Project navigator)中选择Main.storyboard。 8.选择ViewAssistant EditorShow Assistant Editor菜单项,如果没有显示对象库(Object Library),请选择ViewUtilitiesShow Object Library。 9.然后选择EditorCanvasShow Bounds Rectangles,这样在向场景添加视图时就可以看到轮廓了。 10.把一个视图(View)从对象库拖入视图控制器场景,保持选中状态,在尺寸检查器(ViewUtilitiesShow Size Inspector)中将x和y设为150,Width和Height设为300。 11.视图保持选中,点击自动布局工具栏(Storyboard右下角)的Align按钮,选中Horizontal Center in Container和Vertical Center in Container,数值均为0,然后点击Add 2 Constraints。 12.点击Pin按钮,选中Width和Height,数值均设为300,点击Add 2 Constraints。 最后按住control从刚刚创建的视图拖到ViewController.swift文件中viewDidLoad()方法的上方,在弹框中将outlet命名为viewForLayer,如图:
点击Connect创建outlet。 将ViewController.swift中的代码改写为: importUIKit classViewController:UIViewController{ @IBOutletweakvarviewForLayer:UIView! varl:CALayer{ returnviewForLayer.layer } overridefuncviewDidLoad(){ super.viewDidLoad() setUpLayer() } funcsetUpLayer(){ l.backgroundColor=UIColor.blueColor().CGColor l.borderWidth=100.0 l.borderColor=UIColor.redColor().CGColor l.shadowOpacity=0.7 l.shadowRadius=10.0 } } 之前提到iOS中的每个视图都拥有一个关联的图层,你可以通过yourView.layer访问图层。这段代码首先创建了一个叫"l"(小写L)的计算属性,方便访问viewForLayer的图层,可让你少写一些代码。 这段代码还调用了setUpLayer方法设置图层属性:阴影,蓝色背景,红色粗边框。你马上就可以了解这些东西,不过现在还是先构建App,在iOS模拟器中运行(我选了iPhone 6),看看自定义的图层如何。 几行代码,效果还不错吧?还是那句话,每个视图都由图层支撑,所以你也可以对App中的任何视图做出类似修改。我们继续深入。 CALayer基本属性 CALayer有几个属性可以用来自定外观,想想刚才做的:
以上只是CALayer中可以设置的部分属性。我们再试两个,在setUpLayer()中追加以下代码: l.contents=UIImage(named:"star")?.CGImage l.contentsGravity=kCAGravityCenter CALayer的contents属性可以把图层的内容设为图片,这里我们要设置一张"星星"的图片,为此你需要把图片添加到项目中,请下载图片并添加到项目中。 构建,运行,欣赏一下效果: 注意星星居中,这是因为contentsGravity属性被设为kCAGravityCenter,如你所想,重心也可以设为上、右上、右、右下、下、左下、左、左上。 更改图层外观 仅供娱乐,我们来添加几个手势识别器来控制图层外观。在Xcode中,向viewForLayer对象上拖一个轻触手势识别器(tap gesture recognizer),见下图: 注:如果你对手势识别器比较陌生,请参阅Using UIGestureRecognizer with Swift。 以此类推,再添加一个捏合手势识别器(pinch gesture recognizer)。 然后按住control依次将两个手势识别器从Storyboard场景停靠栏拖入ViewController.swift,放在setUpLayer()和类自身的闭合花括号之间。 在弹框中修改连接为Action,命名轻触识别操作为tapGestureRecognized,捏合识别操作为pinchGestureRecognized,例如: 如下改写tapGestureRecognized(_:): @IBActionfunctapGestureRecognized(sender:UITapGestureRecognizer){ l.shadowOpacity=l.shadowOpacity==0.7?0.0:0.7 } 当令视图识别出轻触手势时,代码告知viewForLayer图层在0.7和0之间切换阴影透明度。 你说视图?嗯,没错,重写CALayer的hitTest(_:)也可以实现相同效果,本文后面也会看到这个方法,不过我们这里用的方法也有道理:图层本身并不能响应手势识别,只能响应点击测试,所以我们在视图上设置了轻触手势识别器。 然后如下修改pinchGestureRecognized(_:): @IBActionfuncpinchGestureRecognized(sender:UIPinchGestureRecognizer){ letoffset:CGFloat=sender.scale<1?5.0:-5.0 letoldFrame=l.frame letoldOrigin=oldFrame.origin letnewOrigin=CGPoint(x:oldOrigin.x+offset,y:oldOrigin.y+offset) letnewSize=CGSize(width:oldFrame.width+(offset*-2.0),height:oldFrame.height+(offset*-2.0)) letnewFrame=CGRect(origin:newOrigin,size:newSize) ifnewFrame.width>=100.0&&newFrame.width<=300.0{ l.borderWidth-=offset l.cornerRadius+=(offset/2.0) l.frame=newFrame } } 此处基于用户的捏合手势创建正负偏移值,借此调整图层框架大小、边缘宽度和边角半径。 图层的边角半径默认值为0,意即标准的90度直角。增大半径会产生圆角,如果想将图层变成圆形,可以设边角半径为宽度的一半。 注意:调整边角半径并不会裁剪图层内容(星星图片),除非图层的masksToBounds属性被设为true。 构建运行,尝试在视图中使用轻触和捏合手势: 嘿,再好好装扮一下都能当头像用了! :] CALayer体验 CALayer中的属性和方法琳琅满目,此外还有几个包含特有属性和方法的子类。 要遍历如此酷炫的API,Raywenderlich.com导游先生最好不过了。 接下来,你需要以下材料:
该App包含十种不同的CALayer示例,本文后面会依次介绍,十分方便。先来吊吊大家的胃口: 下面在讲解每个示例的同时,我建议在CALayer演示应用中亲自动手试验,还可以读读代码。不用写,只要深呼吸,轻松阅读就可以了。 :] 我相信这些酷炫的示例会启发您利用不同的CALayer为自己的App锦上添花,希望大家喜欢! 示例 #1:CALayer 前面我们看过使用CALayer的示例,也就是设置各种属性。 关于CALayer还有几点没提:
刚刚说CALayer图层有很多属性,我们来看一批实用属性:有些属性你可能第一次见,但真的很方便! //1 letlayer=CALayer() layer.frame=someView.bounds //2 layer.contents=UIImage(named:"star")?.CGImage layer.contentsGravity=kCAGravityCenter //3 layer.magnificationFilter=kCAFilterLinear layer.geometryFlipped=false //4 layer.backgroundColor=UIColor(red:11/255.0,green:86/255.0,blue:14/255.0,alpha:1.0).CGColor layer.opacity=1.0 layer.hidden=false layer.masksToBounds=false //5 layer.cornerRadius=100.0 layer.borderWidth=12.0 layer.borderColor=UIColor.whiteColor().CGColor //6 layer.shadowOpacity=0.75 layer.shadowOffset=CGSize(width:0,height:3) layer.shadowRadius=3.0 someView.layer.addSublayer(layer) 在以上代码中:
结果如下: CALayer还有两个附加属性有助于改善性能:shouldRasterize和drawsAsynchronously。 shouldRasterize默认为false,设为true可以改善性能,因为图层内容只需要一次渲染。相对画面中移动但自身外观不变的对象效果拔群。 drawsAsynchronously默认值也是false。与shouldRasterize相对,该属性适用于图层内容需要反复重绘的情况,此时设成true可能会改善性能,比如需要反复绘制大量粒子的粒子发射器图层(可以参考后面的CAEmitterLayer示例)。 谨记:如果想将已有图层的shouldRasterize或drawsAsynchronously属性设为true,一定要三思而后行,考虑可能造成的影响,对比true与false的性能差异,辨明属性设置是否有积极效果。设置不当甚至会导致性能大幅下降。 无论如何还是先回到图层演示应用,其中有些控件可以用来调整CALayer的属性: 调节试试看,感受一下,利用CALayer可以实现怎样的效果。 注:图层不属于响应链(responder chain),无法像视图一样直接响应触摸和手势,我们在CALayerPlayground中见识过。不过图层有点击测试,后面的CATransformLayer会提到。你也可以向图层添加自定义动画,CAReplicatorLayer中会出现。 示例 #2:CAScrollLayer CAScrollLayer显示一部分可滚动图层,该图层十分基础,无法直接响应用户的触摸操作,也不能直接检查可滚动图层的边界,故可避免越界无限滚动。 UIScrollView用的不是CAScrollLayer,而是直接改动图层边界。 CAScrollLayer的滚动模式可设为水平、垂直或者二维,你也可以用代码命令视图滚动到指定位置: //InScrollingView.swift importUIKit classScrollingView:UIView{ //1 overrideclassfunclayerClass()->AnyClass{ returnCAScrollLayer.self } } //InCAScrollLayerViewController.swift importUIKit classCAScrollLayerViewController:UIViewController{ @IBOutletweakvarscrollingView:ScrollingView! //2 varscrollingViewLayer:CAScrollLayer{ returnscrollingView.layerasCAScrollLayer } overridefuncviewDidLoad(){ super.viewDidLoad() //3 scrollingViewLayer.scrollMode=kCAScrollBoth } @IBActionfunctapRecognized(sender:UITapGestureRecognizer){ //4 varnewPoint=CGPoint(x:250,y:250) UIView.animateWithDuration(0.3,delay:0,options:.CurveEaseInOut,animations:{ [unownedself]in self.scrollingViewLayer.scrollToPoint(newPoint) },completion:nil) } } 以上代码:
案例研究:如果ScrollingView实例包含大于滚动视图边界的图片视图,在运行上述代码并点击视图时结果如下: 图层演示应用中有可以锁定滚动方向(水平或垂直)的开关。 以下经验规律用于决定是否使用CAScrollLayer:
示例 #3:CATextLayer CATextLayer能够对普通文本或属性字串进行简单快速的渲染。与UILabel不同,CATextLayer无法指定UIFont,只能使用CTFontRef或CGFontRef。 像下面这样的代码完全可以掌控文本的字体、字体大小、颜色、对齐、折行(wrap)和截断(truncation)规则,也有动画效果: //1 lettextLayer=CATextLayer() textLayer.frame=someView.bounds //2 varstring="" for_in1...20{ string+="Loremipsumdolorsitamet,consecteturadipiscingelit.Fusceauctorarcuquisvelitconguedictum." } textLayer.string=string //3 letfontName:CFStringRef="Noteworthy-Light" textLayer.font=CTFontCreateWithName(fontName,fontSize,nil) //4 textLayer.foregroundColor=UIColor.darkGrayColor().CGColor textLayer.wrapped=true textLayer.alignmentMode=kCAAlignmentLeft textLayer.contentsScale=UIScreen.mainScreen().scale someView.layer.addSublayer(textLayer) 以上代码解释如下:
不仅是CATextLayer,所有图层类的渲染缩放系数都默认为1。在添加到视图时,图层自身的contentsScale缩放系数会自动调整,适应当前画面。你需要为手动创建的图层明确指定contentsScale属性,否则默认的缩放系数1会在Retina显示屏上产生部分模糊。 如果创建的文本图层添加到了方形的someView,效果会像这样: 你可以设置截断(Truncation)属性,生效时被截断的部分文本会由省略号代替显示。默认设定为无截断,位置可设为开头、末尾或中间截断: 图层演示应用中,你可以随心所欲地修改很多CATextLayer属性: 示例 #4:AVPlayerLayer AVPlayerLayer是建立在AVFoundation基础上的实用图层,持有一个AVPlayer,用来播放音视频媒体文件(AVPlayerItems),举例如下: overridefuncviewDidLoad(){ super.viewDidLoad() //1 letplayerLayer=AVPlayerLayer() playerLayer.frame=someView.bounds //2 leturl=NSBundle.mainBundle().URLForResource("someVideo",withExtension:"m4v") letplayer=AVPlayer(URL:url) //3 player.actionAtItemEnd=.None playerLayer.player=player someView.layer.addSublayer(playerLayer) //4 NSNotificationCenter.defaultCenter().addObserver(self,selector:"playerDidReachEndNotificationHandler:",name:"AVPlayerItemDidPlayToEndTimeNotification",object:player.currentItem) } deinit{ NSNotificationCenter.defaultCenter().removeObserver(self) } //5 @IBActionfuncplayButtonTapped(sender:UIButton){ ifplayButton.titleLabel?.text=="Play"{ player.play() playButton.setTitle("Pause",forState:.Normal) }else{ player.pause() playButton.setTitle("Play",forState:.Normal) } updatePlayButtonTitle() updateRateSegmentedControl() } //6 funcplayerDidReachEndNotificationHandler(notification:NSNotification){ letplayerItem=notification.objectasAVPlayerItem playerItem.seekToTime(kCMTimeZero) } 上述代码解释:
注意这只是个入门示例,在实际项目中往往不会采用文字按钮控制播放。 AVPlayerLayer和其中创建的AVPlayer会像这样显示为AVPlayerItem实例的第一帧: AVPlayerLayer还有一些附加属性:
另一方面,AVPlayer也有不少附加属性和方法,有一个值得注意的是rate属性,对于0到1之间的播放速率,0代表暂停,1代表常速播放(1x)。 不过rate属性的设置是与播放行为联动的,也就是说调用pause()方法和把rate设为0是等价的,调用play()与把rate设为1也一样。 那快进、慢动作和反向播放呢?交给AVPlayerLayer把。rate大于1时会令播放器以相应倍速进行播放,例如rate设为2就是二倍速。 如你所想,rate为负时会让播放器以相应倍速反向播放。 然而,在以非常规速率播放之前,AVPlayerItem上会调用适当方法,验证是否能够以相应速率进行播放:
绝大多数视频都支持以不同速率正向播放,可以反向播放的视频相对少一些。演示应用也包含了播放控件: 示例 #5:CAGradientLayer CAGradientLayer简化了混合两种或更多颜色的工作,尤其适用于背景。要配置渐变色,你需要分配一个CGColor数组,以及标识渐变图层起止点的startPoint和endPoint。 注意:startPoint和endPoint并不是明确的点,而是用单位坐标空间定义,在绘制时映射到图层边界。也就是说x值为1表示点在图层右边缘,y值为1表示点在图层下边缘。 CAGradientLayer包含type属性,虽说该属性只有kCAGradientLayerAxial一个选择,由数组中的各颜色产生线性过渡渐变。 具体含义是渐变过渡沿startPoint到endPoint的向量A方向产生,设B与A垂直,则各条B平行线上的所有点颜色相同。 此外,locations属性可以使用一个数组(元素取值范围0到1),指定渐变图层参照colors顺序取用下一个过渡点颜色的位置。 未设定时默认会平均分配过渡点。一旦设定就必须与colors的数量保持一致,否则会出错。 :[ 下面是创建渐变图层的例子: letgradientLayer=CAGradientLayer() gradientLayer.frame=someView.bounds gradientLayer.colors=[cgColorForRed(209.0,green:0.0,blue:0.0),cgColorForRed(255.0,green:102.0,blue:34.0),green:218.0,blue:33.0),cgColorForRed(51.0,green:221.0,cgColorForRed(17.0,green:51.0,blue:204.0),cgColorForRed(34.0,blue:102.0),blue:68.0)] gradientLayer.startPoint=CGPoint(x:0,y:0) gradientLayer.endPoint=CGPoint(x:0,y:1) someView.layer.addSublayer(gradientLayer) funccgColorForRed(red:CGFloat,green:CGFloat,blue:CGFloat)->AnyObject{ returnUIColor(red:red/255.0,green:green/255.0,blue:blue/255.0,alpha:1.0).CGColorasAnyObject } 上述代码创建一个渐变图层,框架设为someView边界,指定颜色数组,设置起止点,添加图层到视图结构树。效果如下: 五彩缤纷,姹紫嫣红! 图层演示应用中,你可以随意修改起止点、颜色和过渡点: 示例 #6:CAReplicatorLayer CAReplicatorLayer能够以特定次数复制图层,可以用来创建一些很棒的效果。 每个图层复件的颜色和位置都可以改动,而且可以在总复制图层之后延迟绘制,营造一种动画效果。还可以利用深度,创造三维效果。举个例子 //1 letreplicatorLayer=CAReplicatorLayer() replicatorLayer.frame=someView.bounds //2 replicatorLayer.instanceCount=30 replicatorLayer.instanceDelay=CFTimeInterval(1/30.0) replicatorLayer.preservesDepth=false replicatorLayer.instanceColor=UIColor.whiteColor().CGColor //3 replicatorLayer.instanceRedOffset=0.0 replicatorLayer.instanceGreenOffset=-0.5 replicatorLayer.instanceBlueOffset=-0.5 replicatorLayer.instanceAlphaOffset=0.0 //4 letangle=Float(M_PI*2.0)/30 replicatorLayer.instanceTransform=CATransform3DMakeRotation(CGFloat(angle),0.0,1.0) someView.layer.addSublayer(replicatorLayer) //5 letinstanceLayer=CALayer() letlayerWidth:CGFloat=10.0 letmidX=CGRectGetMidX(someView.bounds)-layerWidth/2.0 instanceLayer.frame=CGRect(x:midX,y:0.0,width:layerWidth,height:layerWidth*3.0) instanceLayer.backgroundColor=UIColor.whiteColor().CGColor replicatorLayer.addSublayer(instanceLayer) //6 letfadeAnimation=CABasicAnimation(keyPath:"opacity") fadeAnimation.fromValue=1.0 fadeAnimation.toValue=0.0 fadeAnimation.duration=1 fadeAnimation.repeatCount=Float(Int.max) //7 instanceLayer.opacity=0.0 instanceLayer.addAnimation(fadeAnimation,forKey:"FadeAnimation")
创建一个CAReplicatorLayer实例,设框架为someView边界。 设复制图层数instanceCount和绘制延迟,设图层为2D(preservesDepth = false),实例颜色为白色。 为陆续的实例复件设置RGB颜色偏差值(默认为0,即所有复件保持颜色不变),不过这里实例初始颜色为白色,即RGB都为1.0,所以偏差值设红色为0,绿色和蓝色为相同负数会使其逐渐现出红色,alpha透明度偏差值的变化也与此类似,针对陆续的实例复件。 创建旋转变换,使得实例复件按一个圆排列。 创建供复制图层使用的实例图层,设置框架,使第一个实例在someView边界顶端水平中心处绘制,另外设置实例颜色,把实例图层添加到复制图层。 创建一个透明度由1(不透明)过渡为0(透明)的淡出动画。 设实例图层透明度为0,使得每个实例在绘制和改变颜色与alpha前保持透明。 这段代码会实现这样的东西: 图层演示应用中,你可以改动这些属性: 示例 #7:CATiledLayer CATiledLayer以图块(tile)为单位异步绘制图层内容,对超大尺寸图片或者只能在视图中显示一小部分的内容效果拔群,因为不用把内容完全载入内存就可以看到内容。 处理绘制有几种方法,一种是重写UIView,使用CATiledLayer绘制图块填充视图背景,如下: //InViewController.swift importUIKit classViewController:UIViewController{ //1 @IBOutletweakvartiledBackgroundView:TiledBackgroundView! } //InTiledBackgroundView.swift importUIKit classTiledBackgroundView:UIView{ letsideLength=CGFloat(50.0) //2 overrideclassfunclayerClass()->AnyClass{ returnCATiledLayer.self } //3 requiredinit(coderaDecoder:NSCoder){ super.init(coder:aDecoder) srand48(Int(NSDate().timeIntervalSince1970)) letlayer=self.layerasCATiledLayer letscale=UIScreen.mainScreen().scale layer.contentsScale=scale layer.tileSize=CGSize(width:sideLength*scale,height:sideLength*scale) } //4 overridefuncdrawRect(rect:CGRect){ letcontext=UIGraphicsGetCurrentContext() varred=CGFloat(drand48()) vargreen=CGFloat(drand48()) varblue=CGFloat(drand48()) CGContextSetRGBFillColor(context,red,green,blue,1.0) CGContextFillRect(context,rect) } } 代码解释:
代码绘制6×6随机色块方格,最终效果如下: 图层演示应用中除此之外还可以在图层背景上绘制轨迹: 在视图中放大时,上述截图中的星星图案会变得模糊: 产生模糊的根源是图层的细节层次(level of detail,简称LOD),CATiledLayer有两个相关属性:levelsOfDetail和levelsOfDetailBias。 levelsOfDetail顾名思义,指图层维护的LOD数目,默认值为1,每进一级会对前一级分辨率的一半进行缓存,图层的levelsOfDetail最大值,也就是最底层细节,对应至少一个像素点。 而levelsOfDetailBias指的是该图层缓存的放大LOD数目,默认为0,即不会额外缓存放大层次,每进一级会对前一级两倍分辨率进行缓存。 例如,设上述分块图层的levelsOfDetailBias为5会缓存2x、4x、8x、16x和32x的放大层次,放大的图层效果如下: 不错吧?别着急,还没讲完呢。 CATiledLayer裁刀,买不了吃亏,买不了上当,只要998…(译注:此处内容稍作本地化处理,原文玩的是1978年美国Ginsu刀具的梗,堪称询价型电视购物广告的万恶之源。) :] 开个玩笑。CATiledLayer还有一个更实用的功能:异步绘制图块,比如在滚动视图中显示一张超大图片。 在用户滚动画面时,要让分块图层知道哪些图块需要绘制,写代码在所难免,不过换来性能提升也值了。 图层演示应用的UIImage+TileCutter.swift中包含一个UIImage扩展,教程编纂组成员Nick Lockwood在著作iOS Core Animation: Advanced Techniques的一个终端应用程序中利用了这段代码。 代码的职责是把原图片拆分成指定尺寸的方块,按行列位置命名图块,比如第三行第七列的图块windingRoad62.png(索引从零开始)。 有了这些图块,我们可以自定义一个UIView子类,绘制分块图层: importUIKit classTilingViewForImage:UIView{ //1 letsideLength=CGFloat(640.0) letfileName="windingRoad" letcachesPath=NSSearchPathForDirectoriesInDomains(.CachesDirectory,.UserDomainMask,true)[0]asString //2 overrideclassfunclayerClass()->AnyClass{ returnCATiledLayer.self } //3 requiredinit(coderaDecoder:NSCoder){ super.init(coder:aDecoder) letlayer=self.layerasCATiledLayer layer.tileSize=CGSize(width:sideLength,height:sideLength) } //4 overridefuncdrawRect(rect:CGRect){ letfirstColumn=Int(CGRectGetMinX(rect)/sideLength) letlastColumn=Int(CGRectGetMaxX(rect)/sideLength) letfirstRow=Int(CGRectGetMinY(rect)/sideLength) letlastRow=Int(CGRectGetMaxY(rect)/sideLength) forrowinfirstRow...lastRow{ forcolumninfirstColumn...lastColumn{ iflettile=imageForTileAtColumn(column,row:row){ letx=sideLength*CGFloat(column) lety=sideLength*CGFloat(row) letpoint=CGPoint(x:x,y:y) letsize=CGSize(width:sideLength,height:sideLength) vartileRect=CGRect(origin:point,size:size) tileRect=CGRectIntersection(bounds,tileRect) tile.drawInRect(tileRect) } } } } funcimageForTileAtColumn(column:Int,row:Int)->UIImage?{ letfilePath="(cachesPath)/(fileName)_(column)_(row)" returnUIImage(contentsOfFile:filePath) } }
创建属性,分别是图块边长、原图文件名、供TileCutter扩展保存图块的缓存文件夹路径。 重写layerClass()返回CATiledLayer。 实现init(_:),把视图的图层转换为分块图层,设置图块大小。注意此处不必设置contentsScale适配屏幕,因为是直接修改视图自身的图层,而不是手动创建子图层。 重写drawRect(),按行列绘制各个图块。 像这样,原图大小的自定义视图就可以塞进一个滚动视图: 多亏CATiledLayer,滚动5120 x 3200的大图也会这般顺滑: 如你所见,快速滚动时绘制图块的过程还是很明显,你可以利用更小的分块(上述例子中分块为640 x 640),或者自己创建一个CATiledLayer子类,重写fadeDuration()返回0: classTiledLayer:CATiledLayer{ overrideclassfuncfadeDuration()->CFTimeInterval{ return0.0 } } 示例 #8:CAShapeLayer CAShapeLayer利用可缩放的矢量路径进行绘制,绘制速度比使用图片快很多,还有个好处是不用分别提供常规、@2x和@3x版本的图片,好用。 另外还有各种属性,让你可以自定线粗、颜色、虚实、线条接合方式、闭合线条是否形成闭合区域,还有闭合区域要填充何种颜色等。举例如下: importUIKit classViewController:UIViewController{ @IBOutletweakvarsomeView:UIView! //1 letrwColor=UIColor(red:11/255.0,alpha:1.0) letrwPath=UIBezierPath() letrwLayer=CAShapeLayer() //2 funcsetUpRWPath(){ rwPath.moveToPoint(CGPointMake(0.22,124.79)) rwPath.addLineToPoint(CGPointMake(0.22,249.57)) rwPath.addLineToPoint(CGPointMake(124.89,249.57)) rwPath.addLineToPoint(CGPointMake(249.57,143.79)) rwPath.addCurveToPoint(CGPointMake(249.37,38.25),controlPoint1:CGPointMake(249.57,85.64),controlPoint2:CGPointMake(249.47,38.15)) rwPath.addCurveToPoint(CGPointMake(206.47,112.47),controlPoint1:CGPointMake(249.27,38.35),controlPoint2:CGPointMake(229.94,71.76)) rwPath.addCurveToPoint(CGPointMake(163.46,186.84),controlPoint1:CGPointMake(182.99,153.19),controlPoint2:CGPointMake(163.61,186.65)) rwPath.addCurveToPoint(CGPointMake(146.17,156.99),controlPoint1:CGPointMake(163.27,187.03),controlPoint2:CGPointMake(155.48,173.59)) rwPath.addCurveToPoint(CGPointMake(128.79,127.08),controlPoint1:CGPointMake(136.82,140.43),controlPoint2:CGPointMake(129.03,126.94)) rwPath.addCurveToPoint(CGPointMake(109.31,157.77),controlPoint1:CGPointMake(128.59,127.18),controlPoint2:CGPointMake(119.83,141.01)) rwPath.addCurveToPoint(CGPointMake(89.83,187.86),controlPoint1:CGPointMake(98.79,174.52),controlPoint2:CGPointMake(90.02,188.06)) rwPath.addCurveToPoint(CGPointMake(56.52,108.28),controlPoint1:CGPointMake(89.24,187.23),controlPoint2:CGPointMake(56.56,109.11)) rwPath.addCurveToPoint(CGPointMake(64.02,102.25),controlPoint1:CGPointMake(56.47,107.75),controlPoint2:CGPointMake(59.24,105.56)) rwPath.addCurveToPoint(CGPointMake(101.42,67.57),controlPoint1:CGPointMake(81.99,89.78),controlPoint2:CGPointMake(93.92,78.72)) rwPath.addCurveToPoint(CGPointMake(108.38,30.65),controlPoint1:CGPointMake(110.28,54.47),controlPoint2:CGPointMake(113.01,39.96)) rwPath.addCurveToPoint(CGPointMake(10.35,0.41),controlPoint1:CGPointMake(99.66,13.17),controlPoint2:CGPointMake(64.11,2.16)) rwPath.addLineToPoint(CGPointMake(0.22,0.07)) rwPath.addLineToPoint(CGPointMake(0.22,124.79)) rwPath.closePath() } //3 funcsetUpRWLayer(){ rwLayer.path=rwPath.CGPath rwLayer.fillColor=rwColor.CGColor rwLayer.fillRule=kCAFillRuleNonZero rwLayer.lineCap=kCALineCapButt rwLayer.lineDashPattern=nil rwLayer.lineDashPhase=0.0 rwLayer.lineJoin=kCALineJoinMiter rwLayer.lineWidth=1.0 rwLayer.miterLimit=10.0 rwLayer.strokeColor=rwColor.CGColor } overridefuncviewDidLoad(){ super.viewDidLoad() //4 setUpRWPath() setUpRWLayer() someView.layer.addSublayer(rwLayer) } }
创建颜色、路径、图形图层对象。 绘制图形图层路径。如果不喜欢编写生硬的绘图代码的话,你可以尝试PaintCode这款软件,可以利用简便的工具进行可视化绘制,支持导入现有的矢量图(SVG)和Photoshop(PSD)文件,并自动生成代码。 设置图形图层。路径设为第二步中绘制的CGPath路径,填充色设为第一步中创建的CGColor颜色,填充规则设为非零(non-zero),即默认填充规则。 填充规则共有两种,另一种是奇偶(even-odd)。不过示例代码中的图形没有相交路径,两种填充规则的结果并无差异。 非零规则记从左到右的路径为+1,从右到左的路径为-1,累加所有路径值,若总和大于零,则填充路径围成的图形。 从结果上来讲,非零规则会填充图形内部所有的点。 奇偶规则计算围成图形的路径交叉数,若结果为奇数则填充。这样讲有些晦涩,还是有图有真相: 右图围成中间五边形的路径交叉数为偶数,故中间没有填充,而围成每个三角的路径交叉数为奇数,故三角部分填充颜色。
上述代码绘制raywenderlich.com的图标: 顺便看看使用PaintCode的效果图: 图层演示应用中,你可以随意修改很多CAShapeLayer属性: 注:我们先跳过演示应用中的下一个示例,因为CAEAGLLayer多少显得有些过时了,iOS 8 Metal框架有更先进的CAMetalLayer。在此推荐iOS 8 Metal入门教程。 示例 #9:CATransformLayer CATransformLayer不像其他图层类一样把子图层结构平面化,故适宜绘制3D结构。变换图层本质上是一个图层容器,每个子图层都可以应用自己的透明度和空间变换,而其他渲染图层属性(如边宽、颜色)会被忽略。 变换图层本身不支持点击测试,因为无法直接在触摸点和平面坐标空间建立映射,不过其中的子图层可以响应点击测试,例如: importUIKit classViewController:UIViewController{ @IBOutletweakvarsomeView:UIView! //1 letsideLength=CGFloat(160.0) varredColor=UIColor.redColor() varorangeColor=UIColor.orangeColor() varyellowColor=UIColor.yellowColor() vargreenColor=UIColor.greenColor() varblueColor=UIColor.blueColor() varpurpleColor=UIColor.purpleColor() vartransformLayer=CATransformLayer() //2 funcsetUpTransformLayer(){ varlayer=sideLayerWithColor(redColor) transformLayer.addSublayer(layer) layer=sideLayerWithColor(orangeColor) vartransform=CATransform3DMakeTranslation(sideLength/2.0,sideLength/-2.0) transform=CATransform3DRotate(transform,degreesToRadians(90.0),1.0,0.0) layer.transform=transform transformLayer.addSublayer(layer) layer=sideLayerWithColor(yellowColor) layer.transform=CATransform3DMakeTranslation(0.0,-sideLength) transformLayer.addSublayer(layer) layer=sideLayerWithColor(greenColor) transform=CATransform3DMakeTranslation(sideLength/-2.0,0.0) layer.transform=transform transformLayer.addSublayer(layer) layer=sideLayerWithColor(blueColor) transform=CATransform3DMakeTranslation(0.0,sideLength/-2.0,0.0) layer.transform=transform transformLayer.addSublayer(layer) layer=sideLayerWithColor(purpleColor) transform=CATransform3DMakeTranslation(0.0,sideLength/2.0,0.0) layer.transform=transform transformLayer.addSublayer(layer) transformLayer.anchorPointZ=sideLength/-2.0 applyRotationForXOffset(16.0,yOffset:16.0) } //3 funcsideLayerWithColor(color:UIColor)->CALayer{ letlayer=CALayer() layer.frame=CGRect(origin:CGPointZero,size:CGSize(width:sideLength,height:sideLength)) layer.position=CGPoint(x:CGRectGetMidX(someView.bounds),y:CGRectGetMidY(someView.bounds)) layer.backgroundColor=color.CGColor returnlayer } funcdegreesToRadians(degrees:Double)->CGFloat{ returnCGFloat(degrees*M_PI/180.0) } //4 funcapplyRotationForXOffset(xOffset:Double,yOffset:Double){ lettotalOffset=sqrt(xOffset*xOffset+yOffset*yOffset) lettotalRotation=CGFloat(totalOffset*M_PI/180.0) letxRotationalFactor=CGFloat(totalOffset)/totalRotation letyRotationalFactor=CGFloat(totalOffset)/totalRotation letcurrentTransform=CATransform3DTranslate(transformLayer.sublayerTransform,0.0) letrotationTransform=CATransform3DRotate(transformLayer.sublayerTransform,totalRotation,xRotationalFactor*currentTransform.m12-yRotationalFactor*currentTransform.m11,xRotationalFactor*currentTransform.m22-yRotationalFactor*currentTransform.m21,xRotationalFactor*currentTransform.m32-yRotationalFactor*currentTransform.m31) transformLayer.sublayerTransform=rotationTransform } //5 overridefunctouchesBegan(touches:NSSet,withEventevent:UIEvent){ ifletlocation=touches.anyObject()?.locationInView(someView){ forlayerintransformLayer.sublayers{ iflethitLayer=layer.hitTest(location){ println("Transformlayertapped!") break } } } } overridefuncviewDidLoad(){ super.viewDidLoad() //6 setUpTransformLayer() someView.layer.addSublayer(transformLayer) } } 上述代码解释:
注:currentTransform.m##是啥?问得好,是CATransform3D属性,代表矩阵元素。想学习如上代码中的矩阵变换,请参考RW教程组成员Rich Turton的三维变换娱乐教学,还有Mark Pospesel的初识矩阵项目。 在250 x 250的someView视图中运行上述代码结果如下: 再试试点击立方体的任意位置,控制台会输出“Transform layer tapped!”信息。 图层演示应用中可以调整透明度,此外Bill Dudney轨迹球工具,Swift移植版可以基于简单的用户手势应用三维变换。 示例 #10:CAEmitterLayer CAEmitterLayer渲染的动画粒子是CAEmitterCell实例。CAEmitterLayer和CAEmitterCell都包含可调整渲染频率、大小、形状、颜色、速率以及生命周期的属性。示例如下: importUIKit classViewController:UIViewController{ //1 letemitterLayer=CAEmitterLayer() letemitterCell=CAEmitterCell() //2 funcsetUpEmitterLayer(){ emitterLayer.frame=view.bounds emitterLayer.seed=UInt32(NSDate().timeIntervalSince1970) emitterLayer.renderMode=kCAEmitterLayerAdditive emitterLayer.drawsAsynchronously=true setEmitterPosition() } //3 funcsetUpEmitterCell(){ emitterCell.contents=UIImage(named:"smallStar")?.CGImage emitterCell.velocity=50.0 emitterCell.velocityRange=500.0 emitterCell.color=UIColor.blackColor().CGColor emitterCell.redRange=1.0 emitterCell.greenRange=1.0 emitterCell.blueRange=1.0 emitterCell.alphaRange=0.0 emitterCell.redSpeed=0.0 emitterCell.greenSpeed=0.0 emitterCell.blueSpeed=0.0 emitterCell.alphaSpeed=-0.5 letzeroDegreesInRadians=degreesToRadians(0.0) emitterCell.spin=degreesToRadians(130.0) emitterCell.spinRange=zeroDegreesInRadians emitterCell.emissionRange=degreesToRadians(360.0) emitterCell.lifetime=1.0 emitterCell.birthRate=250.0 emitterCell.xAcceleration=-800.0 emitterCell.yAcceleration=1000.0 } //4 funcsetEmitterPosition(){ emitterLayer.emitterPosition=CGPoint(x:CGRectGetMidX(view.bounds),y:CGRectGetMidY(view.bounds)) } funcdegreesToRadians(degrees:Double)->CGFloat{ returnCGFloat(degrees*M_PI/180.0) } overridefuncviewDidLoad(){ super.viewDidLoad() //5 setUpEmitterLayer() setUpEmitterCell() emitterLayer.emitterCells=[emitterCell] view.layer.addSublayer(emitterLayer) } //6 overridefunctraitCollectionDidChange(previousTraitCollection:UITraitCollection?){ setEmitterPosition() } } 以上代码解析: 1.创建粒子发射器图层和粒子胞(Creates an emitter layer and cell.)。 2.按照下方步骤设置粒子发射器图层:
注:渲染模式默认为无序(unordered),其他模式包括旧粒子优先(oldest first),新粒子优先(oldest last),按z轴位置从后至前(back to front)还有叠加式渲染(additive)。
3.这段代码设了不少东西。
4.把角度转换成弧度的辅助方法,还有设置粒子胞位置为视图中点。 5.设置发射器图层和粒子胞,把粒子胞添加到图层,然后把图层添加到视图结构树。 6.iOS 8的新方法,处理当前设备形态集(trait collection)的变化,比如设备旋转。不熟悉形态集的话可以参阅iOS 8教程。 总算说完了!信息量很大,但相信各位聪明的读者可以高效吸收。 上述代码运行效果如下: 图层演示应用中,你可以随意调节很多属性:
何去何从? 恭喜,看完十则示例和各种图层子类,CALayer之旅至此告一段落。 但现在才刚刚开始!新建一个项目,或者打开已有项目,尝试利用图层提升性能或营造酷炫效果!实践出真知。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |