1.首先要知道CAShapeLayer是以UIBezierPath生成的路径为基础显示形状的。单单CAShapeLayer初始化是没有意义的,不会显示出形状的
想完成下图的动画:
![](https://img.haomeiwen.com/i2279620/fd2f380e5d02e619.gif)
从动图可以看出,动画分四个阶段,第一是圆从无到有再到一定大小的动画(波动动画),第二是圆形再变大的过程中,并且同时透明度也跟着变淡到不见,第三个动画先是从小的带圆角的矩形,变成大的带圆角矩形,第四个是类似数据加载的动画,不停旋转。
现在先分析第一阶段波动动画:
![](https://img.haomeiwen.com/i2279620/5b6bef3294760204.gif)
实现思路:要实现它,我们就得知道首先得有个图层,然后图层给个圆形形状,再结合动画的变化,就基本可以实现了。基于这样思路,这时候就要想起CAShapeLayer,calayer,而要圆形形状就得用到UIBezierPath的roundedRect函数,而CAShapeLayer和UIBezierPath配合完美,所以我们选择图层由CAShapeLayer生成。下面开始实现
记得CAShapeLayer需要设置的fill是要填充颜色的,bounds不需要设置,位置后面动画会给出
作为加载动画的图层,要预先初始化和fill,用懒加载
lazy var aniLayer1:CAShapeLayer = {
() -> CAShapeLayer in
let layee = CAShapeLayer()//
layee.fillColor = UIColor.white.cgColor
return layee
}()
//UIBezierPath生成的圆形的私有方法,其中bgImage就是要添加动画的view
fileprivate func drawPathSize(_ size : CGSize , cornerRadius : CGFloat) -> CGPath{
// 根据一个Rect 画一个圆角矩形曲线 (Radius:圆角半径) 当Rect为正方形时且Radius等于边长一半时画的是一个圆
let path = UIBezierPath(roundedRect:CGRect(x:(((bgImage.bounds.width) - size.width) * 0.5),y:(((bgImage.bounds.height) - size.height) * 0.5),width:size.width,height:size.height),cornerRadius: cornerRadius).cgPath
return path
}
作为加载动画的 UIView,要设置好位置和大小,用懒加载
lazy var bgImage: UIView = {
() -> UIView in
let bImage = UIView()
bImage.backgroundColor = UIColor.blue
bImage.frame = CGRect(x:100,y:100,width:200,height:40)
return bImage
}()
在这里先看看单独给 图层aniLayer1加path,不加动画是怎样的
aniLayer1.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 80, y: 5), size: CGSize(width:30,height:30)), cornerRadius: 15).cgPath
对 aniLayer1的paths赋值UIBezierPath.cgPath就得到圆形,很神奇,如图
![](https://img.haomeiwen.com/i2279620/6f55ca2eeccc4b19.png)
这里插入 图层aniLayer1的 "strokeStart"或者 "strokeEnd" 属性设置的情况,因为以前对这很不清楚,也顺便记录下, aniLayer1需要这样设置
lazy var aniLayer1:CAShapeLayer = {
() -> CAShapeLayer in
let layee = CAShapeLayer()//
layee.fillColor = UIColor.clear.cgColor//中间填充是没有的
layee.strokeColor = UIColor.white.cgColor//设置路径线的颜色,只有设置了 layee.path才有用
layee.lineWidth = 3//设置路径线的宽度,只有设置了 layee.path才有用
return layee
}()
aniLayer1.strokeEnd = 1//添加strokeEnd = 1表示刚好一圈,0.5是半圈
aniLayer1.path = drawPathSize(CGSize(width:30,height:30), cornerRadius: 15)
这样得到的是一个圆环,可以通过设置strokeEnd的值就可以生成环形动画了。如下图
![](https://img.haomeiwen.com/i2279620/9a757faeab0da4d0.gif)
好了,回到波动动画分析上来,从上面的分析就可以联想到如果我做圆从小到大的动画就可以用这个path属性结合相关的UIBezierPath.cgPath就可以做到了 。
所以得到与之配合的动画和UIBezierPath
func addAmimtion1() {
bgImage.layer.addSublayer(aniLayer1)
let add = CABasicAnimation.init(keyPath: "path")//CAShapeLayer.path = UIBezierPath.cgpath路径,根据它(UIBezierPath.cgpath)可以生成相应形状
//起始值
add.fromValue = drawPathSize(CGSize(width:0,height:0), cornerRadius: 0)//初始的圆是圆心有但半径为0的所以这样设置
//变成什么,或者说到哪个值
add.toValue = drawPathSize(CGSize(width:30,height:30), cornerRadius: 15)//最后的圆是半径是15的
add.duration = 1
//重复次数 Float.infinity 一直重复 OC:HUGE_VALF
add.repeatCount = 0
//延时动画开始时间,使用CACurrentMediaTime() + 秒(s)
// animate.beginTime = CACurrentMediaTime() + 2;
add.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
//设置动画的速度变化
/*
kCAMediaTimingFunctionLinear: String 匀速
kCAMediaTimingFunctionEaseIn: String 先慢后快
kCAMediaTimingFunctionEaseOut: String 先快后慢
kCAMediaTimingFunctionEaseInEaseOut: String 两头慢,中间快
kCAMediaTimingFunctionDefault: String 默认效果和上面一个效果极为类似,不易区分
*/
//动画在开始和结束的时候的动作
/*
kCAFillModeForwards 保持在最后一帧,如果想保持在最后一帧,那么isRemovedOnCompletion应该设置为false
kCAFillModeBackwards 将会立即执行第一帧,无论是否设置了beginTime属性
kCAFillModeBoth 该值是上面两者的组合状态
kCAFillModeRemoved 默认状态,会恢复原状
*/
add.fillMode = kCAFillModeForwards
//动画代理,设置为CAAnimationDelegate后,就可以在 func animationDidStop(_ anim: CAAnimation, finished flag: Bool)通过判断anim 水分== aniLayer1.animation(forKey: ani1),对动画进行结束操作
add.delegate = self as CAAnimationDelegate
//动画结束时,是否执行逆向动画
// aii.autoreverses = true
//动画结束是否停留在动画结束的位置
add.isRemovedOnCompletion = false
aniLayer1.add(add, forKey: ani1)//要定义个字符串常量作为动画的唯一标示符 let ani1 = "ani1PathAnimation"
}
这里总结一下:当你要实现实心圆从大到小的变化的,就得加载aniLayer1是fill为某个颜色(不为clear),同时再做CABasicAnimation动画的keypath赋值“path”,变化(fromvalue or tovalue )都为贝塞尔曲线的圆形函数;而当你要实现环形动画就需要加载aniLayer1的fill为clear,同时aniLayer1的strokeColor = UIColor.white.cgColor(设置为某个颜色),aniLayer1的lineWidth = 3(设置为某个宽度值), aniLayer1.path = drawPathSize(CGSize(width:30,height:30), cornerRadius: 15),同时再做CABasicAnimation动画的keypath赋值“strokeEnd”,变化(fromvalue or tovalue )都为数值;
![](https://img.haomeiwen.com/i2279620/67953f5f9b0c50e9.gif)
到此第一阶段的动画就完成了,现在开始第二段,在开始第二段前我们要记得把第一阶段的图层删除添加第二图层的动画,而这个处理就是在func animationDidStop(_ anim: CAAnimation, finished flag: Bool)里做
func animationDidStop(_ anim: CAAnimation, finished flag: Bool){
let ss = aniLayer1.animation(forKey: ani1)
if (anim == ss){
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {//此处使用延迟0.2秒,使动画过渡更自然
self.aniLayer1.removeFromSuperlayer()//删除第一阶段的图层
})
// addAmimtion2()//开始下一段动画
}
}
针对第二图层的动画,实现思路:首先是基于第一个动画的圆的基础上进行再次扩展,同时可以看到中间是空心的,还有透明度是逐渐变淡的,所以要达到那个目的,就得想到怎样可以生成圆环图层,这样自然就想到图层shape初始化是空心(fill = clear)的,再就是设置一定的线宽,为了统一接口,可以用上面第一阶段的UIBezierPath路径生成,配上动画的针对'''path''的变化就可以完成,但这只是圆环向外扩展的实现,还有透明度看起来也是同时进行的,所以再加上个''opacity''的动画,就可以真正实现了,但要统一两个动画并发发动,就要用到CAAnimationGroup,下面看具体实现
func addAmimtion2() {
bgImage.layer.addSublayer(aniLayer)
//形状动画
let saii = CABasicAnimation(keyPath: "path")
saii.fromValue = drawPathSize(CGSize(width:30,height:30), cornerRadius: 15)
saii.toValue = drawPathSize(CGSize(width:60,height:60), cornerRadius: 30)
// saii.fromValue = drawPathSize(CGSize(width:60,height:60), cornerRadius: 30) 低级错误 竟然是把tovalue写成fromvalue
saii.duration = 0.5
saii.repeatCount = nil ?? 0
saii.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
saii.fillMode = kCAFillModeBoth
//saii.delegate = self as CAAnimationDelegate
// aii.autoreverses = true
// saii.isRemovedOnCompletion = false
//
//透明度动画
let sii = CABasicAnimation(keyPath: "opacity")
sii.fromValue = NSNumber(value: 1)
sii.toValue = NSNumber(value: 0.1)
sii.repeatCount = 0
sii.duration = 0.5//记得这个时间必须大于等于包含动画的最大值
sii.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
sii.fillMode = kCAFillModeBoth
//saii.delegate = self as CAAnimationDelegate
// aii.autoreverses = true
// sii.isRemovedOnCompletion = false
let ani2Group = CAAnimationGroup()
ani2Group.delegate = self
ani2Group.animations = [saii,sii]
ani2Group.duration = 0.5
ani2Group.isRemovedOnCompletion = false
aniLayer.add(ani2Group, forKey: ani)
}
lazy var aniLayer:CAShapeLayer = {
() -> CAShapeLayer in
let layee = CAShapeLayer()
layee.fillColor = UIColor.clear.cgColor//这样UIBezierPath的圆角矩形函数配合才是圆环
layee.strokeColor = UIColor.white.cgColor
layee.lineWidth = 8
return layee
}()
![](https://img.haomeiwen.com/i2279620/9abdafc98e8f0e2c.gif)
好,第二段动画讲完,到第三段了,第三段的思路初看起来好像无从下手的,到底是怎样从中间往bgView的两边扩张的呢?可以想想,怎样可以得到最终覆盖bgview的图层,那就是可以用UIBezierPath的矩形函数(rect: CGRect)(当然如果bgview两头是圆形,就用圆角矩形的函数(roundedRect rect: CGRect, cornerRadius: CGFloat)),这样再初始化图层fill填充颜色,再加上为keypath="path"的动画就基本完成了。下面看具体实现。
func addAmimtion3() {
bgImage.layer.addSublayer(aniLayer2)
// aniLayer2.path = drawPathSize(CGSize(width:20,height:bgImage.bounds.size.height), cornerRadius: 5)
//bgImage.bounds.size.width/2)
let add = CABasicAnimation.init(keyPath: "path")
add.fromValue = drawPathSize1(CGSize(width:0,height:bgImage.bounds.size.height), cornerRadius: 20/2)//开始是圆角半径为20/2的矩形
add.toValue = drawPathSize1(bgImage.bounds.size, cornerRadius: bgImage.bounds.size.height/2)//后面是圆角半径为bgImage高度/2的矩形
//
add.duration = 0.8
add.repeatCount = 0
add.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
add.fillMode = kCAFillModeForwards
add.delegate = self as CAAnimationDelegate
// aii.autoreverses = true
add.isRemovedOnCompletion = false
aniLayer2.add(add, forKey: ani2)
}
fileprivate lazy var aniLayer2 : CAShapeLayer = {
let layer = CAShapeLayer()
//填充颜色,给点透明度
layer.fillColor = UIColor(white: 1, alpha: 0.6).cgColor
return layer
}()
![](https://img.haomeiwen.com/i2279620/9d0c0c9e4460727c.gif)
好了,终于到最后一段了,第四段动画加载动画,实际上就是一段圆弧环绕圆心无限旋转,基于此,可以分析,有两个点要清楚,一圆弧怎样生成,怎样让圆弧环绕圆心无限旋转,其实也简单,图层首先就得是fill = clear,有线宽,圆弧形状生成可以用UIBezierPath的以某个中心点画弧线的函数就可以(arcCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool),而圆弧环绕圆心无限旋转,就要用到动画的keypath = "transform.rotation.z",具体实现看下面:
func addAmimtion4() {
bgImage.layer.addSublayer(aniLayer3)
let add = CABasicAnimation.init(keyPath: "transform.rotation.z")
add.fromValue = nil
add.toValue = Double.pi*2
add.duration = 0.5
add.repeatCount = Float.infinity
add.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// add.fillMode = kCAFillModeForwards
add.delegate = self as CAAnimationDelegate
// aii.autoreverses = true
// add.isRemovedOnCompletion = false
aniLayer3.add(add, forKey: ani3)
}
lazy var aniLayer3:CAShapeLayer = {
() -> CAShapeLayer in
let layee = CAShapeLayer()
layee.fillColor = UIColor.clear.cgColor
layee.strokeColor = UIColor.white.cgColor
layee.lineWidth = 3
layee.position = CGPoint(x: (bgImage.bounds.width - bgImage.bounds.height * 0.5) * 0.5, y: bgImage.bounds.height * 0.5)
let ra = bgImage.bounds.size.height/2 - 3
layee.path = UIBezierPath.init(arcCenter: CGPoint(x:0,y:0), radius: 17, startAngle: radius(0), endAngle: radius(60), clockwise: true).cgPath
return layee
}()
![](https://img.haomeiwen.com/i2279620/00fd06bc31f03edf.gif)
好,终于大功告成,当然动画的细节还是要打磨打磨才跟柔和。
网友评论