APPLE WATCH 的呼吸动效是怎么实现的?

栏目: IOS · 发布时间: 5年前

内容简介:本文包含动图较多,总共大约有10M,移动端请谨慎Apple Watch 第三代发布的时候,我借健身的理由入手了一个。除了丰富的各种类型运动数据记录功能外,令我印象深刻的便是定时提醒我呼吸应用里的那个动画效果了。本篇文章我将完整地记录仿制这一动画的过程,不使用第三方库。

本文包含动图较多,总共大约有10M,移动端请谨慎

本文示例代码下载

Apple Watch 第三代发布的时候,我借健身的理由入手了一个。除了丰富的各种类型运动数据记录功能外,令我印象深刻的便是定时提醒我呼吸应用里的那个动画效果了。本篇文章我将完整地记录仿制这一动画的过程,不使用第三方库。

APPLE WATCH 的呼吸动效是怎么实现的?

图1 猜一猜哪个才是官方的动画?

实现分析

不着急写代码,我们先仔细多观察几遍动画 (下载gif) 。整朵花由 6 个圆形花瓣组成,伴随着花的旋转,花瓣慢慢由小变大并从合起状态到完全展开,整个动画持续时间大约是10秒。不难发现其实动画一共只有这几个步骤:

  1. 花瓣变大,花瓣半径从最小的 24pt 变大到最终的 80pt

  2. 花瓣展开,表现为花瓣圆点从画布中心向 6 个方向移动了最大半径 (80pt) 的距离

  3. 整体旋转,整个画布在花瓣展开过程中旋转了 2π/3 弧度

APPLE WATCH 的呼吸动效是怎么实现的?

图2 花瓣展开方式

代码实现

总体框架

首先我们要确定6个花瓣该如何绘制,最简单办法当然是添加6个子 Layer 来画圆,然后依次给它们添加动画效果...等等,这6个圆中心对称,而且动画套路一样...如果你之前熟悉框架自带的各种 CALayer 常用子类,你肯定已经想到了 CAReplicatorLayer ,它可以依据你预设的图层和配置快速高效地复制出数个几何、时间、颜色规律变换的图层。那么我们就可以开始自定义视图 BreatheView

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 的花瓣:

private func createPetal(center: CGPoint, radius: CGFloat) -> CAShapeLayer {let petal = CAShapeLayer()
    petal.fillColor = UIColor.white.cgColorlet 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 并添加到视图中, 然后让它显示在屏幕上的时候就开始动画:

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()
    }
}复制代码

运行项目看看效果,当然你现在只能看到屏幕中心的一个小白点:

APPLE WATCH 的呼吸动效是怎么实现的?

图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 的主要原因是动画开头和中途的停顿,以及花瓣展开和收回所花的时间是不相等的

再看看效果:

APPLE WATCH 的呼吸动效是怎么实现的?

图4 花瓣展开的过程中没有放大导致有点偏差

放大花瓣

熟悉了前面的过程,添加放大效果就很简单了:

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]复制代码
APPLE WATCH 的呼吸动效是怎么实现的?

图5 花瓣展开现在正常了

旋转花瓣

旋转花瓣是通过画布整体旋转实现而不是花瓣本身,也就是现在需要给 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 。而选择这个初始弧度是为了后续添加颜色考虑。

APPLE WATCH 的呼吸动效是怎么实现的?

图6 太棒了,我们的花瓣开了又开

添加颜色

接下来我们给花瓣上颜色,首先我们定义两个颜色变量,代表第一个和最后一个花瓣的颜色:

/// 第一朵花瓣的颜色/// 设定好第一朵花瓣和最后一朵花瓣的颜色后,如果花瓣数量大于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
}()复制代码

在上面代码中分别设置了 containerLayerinstanceColorinstanceRedOffsetinstanceGreenOffsetinstanceBlueOffset ,这样就能使得每个花瓣的颜色根据这些变量呈现出规律变化的颜色。

我一直以为复制出来的实例的颜色 RGB 各部分是这么算的:

(source * instanceColor) + instanceXXXOffset //source指被添加到CAReplicatorLayer中的layer的颜色,就是文章中petalLayer的背景色复制代码

实际上是这么算的:

source * (instanceColor + instanceXXXOffset)复制代码

我感觉这非常别扭,如果把 source 设置为 firstPetalColor ,那 instanceColorinstanceXXXOffset 得怎么设置才能最终变化到 lastPetalColor ?最后我只能将 instanceColor 设置为 firstPetalColorsource 设置为白色才解决问题。

APPLE WATCH 的呼吸动效是怎么实现的?

图7 这颜色差别有点大啊

是我们颜色或者不透明度选错了吗?这并不是主要原因,而是和官方的动画里的颜色混合模式不一致导致的。 混合模式 是什么?它是指在数字图像编辑中两个图层通过混合各自的颜色作为最终色的方法,一般默认的模式都是采用顶层的颜色。通过观察官方动画比我们目前的动画亮许多,经过多种模式对比发现应该是 滤色模式iOS 中, CALayer 有一个 compositingFilter 属性,通过它我们可以指定想要的混合模式。

//只要在createPetal()函数中增加这一句即可,指明我们使用滤色混合模式petalLayer.compositingFilter = "screenBlendMode"复制代码

顺便别忘了删除给花瓣添加不透明度的代码,现在我们不需要了:

petalLayer.opacity = 0.75复制代码
APPLE WATCH 的呼吸动效是怎么实现的?

图8 滤色混合模式使得画面更加明亮

画龙点睛

我们的动画还没有结束,因为还有花瓣收回的时候有一个残影效果。经过前面动画绘制,相信你已经明白该怎么做了!继续修改我们的 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.0let 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 中,只要关心它的不透明度和大小在什么时候变化就好了。运行项目,得到最终效果:

APPLE WATCH 的呼吸动效是怎么实现的?

图9 呼吸动画最终效果


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深度探索C++对象模型

深度探索C++对象模型

[美] Stanley B. Lippman / 侯捷 / 华中科技大学出版社 / 2001-5 / 54.00元

这本书探索“对象导向程序所支持的C++对象模型”下的程序行为。对于“对象导向性质之基础实现技术”以及“各种性质背后的隐含利益交换”提供一个清楚的认识。检验由程序变形所带来的效率冲击。提供丰富的程序范例、图片,以及对象导向观念和底层对象模型之间的效率测量。一起来看看 《深度探索C++对象模型》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换