@Xiaojun-Jin
2015-04-08 07:01
字数 19521
阅读 2123
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().CGColor
layer.borderWidth = 100.0
layer.borderColor = UIColor.redColor().CGColor
layer.shadowOpacity = 0.7
layer.shadowRadius = 10.0
layer.contents = UIImage(named: "star")?.CGImage
layer.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.0
let oldFrame = l.frame
let oldOrigin = oldFrame.origin
let 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 -= offset
l.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
masksToBounds
property 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 = kCAFilterLinear
layer.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
:
shouldRasterize
is 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.drawsAsynchronously
is 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 UIKit
class ScrollingView: UIView
{
override class func layerClass() -> AnyClass
{
return CAScrollLayer.self
}
}
In CAScrollLayerViewController.swift:
import UIKit
class 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
// 1
let textLayer = CATextLayer()
textLayer.frame = someView.bounds
// 2
var string = ""
for _ in 1...20 {
string += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit congue dictum. "
}
textLayer.string = string
// 3
let fontName: 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)
You need to set the
contentsScale
explicitly 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()
// 1
let playerLayer = AVPlayerLayer()
playerLayer.frame = someView.bounds
// 2
let url = NSBundle.mainBundle().URLForResource("someVideo", withExtension: "m4v")
let player = 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
@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()
}
// 6
func playerDidReachEndNotificationHandler(notification: NSNotification) {
let playerItem = notification.object as AVPlayerItem
playerItem.seekToTime(kCMTimeZero)
}
Setting
rate
also instructs playback to commence at that rate. In other words, calling pause() and settingrate
to 0 does the same thing, as calling play() and settingrate
to 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.bounds
gradientLayer.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.
// 1
let replicatorLayer = 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
let angle = Float(M_PI * 2.0) / 30
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
someView.layer.addSublayer(replicatorLayer)
// 5
let instanceLayer = CALayer()
let layerWidth: CGFloat = 10.0
let midX = 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
let fadeAnimation = 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")
replicatorLayer.instanceTransform
is 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 UIKit
class ViewController: UIViewController {
// 1
@IBOutlet weak var tiledBackgroundView: TiledBackgroundView!
}
In TiledBackgroundView.swift:
import UIKit
class TiledBackgroundView: UIView {
let sideLength = CGFloat(50.0)
// 2
override class func layerClass() -> AnyClass {
return CATiledLayer.self
}
// 3
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
srand48(Int(NSDate().timeIntervalSince1970))
let layer = self.layer as CATiledLayer
let scale = UIScreen.mainScreen().scale
layer.contentsScale = scale
layer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)
}
// 4
override 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
.
levelsOfDetail
as its name aptly applies, is the number of levels of detail maintained by the layer.levelsOfDetailBias
on 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
levelsOfDetail
andlevelsOfDetailBias
: 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 UIKit
class TilingViewForImage: UIView {
// 1
let sideLength = CGFloat(640.0)
let fileName = "windingRoad"
let cachesPath = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String
// 2
override class func layerClass() -> AnyClass {
return CATiledLayer.self
}
// 3
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let layer = self.layer as CATiledLayer
layer.tileSize = CGSize(width: sideLength, height: sideLength)
}
// 4
override 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 UIKit
class ViewController: UIViewController {
@IBOutlet weak var someView: UIView!
// 1
let rwColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0)
let rwPath = UIBezierPath()
let rwLayer = CAShapeLayer()
// 2
func 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()
}
// 3
func setUpRWLayer() {
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
}
override func viewDidLoad() {
super.viewDidLoad()
// 4
setUpRWPath()
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 UIKit
class ViewController: UIViewController {
@IBOutlet weak var someView: UIView!
// 1
let 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()
// 2
func 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 = transform
transformLayer.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 = transform
transformLayer.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 = transform
transformLayer.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 = transform
transformLayer.addSublayer(layer)
transformLayer.anchorPointZ = sideLength / -2.0
applyRotationForXOffset(16.0, yOffset: 16.0)
}
// 3
func 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.CGColor
return layer
}
func degreesToRadians(degrees: Double) -> CGFloat {
return CGFloat(degrees * M_PI / 180.0)
}
// 4
func applyRotationForXOffset(xOffset: Double, yOffset: Double) {
let totalOffset = sqrt(xOffset * xOffset + yOffset * yOffset)
let totalRotation = CGFloat(totalOffset * M_PI / 180.0)
let xRotationalFactor = CGFloat(totalOffset) / totalRotation
let yRotationalFactor = CGFloat(totalOffset) / totalRotation
let 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
}
// 5
override 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()
// 6
setUpTransformLayer()
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 UIKit
class ViewController: UIViewController {
// 1
let emitterLayer = CAEmitterLayer()
let emitterCell = CAEmitterCell()
// 2
func setUpEmitterLayer() {
emitterLayer.frame = view.bounds
emitterLayer.seed = UInt32(NSDate().timeIntervalSince1970)
emitterLayer.renderMode = kCAEmitterLayerAdditive
emitterLayer.drawsAsynchronously = true
setEmitterPosition()
}
// 3
func setUpEmitterCell() {
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
let zeroDegreesInRadians = 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
func 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()
// 5
setUpEmitterLayer()
setUpEmitterCell()
emitterLayer.emitterCells = [emitterCell]
view.layer.addSublayer(emitterLayer)
}
// 6
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
setEmitterPosition()
}
}
Awesome CALayer Demo: https://github.com/scotteg/LayerPlayer