@Xiaojun-Jin
2015-04-08T07:01:57.000000Z
字数 19521
阅读 2295
iOS CALayer
Note from author: This is a learning note of CALayer in iOS with Swift: 10 Examples written by Scott Gardner.
CALayer's coordinate (gravity) is opposite to UIView. For example:
layer.backgroundColor = UIColor.blueColor().CGColorlayer.borderWidth = 100.0layer.borderColor = UIColor.redColor().CGColorlayer.shadowOpacity = 0.7layer.shadowRadius = 10.0layer.contents = UIImage(named: "star")?.CGImagelayer.contentsGravity = kCAGravityTop
The star image will appear in the bottom section of the view. However, if we add layer.geometryFlipped = true to flip the geometry, then it'll be positioned at the top section as we expected.
@IBAction func pinchGestureRecognized(sender: UIPinchGestureRecognizer) {let offset: CGFloat = sender.scale < 1 ? 5.0 : -5.0let oldFrame = l.framelet oldOrigin = oldFrame.originlet newOrigin = CGPoint(x: oldOrigin.x + offset, y: oldOrigin.y + offset)let newSize = CGSize(width : oldFrame.width + (offset * -2.0),height: oldFrame.height + (offset * -2.0))let newFrame = CGRect(origin: newOrigin, size: newSize)if newFrame.width >= 100.0 && newFrame.width <= 300.0 {l.borderWidth -= offsetl.cornerRadius += (offset / 2.0)l.frame = newFrame}}
Here we're creating a positive or negative offset based on the user's pinch, and then adjusting the size of the layer's frame, width of its border and the border's corner radius.
Note that adjusting the corner radius doesn't clip the layer's contents (the star image) unless the layer's
masksToBoundsproperty is set to true.
- Layer properties are animated.
- Layers are lightweight.
- Layers have tons of useful properties.
Use this filter when enlarging the image via contentsGravity:
layer.magnificationFilter = kCAFilterLinearlayer.geometryFlipped = false
which can be used to change both size (resize, resize aspect, and resize aspect fill) and position (center, top, top-right, right, etc.). These changes are not animated, and if geometryFlipped is not set to true, the positional geometry and shadow will be upside-down.
CALayer has two additional properties that can improve performance: shouldRasterize and drawsAsynchronously:
shouldRasterizeis false by default, and when set to true it can improve performance because a layer's contents only need to be rendered once. It's perfect for objects that are animated around the screen but don't change in appearance.drawsAsynchronouslyis sort of the opposite of shouldRasterize. It's also false by default. Set it to true to improve performance when a layer's contents must be repeatedly redrawn.
What you can do with a CAScrollLayer is set its scrolling mode to horizontal and/or vertical, and you can programmatically tell it to scroll to a specific point or rect:
In ScrollingView.swift:
import UIKitclass ScrollingView: UIView{override class func layerClass() -> AnyClass{return CAScrollLayer.self}}
In CAScrollLayerViewController.swift:
import UIKitclass CAScrollLayerViewController: UIViewController{@IBOutlet weak var scrollingView: ScrollingView!var scrollingViewLayer: CAScrollLayer{return scrollingView.layer as CAScrollLayer}override func viewDidLoad(){super.viewDidLoad()scrollingViewLayer.scrollMode = kCAScrollBoth}@IBAction func tapRecognized(sender: UITapGestureRecognizer){var newPoint = CGPoint(x: 250, y: 250)UIView.animateWithDuration(0.3, delay: 0, options: .CurveEaseInOut, animations:{[unowned self] in self.scrollingViewLayer.scrollToPoint(newPoint)}, completion: nil)}}
Create a layer:
var l: CALayer { return sView.layer }
or
// creating a new layer and adding it as a sublayer
override class func layerClass() -> AnyClass { return CAScrollLayer.self }
P.S. for this demo, add a UIImageView as the subview of scrollingView, and setting its view mode with property Center
Check out what is
[unowned self]: http://stackoverflow.com/questions/24320347/shall-we-always-use-unowned-self-inside-closure-in-swift
With a block of code like this, it's possible to manipulate font, font size, color, alignment, wrapping and truncation, as well as animate changes
// 1let textLayer = CATextLayer()textLayer.frame = someView.bounds// 2var string = ""for _ in 1...20 {string += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit congue dictum. "}textLayer.string = string// 3let fontName: CFStringRef = "Noteworthy-Light"textLayer.font = CTFontCreateWithName(fontName, fontSize, nil)// 4textLayer.foregroundColor = UIColor.darkGrayColor().CGColortextLayer.wrapped = truetextLayer.alignmentMode = kCAAlignmentLefttextLayer.contentsScale = UIScreen.mainScreen().scalesomeView.layer.addSublayer(textLayer)
You need to set the
contentsScaleexplicitly for layers you create manually, or else their scale factor will be 1 and you'll have pixilation on retina displays.
override func viewDidLoad() {super.viewDidLoad()// 1let playerLayer = AVPlayerLayer()playerLayer.frame = someView.bounds// 2let url = NSBundle.mainBundle().URLForResource("someVideo", withExtension: "m4v")let player = AVPlayer(URL: url)// 3player.actionAtItemEnd = .NoneplayerLayer.player = playersomeView.layer.addSublayer(playerLayer)// 4NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidReachEndNotificationHandler:", name: "AVPlayerItemDidPlayToEndTimeNotification", object: player.currentItem)}deinit {NSNotificationCenter.defaultCenter().removeObserver(self)}// 5@IBAction func playButtonTapped(sender: UIButton) {if playButton.titleLabel?.text == "Play" {player.play()playButton.setTitle("Pause", forState: .Normal)} else {player.pause()playButton.setTitle("Play", forState: .Normal)}updatePlayButtonTitle()updateRateSegmentedControl()}// 6func playerDidReachEndNotificationHandler(notification: NSNotification) {let playerItem = notification.object as AVPlayerItemplayerItem.seekToTime(kCMTimeZero)}
Setting
ratealso instructs playback to commence at that rate. In other words, calling pause() and settingrateto 0 does the same thing, as calling play() and settingrateto 1.
To configure CAGradientLayer, you assign an array of CGColors, as well as a startPoint and an endPoint to specify where the gradient layer should begin and end.
let gradientLayer = CAGradientLayer()gradientLayer.frame = someView.boundsgradientLayer.colors = [cgColorForRed(209.0, green: 0.0, blue: 0.0),cgColorForRed(255.0, green: 102.0, blue: 34.0),cgColorForRed(255.0, green: 218.0, blue: 33.0),cgColorForRed(51.0, green: 221.0, blue: 0.0),cgColorForRed(17.0, green: 51.0, blue: 204.0),cgColorForRed(34.0, green: 0.0, blue: 102.0),cgColorForRed(51.0, green: 0.0, blue: 68.0)]gradientLayer.startPoint = CGPoint(x: 0, y: 0)gradientLayer.endPoint = CGPoint(x: 0, y: 1)someView.layer.addSublayer(gradientLayer)func cgColorForRed(red: CGFloat, green: CGFloat, blue: CGFloat) -> AnyObject {return UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0).CGColor as AnyObject}
CAReplicatorLayer duplicates a layer a specified number of times, which can allow you to create some cool effects.
Each layer copy can have it's own color and positioning changes, and its drawing can be delayed to give an animation effect to the overall replicator layer. Depth can also be preserved to give the replicator layer a 3D effect.
// 1let replicatorLayer = CAReplicatorLayer()replicatorLayer.frame = someView.bounds// 2replicatorLayer.instanceCount = 30replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0)replicatorLayer.preservesDepth = falsereplicatorLayer.instanceColor = UIColor.whiteColor().CGColor// 3replicatorLayer.instanceRedOffset = 0.0replicatorLayer.instanceGreenOffset = -0.5replicatorLayer.instanceBlueOffset = -0.5replicatorLayer.instanceAlphaOffset = 0.0// 4let angle = Float(M_PI * 2.0) / 30replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)someView.layer.addSublayer(replicatorLayer)// 5let instanceLayer = CALayer()let layerWidth: CGFloat = 10.0let midX = CGRectGetMidX(someView.bounds) - layerWidth / 2.0instanceLayer.frame = CGRect(x: midX, y: 0.0, width: layerWidth, height: layerWidth * 3.0)instanceLayer.backgroundColor = UIColor.whiteColor().CGColorreplicatorLayer.addSublayer(instanceLayer)// 6let fadeAnimation = CABasicAnimation(keyPath: "opacity")fadeAnimation.fromValue = 1.0fadeAnimation.toValue = 0.0fadeAnimation.duration = 1fadeAnimation.repeatCount = Float(Int.max)// 7instanceLayer.opacity = 0.0instanceLayer.addAnimation(fadeAnimation, forKey: "FadeAnimation")
replicatorLayer.instanceTransformis applied relative to the center of the replicator layer, i.e. the superlayer of each replicated sublayer.
There are a couple of ways to handle the drawing. One is to override UIView and use a CATiledLayer to repeatedly draw tiles to fill up view's background, like this:
In ViewController.swift:
import UIKitclass ViewController: UIViewController {// 1@IBOutlet weak var tiledBackgroundView: TiledBackgroundView!}
In TiledBackgroundView.swift:
import UIKitclass TiledBackgroundView: UIView {let sideLength = CGFloat(50.0)// 2override class func layerClass() -> AnyClass {return CATiledLayer.self}// 3required init(coder aDecoder: NSCoder) {super.init(coder: aDecoder)srand48(Int(NSDate().timeIntervalSince1970))let layer = self.layer as CATiledLayerlet scale = UIScreen.mainScreen().scalelayer.contentsScale = scalelayer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)}// 4override func drawRect(rect: CGRect) {let context = UIGraphicsGetCurrentContext()var red = CGFloat(drand48())var green = CGFloat(drand48())var blue = CGFloat(drand48())CGContextSetRGBFillColor(context, red, green, blue, 1.0)CGContextFillRect(context, rect)}}
CATiledLayer has two properties, levelsOfDetail and levelsOfDetailBias.
levelsOfDetailas its name aptly applies, is the number of levels of detail maintained by the layer.levelsOfDetailBiason the other hand, is the number of magnified levels of detail cached by this layer.
For example, increasing the levelsOfDetailBias to 5 for the blurry tiled layer above would result in caching levels magnified at 2x, 4x, 8x, 16x and 32x.
Check out this link for more detail about
levelsOfDetailandlevelsOfDetailBias: http://www.cocoachina.com/bbs/read.php?tid-31201.html
CATiledLayer has another useful power: asynchronously drawing tiles of a very large image, for example, within a scroll view.
Layer Player includes a UIImage extension in a file named UIImage+TileCutter.swift. Fellow tutorial team member Nick Lockwood adapted this code for his Terminal app, which he provided in his excellent book, iOS Core Animation: Advanced Techniques.
Its job is to slice and dice the source image into square tiles of the specified size, named according to the column and row location of each tile; for example, windingRoad_6_2.png for the tile at column 7, row 3 (zero-indexed):
import UIKitclass TilingViewForImage: UIView {// 1let sideLength = CGFloat(640.0)let fileName = "windingRoad"let cachesPath = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String// 2override class func layerClass() -> AnyClass {return CATiledLayer.self}// 3required init(coder aDecoder: NSCoder) {super.init(coder: aDecoder)let layer = self.layer as CATiledLayerlayer.tileSize = CGSize(width: sideLength, height: sideLength)}// 4override func drawRect(rect: CGRect) {let firstColumn = Int(CGRectGetMinX(rect) / sideLength)let lastColumn = Int(CGRectGetMaxX(rect) / sideLength)let firstRow = Int(CGRectGetMinY(rect) / sideLength)let lastRow = Int(CGRectGetMaxY(rect) / sideLength)for row in firstRow...lastRow {for column in firstColumn...lastColumn {if let tile = imageForTileAtColumn(column, row: row) {let x = sideLength * CGFloat(column)let y = sideLength * CGFloat(row)let point = CGPoint(x: x, y: y)let size = CGSize(width: sideLength, height: sideLength)var tileRect = CGRect(origin: point, size: size)tileRect = CGRectIntersection(bounds, tileRect)tile.drawInRect(tileRect)}}}}func imageForTileAtColumn(column: Int, row: Int) -> UIImage? {let filePath = "\(cachesPath)/\(fileName)_\(column)_\(row)"return UIImage(contentsOfFile: filePath)}}
Then a view of that subclass type, sized to the original image's dimensions can be added to a scroll view.
Note that it is not necessary to match contentsScale to the screen scale, because you're working with the backing layer of the view directly vs. creating a new layer and adding it as a sublayer.
As you can see in the above animation, though, there is noticeable blockiness when fast-scrolling as individual tiles are drawn. Minimize this behavior by using smaller tiles (the tiles used in the above example were cut to 640 x 640) and by creating a custom CATiledLayer subclass and overriding fadeDuration() to return 0:
class TiledLayer: CATiledLayer {override class func fadeDuration() -> CFTimeInterval {return 0.0}}
import UIKitclass ViewController: UIViewController {@IBOutlet weak var someView: UIView!// 1let rwColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0)let rwPath = UIBezierPath()let rwLayer = CAShapeLayer()// 2func setUpRWPath() {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, 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()}// 3func setUpRWLayer() {rwLayer.path = rwPath.CGPathrwLayer.fillColor = rwColor.CGColorrwLayer.fillRule = kCAFillRuleNonZerorwLayer.lineCap = kCALineCapButtrwLayer.lineDashPattern = nilrwLayer.lineDashPhase = 0.0rwLayer.lineJoin = kCALineJoinMiterrwLayer.lineWidth = 1.0rwLayer.miterLimit = 10.0rwLayer.strokeColor = rwColor.CGColor}override func viewDidLoad() {super.viewDidLoad()// 4setUpRWPath()setUpRWLayer()someView.layer.addSublayer(rwLayer)}}
Draws the shape layer's path. If writing this sort of boilerplate drawing code is not your cup of tea, check out PaintCode; it generates the code for you by letting you draw using intuitive visual controls or import existing vector (SVG) or Photoshop (PSD) files.
CATransformLayer does not flatten its sublayer hierarchy like other layer classes, so it's handy for drawing 3D structures.
import UIKitclass ViewController: UIViewController {@IBOutlet weak var someView: UIView!// 1let sideLength = CGFloat(160.0)var redColor = UIColor.redColor()var orangeColor = UIColor.orangeColor()var yellowColor = UIColor.yellowColor()var greenColor = UIColor.greenColor()var blueColor = UIColor.blueColor()var purpleColor = UIColor.purpleColor()var transformLayer = CATransformLayer()// 2func setUpTransformLayer() {var layer = sideLayerWithColor(redColor)transformLayer.addSublayer(layer)layer = sideLayerWithColor(orangeColor)var transform = CATransform3DMakeTranslation(sideLength / 2.0, 0.0, sideLength / -2.0)transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)layer.transform = transformtransformLayer.addSublayer(layer)layer = sideLayerWithColor(yellowColor)layer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength)transformLayer.addSublayer(layer)layer = sideLayerWithColor(greenColor)transform = CATransform3DMakeTranslation(sideLength / -2.0, 0.0, sideLength / -2.0)transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)layer.transform = transformtransformLayer.addSublayer(layer)layer = sideLayerWithColor(blueColor)transform = CATransform3DMakeTranslation(0.0, sideLength / -2.0, sideLength / -2.0)transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)layer.transform = transformtransformLayer.addSublayer(layer)layer = sideLayerWithColor(purpleColor)transform = CATransform3DMakeTranslation(0.0, sideLength / 2.0, sideLength / -2.0)transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)layer.transform = transformtransformLayer.addSublayer(layer)transformLayer.anchorPointZ = sideLength / -2.0applyRotationForXOffset(16.0, yOffset: 16.0)}// 3func sideLayerWithColor(color: UIColor) -> CALayer {let layer = 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.CGColorreturn layer}func degreesToRadians(degrees: Double) -> CGFloat {return CGFloat(degrees * M_PI / 180.0)}// 4func applyRotationForXOffset(xOffset: Double, yOffset: Double) {let totalOffset = sqrt(xOffset * xOffset + yOffset * yOffset)let totalRotation = CGFloat(totalOffset * M_PI / 180.0)let xRotationalFactor = CGFloat(totalOffset) / totalRotationlet yRotationalFactor = CGFloat(totalOffset) / totalRotationlet currentTransform = CATransform3DTranslate(transformLayer.sublayerTransform, 0.0, 0.0, 0.0)let rotationTransform = 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}// 5override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {if let location = touches.anyObject()?.locationInView(someView) {for layer in transformLayer.sublayers {if let hitLayer = layer.hitTest(location) {println("Transform layer tapped!")break}}}}override func viewDidLoad() {super.viewDidLoad()// 6setUpTransformLayer()someView.layer.addSublayer(transformLayer)}}
To learn more about matrix transformations like those used in this example, check out 3DTransformFun project by fellow tutorial team member Rich Turton and Enter The Matrix project by Mark Pospesel.
Layer Player includes switches to toggle the opacity of each sublayer, and the TrackBall utility from Bill Dudney, ported to Swift, which makes it easy to apply 3D transforms based on user gestures.
CAEmitterLayer renders animated particles that are instances of CAEmitterCell. Both CAEmitterLayer and CAEmitterCell have properties to change rendering rate, size, shape, color, velocity, lifetime and more.
import UIKitclass ViewController: UIViewController {// 1let emitterLayer = CAEmitterLayer()let emitterCell = CAEmitterCell()// 2func setUpEmitterLayer() {emitterLayer.frame = view.boundsemitterLayer.seed = UInt32(NSDate().timeIntervalSince1970)emitterLayer.renderMode = kCAEmitterLayerAdditiveemitterLayer.drawsAsynchronously = truesetEmitterPosition()}// 3func setUpEmitterCell() {emitterCell.contents = UIImage(named: "smallStar")?.CGImageemitterCell.velocity = 50.0emitterCell.velocityRange = 500.0emitterCell.color = UIColor.blackColor().CGColoremitterCell.redRange = 1.0emitterCell.greenRange = 1.0emitterCell.blueRange = 1.0emitterCell.alphaRange = 0.0emitterCell.redSpeed = 0.0emitterCell.greenSpeed = 0.0emitterCell.blueSpeed = 0.0emitterCell.alphaSpeed = -0.5let zeroDegreesInRadians = degreesToRadians(0.0)emitterCell.spin = degreesToRadians(130.0)emitterCell.spinRange = zeroDegreesInRadiansemitterCell.emissionRange = degreesToRadians(360.0)emitterCell.lifetime = 1.0emitterCell.birthRate = 250.0emitterCell.xAcceleration = -800.0emitterCell.yAcceleration = 1000.0}// 4func setEmitterPosition() {emitterLayer.emitterPosition = CGPoint(x: CGRectGetMidX(view.bounds), y: CGRectGetMidY(view.bounds))}func degreesToRadians(degrees: Double) -> CGFloat {return CGFloat(degrees * M_PI / 180.0)}override func viewDidLoad() {super.viewDidLoad()// 5setUpEmitterLayer()setUpEmitterCell()emitterLayer.emitterCells = [emitterCell]view.layer.addSublayer(emitterLayer)}// 6override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {setEmitterPosition()}}
Awesome CALayer Demo: https://github.com/scotteg/LayerPlayer