本篇预备知识
- 这一系列是讲iOS开发中动画的使用,所以得基本熟悉iOS开发.
- 代码都是基于swift的,所以也得了解swift啊.
- 这一篇是在前一篇的基础上写的,所以得知道Layer Animations的基本使用吧!
概述
上一篇iOS动画指南 - 2.Layer Animations的基本使用中介绍了Layer Animations的一些基本使用,这一篇我们通过几个小的例子深入了解Layer Animations的用法,所以相比上篇,这篇无论是从篇幅还是连贯性都会有点大,大家准备上车吧.
文章大纲
- 可以在多个值之间变幻的CAKeyframeAnimation.
-
可以画出各种形状的CAShapeLayer.
DOG VS FOX -
可以给文字添加效果的CAGradientLayer.
滑动解锁效果 -
有轨迹的下拉刷新.
模拟下拉刷新 -
可以无限复制的CAReplicatorLayer.
CAReplicatorLayer
1. CAKeyframeAnimation
开发中情况多种多样,从一个值到另一个值的fromValue和toValue属性并不能高效的满足开发需要,比如我们要将一个view一次经过三个点呢?难道分为两次去做,那太麻烦了.对的,可以用CAKeyframeAnimation去实现,CAKeyframeAnimation有个属性values是个数组完美替代了fromValue,toValue,我们可以把三个点放进values数组,解决问题.
let flight = CAKeyframeAnimation(keyPath: "position")
flight.duration = 2.0
// 无限重复
flight.repeatCount = MAXFLOAT
// 注意:不能将CGPoint直接赋值给values需要转换,数组中的元素可以使结构体
// .map { NSValue(CGPoint: $0)}可以将数组中的每一个CGPoint转化为NSValue
flight.values = [
CGPoint(x: 50.0, y: 100.0),
CGPoint(x: view.frame.width-50, y: 160),
CGPoint(x: 50.0, y: view.center.y),
CGPoint(x: 50.0, y: 100.0)
].map { NSValue(CGPoint: $0)}
// 等价于上面代码
// flight.values = [
// NSValue(CGPoint: CGPoint(x: 50.0, y: 100.0)),
// NSValue(CGPoint: CGPoint(x: view.frame.width-50, y: 160)),
// NSValue(CGPoint: CGPoint(x: 50.0, y: view.center.y)),
// NSValue(CGPoint: CGPoint(x: 50.0, y: 100.0)),
// ]
flight.keyTimes = [0.0, 0.33, 0.66, 1.0]
dogImageView.layer.addAnimation(flight, forKey: nil)
或者我们可以做一下view的左右晃动,不添加在上面位移基础上,单独去实现:
let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
wobble.duration = 2.5
wobble.repeatCount = MAXFLOAT
// 会依次遍历数组中每一个值
wobble.values = [0.0, -M_PI_4/4, 0.0, M_PI_4/4, 0.0]
// 为values中的值设置时间,keyTimes按照百分比来的,[0,1]之间
wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
dogImageView.layer.addAnimation(wobble, forKey: nil)
2. CAShapeLayer
使用CAShapeLayer可以绘制各种图形.
比如用来画圆:
let circleLayer = CAShapeLayer()
let maskLayer = CAShapeLayer()
circleLayer.path = UIBezierPath(ovalInRect: dogImageView.bounds).CGPath
circleLayer.fillColor = UIColor.clearColor().CGColor
maskLayer.path = circleLayer.path
// 超出maskLayer部分裁剪掉
dogImageView.layer.mask = maskLayer
dogImageView.layer.addSublayer(circleLayer)
接下来让我们来看下:
DOG VS FOX
由于git图片是循环播放的,所以很难分辨动画的开始和结束,动画的开始其实是这样的:
这是AvatarView的层级:
- photoLayer : 是用来放置图片的.
- circleLayer: 是用来绘制圆形的.
- maskLayer: 是用来裁剪图片的.
- label: 用于设置名字.
让我们来分析下步骤:
- 设置两张图片的圆角
- 两张图片向中间移动,完成后将图片变成方角
- 在两张图片在中间的时候,将两张图片做一个椭圆的碰撞效果
- 后退,图片返回到开始的位置,完成后执行步骤1
实现:
我们对控件做了封装,具体看源码.
1 . 在AvatarView中的didMoveToWindow方法中将新建好的几个属性添加进去
override func didMoveToWindow() {
layer.addSublayer(photoLayer)
photoLayer.mask = maskLayer
layer.addSublayer(circleLayer)
addSubview(label)
}
2 . 重写layoutSubviews方法,设置图片的圆角
override func layoutSubviews() {
photoLayer.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
circleLayer.path = UIBezierPath(ovalInRect: bounds).CGPath
circleLayer.strokeColor = UIColor.whiteColor().CGColor
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = UIColor.clearColor().CGColor
maskLayer.path = circleLayer.path
maskLayer.position = CGPoint(x: 0.0, y: 0.0)
label.frame = CGRect(x: 0.0, y: bounds.size.height + 10.0, width: bounds.size.width, height: 24.0)
}
3 . 定义外部控制方法func boundsOffset: boundsOffset: morphSize用于传入偏移位置,以及图片碰撞时候需要设置的尺寸:
func boundsOffset(boundsOffset:CGFloat, morphSize: CGSize) {
}
4 . 在boundsOffset方法中设置图片往中间位移:
// 前进
UIView.animateWithDuration(animationDuration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [], animations: {
self.frame.origin.x = boundsOffset
}, completion: {_ in
// 将圆角图片变成方角图片
self.animateToSquare()
})
5 . 当图片在中间的时候会有一个碰撞效果:
// 碰撞效果
let morphedFrame = (originalCenter.x > boundsOffset) ?
CGRect(x: 0.0, y: bounds.height - morphSize.height,
width: morphSize.width, height: morphSize.height):
CGRect(x: bounds.width - morphSize.width,
y: bounds.height - morphSize.height,
width: morphSize.width, height: morphSize.height)
let morphAnimation = CABasicAnimation(keyPath: "path")
morphAnimation.duration = animationDuration
morphAnimation.toValue = UIBezierPath(ovalInRect: morphedFrame).CGPath
morphAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
circleLayer.addAnimation(morphAnimation, forKey:nil)
maskLayer.addAnimation(morphAnimation, forKey: nil)
6 . 返回到初始位置:
// 后退
UIView.animateWithDuration(animationDuration, delay: animationDuration, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, options: [], animations: {
self.center = originalCenter
}, completion: {_ in
delay(seconds: 0.1) {
if !self.isSquare {
self.boundsOffset(boundsOffset, morphSize: morphSize)
}
}
})
7 . 将圆角图片变成方角图片.严格意义上这不是最后一步,第四步有一个self.animateToSquare()
func animateToSquare() {
isSquare = true
let squarePath = UIBezierPath(rect: bounds).CGPath
let morph = CABasicAnimation(keyPath: "path")
morph.duration = 0.25
morph.fromValue = circleLayer.path
morph.toValue = squarePath
circleLayer.addAnimation(morph, forKey: nil)
maskLayer.addAnimation(morph, forKey: nil)
circleLayer.path = squarePath
maskLayer.path = squarePath
}
大体步骤就这些,下面我们就可以在viewController.swift中使用了
- 创建两个AvatarView,设置好图片,大小,位置.
- 调用AvatarView的boundsOffset方法,设置偏移位置,以及图片碰撞时候需要的尺寸.
let avatarSize = avatar1.frame.size
let morphSize = CGSize(
width: avatarSize.width * 0.85,
height: avatarSize.height * 1.05)
let bounceXOffset: CGFloat = view.frame.size.width/2.0 - avatar1.lineWidth*2 - avatar1.frame.width
avatar2.boundsOffset(bounceXOffset, morphSize:morphSize)
avatar1.boundsOffset(avatar1.frame.origin.x - bounceXOffset, morphSize:morphSize)
3. CAGradientLayer
我们几乎每天都会看到的iPhone滑动来解锁的文字效果是怎么实现的呢?
一步一步来吧 >.<!
- 搞一个懒加载来设置CAGradientLayer
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
// 设置开始位置和结束位置
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
// 从左到右依次的这几种颜色,颜色是渐变的
let colors = [
UIColor.blackColor().CGColor,
UIColor.whiteColor().CGColor,
UIColor.blackColor().CGColor
]
gradientLayer.colors = colors
// 颜色的位置
let locations = [0.25, 0.5, 0.75]
gradientLayer.locations = locations
return gradientLayer
}()
新建一个view,随便设置位置尺寸,将gradientLayer添加到view上面
let gradientView = UIView(frame: CGRect(x: 0, y: self.view.frame.height/2, width: self.view.frame.width, height: 80))
gradientView.backgroundColor = UIColor.lightGrayColor()
gradientLayer.frame = gradientView.bounds
gradientView.layer.addSublayer(gradientLayer)
view.addSubview(gradientView)
然后我们就可以看到这样的效果:
怎么样是不是有点那么回事了!我们还要让它动起来
2 . 为gradientLayer添加动画效果
CABasicAnimation有一个locations属性,通过修改颜色位置形成动画.
let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.75, 1.0, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = Float.infinity
gradientLayer.addAnimation(gradientAnimation, forKey: nil)
git掉帧这么严重将就着看吧!
3 . 修改白色区域的大小
这边我们调整白色区域将其*3倍,更好看一点.
4 . 考虑到代码的可复用性,我们对代码做了封装,自定义了一个view:GradientLabel ,view里面有一个text属性,通过图形上下文把字符串绘制到view上面.
// 设置字体属性
lazy var textAttributes: [String: AnyObject] = {
let style = NSMutableParagraphStyle()
style.alignment = .Center
return [
NSFontAttributeName:UIFont(name: "HelveticaNeue-Thin", size: 28.0)!,
NSParagraphStyleAttributeName:style
]
}()
@IBInspectable var text: String! {
didSet {
setNeedsDisplay()
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
text.drawInRect(bounds, withAttributes: textAttributes)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clearColor().CGColor
maskLayer.frame = CGRectOffset(bounds, bounds.size.width, 0)
maskLayer.contents = image.CGImage
gradientLayer.mask = maskLayer
}
}
然后外面就可以这样使用了
override func viewDidLoad() {
super.viewDidLoad()
let label = GradientLabel()
label.center = view.center
label.bounds = CGRect(x: 0, y: 0, width: 239, height: 44)
label.text = "> 滑动来解锁"
view.addSubview(label)
view.backgroundColor = UIColor.darkGrayColor()
}
滑动解锁效果
4. 有轨迹的下拉刷新.
下拉刷新几乎每个APP都会用到,由于有现成的第三方框架,所以要自己动手实现的情况并不是很多,但有时候需求要求自定义,所以了解下原理吧!
原理很简单:其实就是通过代理方法监听tableView的滚动,当完成刷新就恢复原来的样子.
这次我们要做一个有特效的.
- 使用CAShapeLayer绘制一个带有虚线的圆
let ovalShapeLayer: CAShapeLayer = CAShapeLayer()
let airplaneLayer: CALayer = CALayer()
// 白色的圈
ovalShapeLayer.strokeColor = UIColor.whiteColor().CGColor
ovalShapeLayer.fillColor = UIColor.clearColor().CGColor
ovalShapeLayer.lineWidth = 4.0
ovalShapeLayer.lineDashPattern = [2, 3]
let refreshRadius = frame.size.height/2 * 0.8
ovalShapeLayer.path = UIBezierPath(ovalInRect: CGRect(x: frame.size.width/2 - refreshRadius, y:frame.size.height/2 - refreshRadius , width: 2*refreshRadius, height: 2*refreshRadius)).CGPath
layer.addSublayer(ovalShapeLayer)
然后在开始位置添加飞机图片
// 添加飞机图片
let airplaneImage = UIImage(named: "airplane")
airplaneLayer.contents = airplaneImage?.CGImage
airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0, width: (airplaneImage?.size.width)!, height: airplaneImage!.size.height)
airplaneLayer.position = CGPoint(x: frame.size.width/2 + frame.size.height/2 * 0.8, y: frame.size.height/2)
layer.addSublayer(airplaneLayer)
2 . 设置开始刷新,结束刷新动画
// 开始刷新
func beginRefreshing() {
isRefreshing = true
UIView.animateWithDuration(0.3, animations: {
var newInsets = self.scrollView!.contentInset
newInsets.top += self.frame.size.height
self.scrollView!.contentInset = newInsets
})
let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.fromValue = -0.5
strokeStartAnimation.toValue = 1.0
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.5
strokeAnimationGroup.repeatDuration = 5.0
strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation]
ovalShapeLayer.addAnimation(strokeAnimationGroup, forKey: nil)
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
flightAnimation.calculationMode = kCAAnimationPaced
let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
airplaneOrientationAnimation.fromValue = 0
airplaneOrientationAnimation.toValue = 2 * M_PI
let flightAnimationGroup = CAAnimationGroup()
flightAnimationGroup.duration = 1.5
flightAnimationGroup.repeatDuration = 5.0
flightAnimationGroup.animations = [flightAnimation, airplaneOrientationAnimation]
airplaneLayer.addAnimation(flightAnimationGroup, forKey: nil)
}
// 结束刷新
func endRefreshing() {
isRefreshing = false
UIView.animateWithDuration(0.3, delay:0.0, options: .CurveEaseOut ,animations: {
var newInsets = self.scrollView!.contentInset
newInsets.top -= self.frame.size.height
self.scrollView!.contentInset = newInsets
}, completion: {_ in
})
}
3 . 在tabelView的scrollView方法中根据偏移量设置动画的开始和结束
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat( max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max( offsetY / frame.size.height, 0.0), 1.0)
if !refreshing {
redrawFromProgress(progress)
}
}
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if !refreshing && self.progress >= 1.0 {
delegate?.refreshViewDidRefresh(self)
beginRefreshing()
}
}
模拟下拉刷新
5. 可以无限复制的CAReplicatorLayer
CALayer的子类CAReplicatorLayer通过它可以对其创建的对象进行复制,从而做出复杂的效果.
1 . 创建一个CAReplicatorLayer,然后进行复制.
let replicator = CAReplicatorLayer()
let dot = CALayer()
let dotLength : CGFloat = 6.0
let dotOffset : CGFloat = 8.0
replicator.frame = view.bounds
view.layer.addSublayer(replicator)
dot.frame = CGRect(x: replicator.frame.size.width - dotLength, y: replicator.position.y, width: dotLength, height: dotLength)
dot.backgroundColor = UIColor.lightGrayColor().CGColor
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).CGColor
dot.borderWidth = 0.5
dot.cornerRadius = 1.5
replicator.addSublayer(dot)
// 进行复制
replicator.instanceCount = Int(view.frame.size.width / dotOffset)
replicator.instanceTransform = CATransform3DMakeTranslation(-dotOffset, 0.0, 0.0)
2 . 让它动起来,并且让每一个dot做一点延迟.
let move = CABasicAnimation(keyPath: "position.y")
move.fromValue = dot.position.y
move.toValue = dot.position.y - 50.0
move.duration = 1.0
move.repeatCount = 10
dot.addAnimation(move, forKey: nil)
// 延迟 0.02秒
replicator.instanceDelay = 0.02
3 . 将2中的代码注释掉,做出这样的一个效果
replicator.instanceDelay = 0.02
let scale = CABasicAnimation(keyPath: "transform")
scale.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
scale.toValue = NSValue(CATransform3D: CATransform3DMakeScale(1.4, 15, 1.0))
scale.duration = 0.33
scale.repeatCount = Float.infinity
scale.autoreverses = true
scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.addAnimation(scale, forKey: "dotScale")
4 .添加一个渐变色
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1.0
fade.toValue = 0.2
fade.duration = 0.33
fade.beginTime = CACurrentMediaTime() + 0.33
fade.repeatCount = Float.infinity
fade.autoreverses = true
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.addAnimation(fade, forKey: "dotOpacity")
5 . 添加渐变的颜色
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.magentaColor().CGColor
tint.toValue = UIColor.cyanColor().CGColor
tint.duration = 0.66
tint.beginTime = CACurrentMediaTime() + 0.28
tint.fillMode = kCAFillModeBackwards
tint.repeatCount = Float.infinity
tint.autoreverses = true
tint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.addAnimation(tint, forKey: "dotColor")
- 设置成上下摇摆
let initialRotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
initialRotation.fromValue = 0.0
initialRotation.toValue = 0.01
initialRotation.duration = 0.33
initialRotation.removedOnCompletion = false
initialRotation.fillMode = kCAFillModeForwards
initialRotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
replicator.addAnimation(initialRotation, forKey: "initialRotation")
let rotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
rotation.fromValue = 0.01
rotation.toValue = -0.01
rotation.duration = 0.99
rotation.beginTime = CACurrentMediaTime() + 0.33
rotation.repeatCount = Float.infinity
rotation.autoreverses = true
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
replicator.addAnimation(rotation, forKey: "replicatorRotation")
本文整理自 : iOS.Animations.by.Tutorials.v2.0
源码 : https://github.com/DarielChen/DemoCode
如有疑问,欢迎留言 :-D
网友评论