你的位置:首页 > 操作系统

[操作系统]iOS核心动画高级技巧之CALayer(一)


  UIView和CALayer的关系
  在iOS中一个UIView对应着一个CALayer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作.实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。(CALayer并不能响应事件,它提供了几个能判断一个触点时候再图层的范围之内)

  CALayer存在的意义
  之所以要提供CALayer和UIView这两个平行层级呢,一方面这样可以做到职责分离,可以避免很多重复的代码,另一方面由于iOS和Mac OS的界面其实没多大差别的,但是由于iOS的多点触摸的用户界面和Mac基于鼠标键盘有着本质区别,所以提供一个公用的CALayer来提供界面,分别提供UIView和NSView来提供事件

  CALayer的使用
  平时我们是这样使用UIView的:

1 let blueView = UIView(frame: CGRectMake(100, 100, 100, 100))2 blueView.backgroundColor = UIColor.blueColor()3 self.view.addSubview(blueView)

  而我们可以这样使用CALayer,你可以直接用下面这段话直接替换掉上面的代码,程序在外观上不会有任何区别 

1 let blueLayer = CALayer()2 blueLayer.frame = CGRectMake(100, 100, 100, 100)3 blueLayer.backgroundColor = UIColor.blueColor().CGColor4 self.view.layer.addSublayer(blueLayer)

  CALayer内容(contents)相关属性
contents属性
  layer有一个contents属性,它需要传入一个id(AnyObject!)类型,这是由于它在iOS平台需要CGImage而Mac需要NSImage,在OC中你需要用id类型强转一下,在Swift中你只需要直接赋一个CGImage就可以了,因为任何一个Class类型的对象都能赋值给AnyObject,如果你传入其它对象,程序不会报错,只是图片不会显示出来,UIImageView之所以能显示图片内部也是使用了这个contents属性的缘故

contentGravity属性
  contentGravity属性对应于view的contentMode属性,可以控制layer怎样对应和拉伸,虽然它的值是字符串,但是swift帮它提供了常量字符串把每个字符串对应了起来

contentsScale属性
  contentsScale属性和UIView中的contentScaleFactor是对应的,它决定了一个图片和视图的比例,即屏幕一个点显示几个像素,这是iOS设备做屏幕适配的原理,一个UIImage是包含scale,direction等信息,而转化成CGImage会丢失这些信息,自己可以通过contentsScale属性把image.scale设置给它.

maskToBounds属性
  maskToBounds属性对应于CALayer的masksToBounds属性,如果设置为true,外部就裁剪了

contentsRect属性
  contentsRect属性允许我们在图层里显示图片的一部分,这个图片的裁剪区域就是这个属性,它是一个CGRect,利用它你可以做图片拼合(即把一套图片集合在成一张图片,再对这个图片裁剪处理了再使用),这样在内存使用/载入时间/渲染性能等方面都有优势,它的值是按比例的,最大是1.

contentsCenter属性
  contentsCenter属性对应着UIImage的resizableImageWithCapInsets,它的值是一个CGRect,它代表的放大的区域,它的效果是党contentScale放大的时候,只放大contentsCenter区域,其它区域压缩

  iOS绘图
  CGImage并不是唯一可以赋值给contents属性的,也可以使用Core Graphics绘制寄宿图给它,如果你实现了drawRect方法,然后如果你调用setNeedsDisplay或者外观属性被改变时,它就会自动调用drawRect自动重绘,虽然drawRet是一个UIView方法,但是其实都是底层都是CALayer重绘保存了图片,如果你不需要自定义绘制就不要写一个空的drawRect方法,它很消耗cpu和内存资源
CALayer有一个可选的delegate属性,如果设置了delegate,并主动调用了layer的displey方法(注意和drawRect不同这个重绘时机是开发者自己控制的,也可以调用setNeedsDisplay方法给系统自己找时机调用),它会调用displayLayer(layer:CALayer!)方法,在这里是设置contents属性的最后机会了,如果你没有实现这个方法,它会尝试去调用下面这个方法:drawLayer(layer:CALayer!,inContext ctx:CGContext!),如果你实现了displayLayer方法,下面这个方法就不会调用了,drawLayer这个方法里你可以做绘图

1 override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {2   CGContextSetLineWidth(ctx, 10.0)3   CGContextSetStrokeColorWithColor(ctx, UIColor.redColor().CGColor);4   CGContextStrokeEllipseInRect(ctx, layer.bounds)d0605579435 }6 override func displayLayer(layer: CALayer!) {7   layer.contents = UIImage(named: "11.png")?.CGImage8 }

     由于一个UIView它会把它对应的CALayer的delegate设置为它自己,所以你不能再设置其它的layer的delegate为它,在UIView中都用drawRect方法,而delegate的使用只能单独的使用一个层.

   图层几何学

UIView的frame/bounds/center对应CALayer的frame/bounds/position,center和position是对应父图层的anchorPoint的所在位置,UIView的frame/bounds/center仅仅是存取方法,操纵UIView的这几个属性其实是改变CALayer对应的这几个属性.而CALayer的frame属性又是个计算属性,它是根据bounds/position/transform三个属性计算出来的,而你改变frame的值也可能影响到其中的值,如果你做旋转和缩放后frame和bounds可能不再一致了,bounds就是宽高,而frame还要计算旋转后x和y轴占的空间,如下图

  CALayer通过anchorPoint(锚点)和center(position)对齐来控制UIView的位置,锚点是相对UIView的一个位置,而center就是一个点,由于anchorPoint属性对UIView是屏蔽的,而anchorPoint默认值又是{0.5,0.5},所以这个属性才叫center.而UIView和CALayertransform旋转也是围绕这anchorPoint旋转的,这时候如果是一个圆周运动(比如说时钟旋转)就需要设置锚点的值,让它正常旋转.如下图

            

  在CALayer和UIView都有一套可以把它相对于当前父图层的位置转换成相对其它图层(或view)的位置,Mac OS和iOS的坐标系统是相反的,iOS左上Mac OC左下,你可以用layer的geometryFlipped属性来适配,它可以翻转坐标系.layer的zPosition属性是设置它垂直坐标轴的位置的,默认都是0,所以你只要设置为1,就会显示在其它层的上面

  可以通过相对坐标转换判断点击的点是否在一个layer或view上,代码:

 1 var point = (touches as NSSet).anyObject()?.locationInView(self.view) 2 point = blueLayer.convertPoint(point!, fromLayer: self.view.layer) 3  4 if blueLayer.containsPoint(point!) { 5   println("touch in blue") 6    7   let yellowPoint = yellowLayer.convertPoint(point!, fromLayer: blueLayer) 8   if yellowLayer.containsPoint(yellowPoint) { 9     println("touch in yellow")10   }11   12   let redPoint = redLayer.convertPoint(point!, fromLayer: blueLayer)13   if redLayer.containsPoint(redPoint) {14     println("touch in red")15   }16 }

 

  hitTest可以获取你接触的那个图层:

let point = (touches as NSSet).anyObject()?.locationInView(self.view)let layer = self.view.layer.hitTest(point!)if layer == blueLayer {  println("touch in blue")}if layer == yellowLayer {  println("touch in yellow")}if layer == redLayer {  println("touch in red")}

  UIView可以通过autoresizingMask和constraints等属性做到自适应屏幕旋转,CaLayer也有对应的layoutManager属性和CAConstraintLayoutManager类,但是只能在Mac OS上使用,iOS上还不支持,如果你想使用这个特性你就不能单独使用layer,但是如果你想调整layer的大小还是可以通过设置layer的delegate,然后实现代理方法layoutSublayersOfLayer直接修改大小颜色之类,它也需要调用setNeedsLayout方法,它和UIView对应的layoutSubviews是一样的.

  视觉效果
圆角
  layer的cornerRadius属性可以设置圆角曲率,如果曲率大小为边长的一半,圆角会内切于这条边,如果是个正方形最后的结果就是个圆形,如果是裁剪子视图也是直接按圆内裁剪

边框
  layer的borderColor设置边框颜色,它是CGColorRef,borderWidth设置边框宽度,值得注意的是边框宽度占用的是layer的frame的宽度,它并不会在layer外面加一层边框而是在内部生成.而layer会被子layer覆盖但边框不会被覆盖,并且边框只是显示用的,它不会干扰触摸和事件,有没有边框都是一样的.

阴影
  阴影至少需要shadowColor/shadowOffset/shadowOpacity三个属性才能起作用,shadowOffset的值是CGSize类型,两个正值是朝右下角.还有一个属性shadowRadius,它控制阴影的边界模糊程度,默认值是3,值为0则不模糊,值越大越模糊,模糊的结果是CGSize放心颜色重,其它几个方向也有对应的阴影,会有一个层次感.
因为阴影是在layer外部的,所以如果要裁剪超出layer的子视图则需要使用maskToBounds属性,而这时阴影也会被裁剪掉,所以当maskToBounds和阴影共存时需要特殊处理下:

 1 blueLayer = CALayer() 2 blueLayer.frame = CGRectMake(50, 100, 300, 300) 3 blueLayer.backgroundColor = UIColor.blueColor().CGColor 4 blueLayer.cornerRadius = blueLayer.bounds.size.width / 2.0 5 blueLayer.borderColor = UIColor.purpleColor().CGColor 6 blueLayer.borderWidth = 10.0 7  8 blueLayer.masksToBounds = true 9 10 let shadowLayer = CALayer()11 shadowLayer.frame = blueLayer.frame12 shadowLayer.cornerRadius = blueLayer.cornerRadius13 shadowLayer.shadowColor = UIColor.redColor().CGColor14 shadowLayer.shadowOffset = CGSizeMake(23 , 23.0)15 shadowLayer.shadowOpacity = 116 shadowLayer.shadowRadius = 6017 shadowLayer.backgroundColor = UIColor.redColor().CGColor18 self.view.layer.addSublayer(shadowLayer)19 20 self.view.layer.addSublayer(blueLayer)

  需要注意的是阴影层需要有背景色,不然它的阴影显示不出来

  shadowPath属性可以设置阴影的形状,注意swift中的CGMutablePath并不会增加引用计数,你不需要relase它也没有方法可以提供给你release

1 let circlePath = CGPathCreateMutable()2  CGPathAddEllipseInRect(circlePath, nil, self.blueLayer.bounds)3 shadowLayer.shadowPath = circlePath

  当然你也可以使用UIBezierPath来设置更复杂的形状给shadowPath

1 let path1 = UIBezierPath(roundedRect: CGRectMake(-75, -75, 200, 200), cornerRadius: 30).CGPath2 let path2 = UIBezierPath(arcCenter: CGPointMake(25, 40), radius: 100, startAngle: 0, endAngle: 3.14159265, clockwise: true).CGPath3 shadowLayer.shadowPath = path14 shadowLayer.shadowPath = path2

  图片蒙板

  layer的mask属性可以设置一个layer给他,这个layer的contents应该是一个32位有alpha通道的png图片,你可以设置一个不规则的图片其它部分为透明,这样就对layer设置了一个蒙板,蒙板的颜色不重要,轮廓比较重要,最后被设置了蒙板的layer它只会显示mask蒙板形状的内容.

1 let maskLayer = CALayer()2 maskLayer.frame = CGRectMake(0, 0, 100, 100)3 maskLayer.contents = UIImage(named: "111.png")?.CGImage4 blueLayer.mask = maskLayer

 

  拉伸

  如果设置的图片不需要拉伸有很多好处,即不需要拉伸图片,又能合理的使用内存和cpu,但是很多时候一张图片要多个位置使用,所以需要拉伸,iOS跟我们提供了3中拉伸方式kCAFilterLinear/kCAFilterNearest/kCAFilterTrilinear,设置拉伸方式只需要跟layer的这两个属性赋对应的拉伸方式就可以:minification(缩小图片)和magnification(放大图片),这个属性默认是kCAFilterLinear,它和kCAFilterTrilinear类似,他们都是线性的,意思就是它会取两个值的过度色,让对应点能顺滑的过渡,如果图片有渐变色和斜线比较多就需要用这个默认的,如果图片主要是单色而且主要是垂直方向的颜色,那么就需要kCAFilterNearest,它是暴力的直接取周围的颜色,那样又不会失真,也不会太消耗cpu

  组透明

  iOS8默认就是组透明,在应用透明度之前,它会把子图层和它整合成一个整体的图片,那样就没有透明度混合的问题了,目前没有测试出透明度出问题的情况,如果有可以设置layer的shouldRasterize的值为YES