本文包含動圖較多,總共大約有10M,移動端請謹慎css
本文示例代碼下載git
Apple Watch 第三代發佈的時候,我借健身的理由入手了一個。除了豐富的各類類型運動數據記錄功能外,令我印象深入的即是定時提醒我呼吸應用裏的那個動畫效果了。本篇文章我將完整地記錄仿製這一動畫的過程,不使用第三方庫。github
圖1 猜一猜哪一個纔是官方的動畫?不着急寫代碼,咱們先仔細多觀察幾遍動畫(下載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
的主要緣由是動畫開頭和中途的停頓,以及花瓣展開和收回所花的時間是不相等的
再看看效果:
圖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]
複製代碼
圖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
。而選擇這個初始弧度是爲了後續添加顏色考慮。
接下來咱們給花瓣上顏色,首先咱們定義兩個顏色變量,表明第一個和最後一個花瓣的顏色:
/// 第一朵花瓣的顏色
/// 設定好第一朵花瓣和最後一朵花瓣的顏色後,若是花瓣數量大於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
複製代碼
圖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.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_…