本文包含动图较多,总共大约有10M,移动端请谨慎css
本文示例代码下载git
Apple Watch 第三代发布的时候,我借健身的理由入手了一个。除了丰富的各类类型运动数据记录功能外,令我印象深入的即是定时提醒我呼吸应用里的那个动画效果了。本篇文章我将完整地记录仿制这一动画的过程,不使用第三方库。github
不着急写代码,咱们先仔细多观察几遍动画(下载gif)。整朵花由6
个圆形花瓣组成,伴随着花的旋转,花瓣慢慢由小变大并从合起状态到彻底展开,整个动画持续时间大约是10秒。不难发现其实动画一共只有这几个步骤:swift
24pt
变大到最终的80pt
6
个方向移动了最大半径(80pt)
的距离2π/3
弧度首先咱们要肯定6个花瓣该如何绘制,最简单办法固然是添加6个子Layer
来画圆,而后依次给它们添加动画效果...等等,这6个圆中心对称,并且动画套路同样...若是你以前熟悉框架自带的各类CALayer
经常使用子类,你确定已经想到了CAReplicatorLayer,它能够依据你预设的图层和配置快速高效地复制出数个几何、时间、颜色规律变换的图层。那么咱们就能够开始自定义视图BreatheView
:bash
class BreathView: UIView {
/// 花瓣数量
var petalCount = 6
/// 花瓣最大半径
var petalMaxRadius: CGFloat = 80
/// 花瓣最小半径
var petalMinRadius: CGFloat = 24
/// 动画总时间
var animationDuration: Double = 10.5
/// 花瓣容器图层
lazy private var containerLayer: CAReplicatorLayer = {
var containerLayer = CAReplicatorLayer()
//指明复制的实例数量
containerLayer.instanceCount = petalCount
//这里是关键,指定每一个"复制"出来的layer的几何变换,这里是按Z轴逆时针旋转 2π/6 弧度
containerLayer.instanceTransform = CATransform3DMakeRotation(-CGFloat.pi * 2 / CGFloat(petalCount), 0, 0, 1)
return containerLayer
}()
//如下为相关初始化方法
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
backgroundColor = UIColor.black
layer.addSublayer(containerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
containerLayer.frame = bounds
}
}
复制代码
接下来建立函数createPetal
,它根据参数花瓣中心点
和半径
返回一个CAShapeLayer
的花瓣:app
private func createPetal(center: CGPoint, radius: CGFloat) -> CAShapeLayer {
let petal = CAShapeLayer()
petal.fillColor = UIColor.white.cgColor
let petalPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0.0, endAngle: CGFloat(2 * Float.pi), clockwise: true)
petal.path = petalPath.cgPath
petal.frame = CGRect(x: 0, y: 0, width: containerLayer.bounds.width, height: containerLayer.bounds.height)
return petal
}
复制代码
新建函数animate()
,调用这个方法就启动动画:框架
func animate() {
//调用createPetal获取花瓣
let petalLayer = createPetal(center: CGPoint(x: containerLayer.bounds.width / 2, y: containerLayer.bounds.height / 2), radius: petalMinRadius)
//添加到containerLayer中
containerLayer.addSublayer(petalLayer)
}
复制代码
最后在ViewController
中实例化BreathView
并添加到视图中, 而后让它显示在屏幕上的时候就开始动画:ide
class ViewController: UIViewController {
let breatheView = BreathView(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.addSubview(breatheView)
}
override func viewDidLayoutSubviews() {
breatheView.frame = view.bounds
}
override func viewDidAppear(_ animated: Bool) {
breatheView.animate()
}
}
复制代码
运行项目看看效果,固然你如今只能看到屏幕中心的一个小白点:函数
图3 咱们的进度很快,主体框架已经搭建完成。接下来开始咱们的第一个动画吧。动画
前面提到过,花瓣展开是各自向6个方向移动了petalMaxRadius
距离。借助ReplicatorLayer
的特性,代码能够很是简单:
//为了看清6个花瓣堆叠的样子,暂时设置0.75的不透明度
petalLayer.opacity = 0.75
//定义展开的关键帧动画
let moveAnimation = CAKeyframeAnimation(keyPath: "position.x")
//values和keyTimes一一对应,各个时刻的属性值
moveAnimation.values = [petalLayer.position.x,
petalLayer.position.x - petalMaxRadius,
petalLayer.position.x - petalMaxRadius,
petalLayer.position.x]
moveAnimation.keyTimes = [0.1, 0.4, 0.5, 0.95]
//定义CAAnimationGroup,组合多个动画同时运行。这不待会还有一个"放大花瓣"嘛
let petalAnimationGroup = CAAnimationGroup()
petalAnimationGroup.duration = animationDuration
petalAnimationGroup.repeatCount = .infinity
petalAnimationGroup.animations = [moveAnimation]
petalLayer.add(petalAnimationGroup, forKey: nil)
复制代码
这里用
CAKeyframeAnimation
的主要缘由是动画开头和中途的停顿,以及花瓣展开和收回所花的时间是不相等的
再看看效果:
熟悉了前面的过程,添加放大效果就很简单了:
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
scaleAnimation.values = [1, petalMaxRadius/petalMinRadius, petalMaxRadius/petalMinRadius, 1]
scaleAnimation.keyTimes = [0.1, 0.4, 0.5, 0.95]
...
//别忘了将 scaleAnimation 添加到动画组中
petalAnimationGroup.animations = [moveAnimation, scaleAnimation]
复制代码
旋转花瓣是经过画布总体旋转实现而不是花瓣自己,也就是如今须要给containerlayer
添加动画:
let rotateAnimation = CAKeyframeAnimation(keyPath: "transform.rotation")
rotateAnimation.duration = animationDuration
rotateAnimation.values = [-CGFloat.pi * 2 / CGFloat(petalCount),
-CGFloat.pi * 2 / CGFloat(petalCount),
CGFloat.pi * 2 / CGFloat(petalCount),
CGFloat.pi * 2 / CGFloat(petalCount),
-CGFloat.pi * 2 / CGFloat(petalCount)]
rotateAnimation.keyTimes = [0, 0.1, 0.4, 0.5, 0.95]
rotateAnimation.repeatCount = .infinity
containerLayer.add(rotateAnimation, forKey: nil)
复制代码
从初始弧度-CGFloat.pi * 2 / CGFloat(petalCount)
旋转到CGFloat.pi * 2 / CGFloat(petalCount)
,正好旋转了2π/3
。而选择这个初始弧度是为了后续添加颜色考虑。
接下来咱们给花瓣上颜色,首先咱们定义两个颜色变量,表明第一个和最后一个花瓣的颜色:
/// 第一朵花瓣的颜色
/// 设定好第一朵花瓣和最后一朵花瓣的颜色后,若是花瓣数量大于2,那么中间花瓣的颜色将根据这两个颜色苹果进行平均过渡
var firstPetalColor: (red: Float, green: Float, blue: Float, alhpa: Float) = (0.17, 0.59, 0.60, 1)
/// 最后一朵花瓣的颜色
var lastPetalColor: (red: Float, green: Float, blue: Float, alhpa: Float) = (0.31, 0.85, 0.62, 1)
复制代码
为何这两个变量的类型不是
UIColor
?由于接下来要根据两个颜色的RGB
算出instanceXXXOffset
,为了演示项目简单才这么处理。不过实际项目中建议使用UIColor
,虽然增长了一些代码反算RGB
的值,可是可让BreathView
的使用者避免困惑
而后更新containerLayer
:
lazy private var containerLayer: CAReplicatorLayer = {
var containerLayer = CAReplicatorLayer()
containerLayer.instanceCount = petalCount
///新增代码---start---
containerLayer.instanceColor = UIColor(red: CGFloat(firstPetalColor.red), green: CGFloat(firstPetalColor.green), blue: CGFloat(firstPetalColor.blue), alpha: CGFloat(firstPetalColor.alpha)).cgColor
containerLayer.instanceRedOffset = (lastPetalColor.red - firstPetalColor.red) / Float(petalCount)
containerLayer.instanceGreenOffset = (lastPetalColor.green - firstPetalColor.green) / Float(petalCount)
containerLayer.instanceBlueOffset = (lastPetalColor.blue - firstPetalColor.blue) / Float(petalCount)
///新增代码----end----
containerLayer.instanceTransform = CATransform3DMakeRotation(-CGFloat.pi * 2 / CGFloat(petalCount), 0, 0, 1)
return containerLayer
}()
复制代码
在上面代码中分别设置了containerLayer
的instanceColor
、instanceRedOffset
、instanceGreenOffset
、instanceBlueOffset
,这样就能使得每一个花瓣的颜色根据这些变量呈现出规律变化的颜色。
我一直觉得复制出来的实例的颜色RGB
各部分是这么算的:
(source * instanceColor) + instanceXXXOffset //source指被添加到CAReplicatorLayer中的layer的颜色,就是文章中petalLayer的背景色
复制代码
其实是这么算的:
source * (instanceColor + instanceXXXOffset)
复制代码
我感受这很是别扭,若是把source
设置为firstPetalColor
,那instanceColor
和instanceXXXOffset
得怎么设置才能最终变化到lastPetalColor
?最后我只能将instanceColor
设置为firstPetalColor
,source
设置为白色才解决问题。
是咱们颜色或者不透明度选错了吗?这并非主要缘由,而是和官方的动画里的颜色混合模式不一致致使的。混合模式是什么?它是指在数字图像编辑中两个图层经过混合各自的颜色做为最终色的方法,通常默认的模式都是采用顶层的颜色。经过观察官方动画比咱们目前的动画亮许多,通过多种模式对比发现应该是滤色模式
。iOS
中,CALayer
有一个compositingFilter属性,经过它咱们能够指定想要的混合模式。
//只要在createPetal()函数中增长这一句便可,指明咱们使用滤色混合模式
petalLayer.compositingFilter = "screenBlendMode"
复制代码
顺便别忘了删除给花瓣添加不透明度的代码,如今咱们不须要了:
petalLayer.opacity = 0.75
复制代码
咱们的动画尚未结束,由于还有花瓣收回的时候有一个残影效果。通过前面动画绘制,相信你已经明白该怎么作了!继续修改咱们的animate()
函数:
let ghostPetalLayer = createPetal(center: CGPoint(x: containerLayer.bounds.width / 2 - petalMaxRadius, y: containerLayer.bounds.height / 2), radius: petalMaxRadius)
containerLayer.addSublayer(ghostPetalLayer)
ghostPetalLayer.opacity = 0.0
let fadeOutAnimation = CAKeyframeAnimation(keyPath: "opacity")
fadeOutAnimation.values = [0, 0.3, 0.0]
fadeOutAnimation.keyTimes = [0.45, 0.5, 0.8]
let ghostScaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
ghostScaleAnimation.values = [1.0, 1.0, 0.78]
ghostScaleAnimation.keyTimes = [0.0, 0.5, 0.8]
let ghostAnimationGroup = CAAnimationGroup()
ghostAnimationGroup.duration = animationDuration
ghostAnimationGroup.repeatCount = .infinity
ghostAnimationGroup.animations = [fadeOutAnimation, ghostScaleAnimation]
ghostPetalLayer.add(ghostAnimationGroup, forKey: nil)
复制代码
咱们建立了一个花瓣影子一样也能够放到已经配置好的containerLayer
中,只要关心它的不透明度和大小在何时变化就行了。运行项目,获得最终效果:
图9 呼吸动画最终效果
本文经过Core Animation
实现了 Apple Watch 的呼吸动画效果。CAReplicatorLayer
和CAKeyframeAnimation
拥有很是强大的建立动画能力,让使用者轻松简单便可绘制出复杂的动画。
资料参考
[1] Geoff Graham,重制Apple Watch呼吸动效, css-tricks.com/recreating-…
[2] Apple, CAReplicatorLayer, developer.apple.com/documentati…
[3] 维基百科,混合模式, en.wikipedia.org/wiki/Blend_…