收錄:原文地址
前言
iOS 的動(dòng)畫(huà)框架很成熟旗国,提供必要的信息枯怖,譬如動(dòng)畫(huà)的起始位置與終止位置,動(dòng)畫(huà)效果就出來(lái)了
動(dòng)畫(huà)的實(shí)現(xiàn)方式挺多的能曾,
有系統(tǒng)提供的簡(jiǎn)單 API 度硝,直接提供動(dòng)畫(huà)般的交互效果。
有手動(dòng)設(shè)置交互效果寿冕,看起來(lái)像是動(dòng)畫(huà)蕊程,一般要用到插值。
至于動(dòng)畫(huà)框架驼唱,有 UIView 級(jí)別的藻茂,有功能強(qiáng)勁的 CALayer 級(jí)別的動(dòng)畫(huà)。
CALayer 級(jí)別的動(dòng)畫(huà)通過(guò)靈活設(shè)置的 CoreAnimation玫恳,CoreAnimation 的常規(guī)操作辨赐,就是自定義路徑
當(dāng)然有蘋(píng)果推了幾年的 UIViewPropertyAnimator, 動(dòng)畫(huà)可交互性做得比較好京办;
話(huà)不多說(shuō)掀序;直接來(lái)看案例
例子一:導(dǎo)航欄動(dòng)畫(huà)
navigationController?.hidesBarsOnSwipe = true
簡(jiǎn)單設(shè)置 hidesBarsOnSwipe
屬性,就可以了臂港。
該屬性森枪,除了可以調(diào)節(jié)頭部導(dǎo)航欄进栽,還可以調(diào)節(jié)底部標(biāo)簽工具欄 toolbar
例子二:屏幕開(kāi)鎖效果
一眼看起來(lái)有點(diǎn)炫楷扬,實(shí)際設(shè)置很簡(jiǎn)單
func openLock() {
UIView.animate(withDuration: 0.4, delay: 1.0, options: [], animations: {
// Rotate keyhole.
self.lockKeyhole.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: { _ in
UIView.animate(withDuration: 0.5, delay: 0.2, options: [], animations: {
// Open lock.
let yDelta = self.lockBorder.frame.maxY
self.topLock.center.y -= yDelta
self.lockKeyhole.center.y -= yDelta
self.lockBorder.center.y -= yDelta
self.bottomLock.center.y += yDelta
}, completion: { _ in
self.topLock.removeFromSuperview()
self.lockKeyhole.removeFromSuperview()
self.lockBorder.removeFromSuperview()
self.bottomLock.removeFromSuperview()
})
})
}
總共有四個(gè)控件废岂,先讓中間的鎖控件旋轉(zhuǎn)一下享扔,然后對(duì)四個(gè)控件,做移位操作
用簡(jiǎn)單的關(guān)鍵幀動(dòng)畫(huà)式散,處理要優(yōu)雅一點(diǎn)
例子三:地圖定位波動(dòng)
看上去有些眼花的動(dòng)畫(huà)筋遭,可以分解為三個(gè)動(dòng)畫(huà)
一波未平,一波又起暴拄,做一個(gè)動(dòng)畫(huà)效果的疊加漓滔,就成了動(dòng)畫(huà)的第一幅動(dòng)畫(huà)
一個(gè)動(dòng)畫(huà)波動(dòng)效果,效果用到了透明度的變化乖篷,還有范圍的變化
范圍的變化响驴,用的就是 CoreAnimation 的路徑 path
CoreAnimation 簡(jiǎn)單設(shè)置,就是指明 from 撕蔼、to豁鲤,動(dòng)畫(huà)的起始狀態(tài),和動(dòng)畫(huà)終止?fàn)顟B(tài)鲸沮,然后選擇使用哪一種動(dòng)畫(huà)效果琳骡。
動(dòng)畫(huà)的起始狀態(tài),一般是起始位置讼溺。簡(jiǎn)單的動(dòng)畫(huà)楣号,就是讓他動(dòng)起來(lái)
func sonar(_ beginTime: CFTimeInterval) {
let circlePath1 = UIBezierPath(arcCenter: self.center, radius: CGFloat(3), startAngle: CGFloat(0), endAngle:CGFloat.pi * 2, clockwise: true)
let circlePath2 = UIBezierPath(arcCenter: self.center, radius: CGFloat(80), startAngle: CGFloat(0), endAngle:CGFloat.pi * 2, clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = ColorPalette.green.cgColor
shapeLayer.fillColor = ColorPalette.green.cgColor
shapeLayer.path = circlePath1.cgPath
self.layer.addSublayer(shapeLayer)
// 兩個(gè)動(dòng)畫(huà)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = circlePath1.cgPath
pathAnimation.toValue = circlePath2.cgPath
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 0.8
alphaAnimation.toValue = 0
// 組動(dòng)畫(huà)
let animationGroup = CAAnimationGroup()
animationGroup.beginTime = beginTime
animationGroup.animations = [pathAnimation, alphaAnimation]
// 時(shí)間有講究
animationGroup.duration = 2.76
// 不斷重復(fù)
animationGroup.repeatCount = Float.greatestFiniteMagnitude
animationGroup.isRemovedOnCompletion = false
animationGroup.fillMode = CAMediaTimingFillMode.forwards
// Add the animation to the layer.
// key 用來(lái) debug
shapeLayer.add(animationGroup, forKey: "sonar")
}
波動(dòng)效果調(diào)用了三次
func startAnimation() {
// 三次動(dòng)畫(huà),效果合成怒坯,
sonar(CACurrentMediaTime())
sonar(CACurrentMediaTime() + 0.92)
sonar(CACurrentMediaTime() + 1.84)
}
例子四:加載動(dòng)畫(huà)
這是 UIView 框架自帶的動(dòng)畫(huà)炫狱,看起來(lái)不錯(cuò),就是做了一個(gè)簡(jiǎn)單的縮放敬肚,通過(guò) transform
屬性做仿射變換
func startAnimation() {
dotOne.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
dotTwo.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
dotThree.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
// 三個(gè)不同的 delay, 漸進(jìn)時(shí)間
UIView.animate(withDuration: 0.6, delay: 0.0, options: [.repeat, .autoreverse], animations: {
self.dotOne.transform = CGAffineTransform.identity
}, completion: nil)
UIView.animate(withDuration: 0.6, delay: 0.2, options: [.repeat, .autoreverse], animations: {
self.dotTwo.transform = CGAffineTransform.identity
}, completion: nil)
UIView.animate(withDuration: 0.6, delay: 0.4, options: [.repeat, .autoreverse], animations: {
self.dotThree.transform = CGAffineTransform.identity
}, completion: nil)
}
例子五:下劃線點(diǎn)擊轉(zhuǎn)移動(dòng)畫(huà)
這個(gè)也是 UIView 的動(dòng)畫(huà)
動(dòng)畫(huà)的實(shí)現(xiàn)效果毕荐,是通過(guò)更改約束束析。
約束動(dòng)畫(huà)要注意的是艳馒,確保動(dòng)畫(huà)的起始位置準(zhǔn)確,起始的時(shí)候员寇,一般要調(diào)用其父視圖的 layoutIfNeeded
方法弄慰,確保視圖的實(shí)際位置與約束設(shè)置的一致。
這里的約束動(dòng)畫(huà)蝶锋,是通過(guò) NSLayoutAnchor
做得陆爽。
一般我們用的是 SnapKit 設(shè)置約束,調(diào)用也差不多扳缕。
func animateContraintsForUnderlineView(_ underlineView: UIView, toSide: Side) {
switch toSide {
case .left:
for constraint in underlineView.superview!.constraints {
if constraint.identifier == ConstraintIdentifiers.centerRightConstraintIdentifier {
constraint.isActive = false
let leftButton = optionsBar.arrangedSubviews[0]
let centerLeftConstraint = underlineView.centerXAnchor.constraint(equalTo: leftButton.centerXAnchor)
centerLeftConstraint.identifier = ConstraintIdentifiers.centerLeftConstraintIdentifier
NSLayoutConstraint.activate([centerLeftConstraint])
}
}
case .right:
for constraint in underlineView.superview!.constraints {
if constraint.identifier == ConstraintIdentifiers.centerLeftConstraintIdentifier {
// 先失效慌闭,舊的約束
constraint.isActive = false
// 再新建約束别威,并激活
let rightButton = optionsBar.arrangedSubviews[1]
let centerRightConstraint = underlineView.centerXAnchor.constraint(equalTo: rightButton.centerXAnchor)
centerRightConstraint.identifier = ConstraintIdentifiers.centerRightConstraintIdentifier
NSLayoutConstraint.activate([centerRightConstraint])
}
}
}
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [], animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
例子六:列表視圖的頭部拉伸效果
這個(gè)沒(méi)有用到動(dòng)畫(huà)框架,就是做了一個(gè)交互插值
就是補(bǔ)插連續(xù)的函數(shù) scrollViewDidScroll
, 及時(shí)更新列表視圖頭部的位置驴剔、尺寸
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateHeaderView()
}
func updateHeaderView() {
var headerRect = CGRect(x: 0, y: -tableHeaderHeight, width: tableView.bounds.width, height: tableHeaderHeight)
// 決定拉動(dòng)的方向
if tableView.contentOffset.y < -tableHeaderHeight {
// 就是改 frame
headerRect.origin.y = tableView.contentOffset.y
headerRect.size.height = -tableView.contentOffset.y
}
headerView.frame = headerRect
}
例子七:進(jìn)度繪制動(dòng)畫(huà)
用到了 CoreAnimation省古,也用到了插值。
每一段插值都是一個(gè) CoreAnimation 動(dòng)畫(huà)丧失,進(jìn)度的完成分為多次插值豺妓。
這里動(dòng)畫(huà)效果的主要用到 strokeEnd
屬性, 筆畫(huà)結(jié)束
插值的時(shí)候,要注意布讹,下一段動(dòng)畫(huà)的開(kāi)始琳拭,正是上一段動(dòng)畫(huà)的結(jié)束
// 這個(gè)用來(lái),主要的效果
let progressLayer = CAShapeLayer()
// 這個(gè)用來(lái)描验,附加的顏色
let gradientLayer = CAGradientLayer()
// 給個(gè)默認(rèn)值白嘁,外部設(shè)置
var range: CGFloat = 128
var curValue: CGFloat = 0 {
didSet {
animateStroke()
}
}
func setupLayers() {
progressLayer.position = CGPoint.zero
progressLayer.lineWidth = 3.0
progressLayer.strokeEnd = 0.0
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.black.cgColor
let radius = CGFloat(bounds.height/2) - progressLayer.lineWidth
let startAngle = CGFloat.pi * (-0.5)
let endAngle = CGFloat.pi * 1.5
let width = bounds.width
let height = bounds.height
let modelCenter = CGPoint(x: width / 2, y: height / 2)
let path = UIBezierPath(arcCenter: modelCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
// 指定路徑
progressLayer.path = path.cgPath
layer.addSublayer(progressLayer)
// 有一個(gè)漸變
gradientLayer.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
// teal, 藍(lán)綠色
gradientLayer.colors = [ColorPalette.teal.cgColor, ColorPalette.orange.cgColor, ColorPalette.pink.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
gradientLayer.mask = progressLayer // Use progress layer as mask for gradient layer.
layer.addSublayer(gradientLayer)
}
func animateStroke() {
// 前一段的終點(diǎn)
let fromValue = progressLayer.strokeEnd
let toValue = curValue / range
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = fromValue
animation.toValue = toValue
progressLayer.add(animation, forKey: "stroke")
progressLayer.strokeEnd = toValue
}
}
// 動(dòng)畫(huà)路徑,結(jié)合插值
例子八:漸變動(dòng)畫(huà)
這個(gè)漸變動(dòng)畫(huà)膘流,主要用到了漸變圖層 CAGradientLayer
的 locations
位置屬性权薯,用來(lái)調(diào)整漸變區(qū)域的分布
另一個(gè)關(guān)鍵點(diǎn)是用了圖層 CALayer
的遮罩 mask
,
簡(jiǎn)單理解,把漸變圖層全部蒙起來(lái)睡扬,只露出文本的形狀盟蚣,就是那幾個(gè)字母的痕跡
class LoadingLabel: UIView {
let 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.gray.cgColor, UIColor.white.cgColor, UIColor.gray.cgColor]
gradientLayer.colors = colors
let locations = [0.25, 0.5, 0.75]
gradientLayer.locations = locations as [NSNumber]?
return gradientLayer
}()
// 文字轉(zhuǎn)圖片屎开,然后繪制到視圖上
// 通過(guò)設(shè)置漸變圖層的遮罩 `mask` , 為指定文字,來(lái)設(shè)置漸變閃爍的效果
@IBInspectable var text: String! {
didSet {
setNeedsDisplay()
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
text.draw(in: bounds, withAttributes: textAttributes)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// 從文字中马靠,抽取圖片
let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image?.cgImage
gradientLayer.mask = maskLayer
}
}
// 設(shè)置位置與尺寸
override func layoutSubviews() {
gradientLayer.frame = CGRect(x: -bounds.size.width, y: bounds.origin.y, width: 2 * bounds.size.width, height: bounds.size.height)
}
override func didMoveToWindow() {
super.didMoveToWindow()
layer.addSublayer(gradientLayer)
let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.75, 1.0, 1.0]
gradientAnimation.duration = 1.7
// 一直循環(huán)
gradientAnimation.repeatCount = Float.infinity
gradientAnimation.isRemovedOnCompletion = false
gradientAnimation.fillMode = CAMediaTimingFillMode.forwards
gradientLayer.add(gradientAnimation, forKey: nil)
}
}
例子九:下拉刷新動(dòng)畫(huà)
首先通過(guò)方法 scrollViewDidScroll
和 scrollViewWillEndDragging
做插值
extension PullRefreshView: UIScrollViewDelegate{
// MARK: - UIScrollViewDelegate
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)
// 做互斥的狀態(tài)管理
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if !isRefreshing && self.progress >= 1.0 {
delegate?.PullRefreshViewDidRefresh(self)
beginRefreshing()
}
}
}
畫(huà)面中飛碟動(dòng)來(lái)動(dòng)去奄抽,是通過(guò) CAKeyframeAnimation(keyPath: "position")
,關(guān)鍵幀動(dòng)畫(huà)的位置屬性甩鳄,設(shè)置的
func redrawFromProgress(_ progress: CGFloat) {
/* PART 1 ENTER ANIMATION */
let enterPath = paths.start
// 動(dòng)畫(huà)指定路徑走
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = enterPath.cgPath
pathAnimation.calculationMode = CAAnimationCalculationMode.paced
pathAnimation.timingFunctions = [CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)]
pathAnimation.beginTime = 1e-100
pathAnimation.duration = 1.0
pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
pathAnimation.isRemovedOnCompletion = false
pathAnimation.fillMode = CAMediaTimingFillMode.forwards
flyingSaucerLayer.add(pathAnimation, forKey: nil)
flyingSaucerLayer.position = enterPath.currentPoint
let sizeAlongEnterPathAnimation = CABasicAnimation(keyPath: "transform.scale")
sizeAlongEnterPathAnimation.fromValue = 0
sizeAlongEnterPathAnimation.toValue = progress
sizeAlongEnterPathAnimation.beginTime = 1e-100
sizeAlongEnterPathAnimation.duration = 1.0
sizeAlongEnterPathAnimation.isRemovedOnCompletion = false
sizeAlongEnterPathAnimation.fillMode = CAMediaTimingFillMode.forwards
flyingSaucerLayer.add(sizeAlongEnterPathAnimation, forKey: nil)
}
// 設(shè)置路徑
func customPaths(frame: CGRect = CGRect(x: 4, y: 3, width: 166, height: 74)) -> ( UIBezierPath, UIBezierPath) {
// 兩條路徑
let startY = 0.09459 * frame.height
let enterPath = UIBezierPath()
// ...
enterPath.addCurve(to: CGPoint(x: frame.minX + 0.21694 * frame.width, y: frame.minY + 0.85855 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.04828 * frame.width, y: frame.minY + 0.68225 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.21694 * frame.width, y: frame.minY + 0.85855 * frame.height))
enterPath.addCurve(to: CGPoint(x: frame.minX + 0.36994 * frame.width, y: frame.minY + 0.92990 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.21694 * frame.width, y: frame.minY + 0.85855 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.33123 * frame.width, y: frame.minY + 0.93830 * frame.height))
// ...
enterPath.usesEvenOddFillRule = true
let exitPath = UIBezierPath()
exitPath.move(to: CGPoint(x: frame.minX + 0.98193 * frame.width, y: frame.minY + 0.15336 * frame.height))
exitPath.addLine(to: CGPoint(x: frame.minX + 0.51372 * frame.width, y: frame.minY + 0.28558 * frame.height))
// ...
exitPath.miterLimit = 4
exitPath.usesEvenOddFillRule = true
return (enterPath, exitPath)
}
}
這個(gè)動(dòng)畫(huà)比較復(fù)雜逞度,需要做大量的數(shù)學(xué)計(jì)算,還要調(diào)試妙啃,具體看文尾的 git repo.
一般這種動(dòng)畫(huà)档泽,我們用 Lottie
例子十:文本變換動(dòng)畫(huà)
這個(gè)動(dòng)畫(huà)有些復(fù)雜,重點(diǎn)使用了 CoreAnimation 的組動(dòng)畫(huà)揖赴,疊加了五種效果馆匿,縮放、尺寸燥滑、布局渐北、位置與透明度。
具體看文尾的 git repo.
class func animation(_ layer: CALayer, duration: TimeInterval, delay: TimeInterval, animations: (() -> ())?, completion: ((_ finished: Bool)-> ())?) {
let animation = CLMLayerAnimation()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) {
var animationGroup: CAAnimationGroup?
let oldLayer = self.animatableLayerCopy(layer)
animation.completionClosure = completion
if let layerAnimations = animations {
CATransaction.begin()
CATransaction.setDisableActions(true)
layerAnimations()
CATransaction.commit()
}
animationGroup = groupAnimationsForDifferences(oldLayer, newLayer: layer)
if let differenceAnimation = animationGroup {
differenceAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
differenceAnimation.duration = duration
differenceAnimation.beginTime = CACurrentMediaTime()
layer.add(differenceAnimation, forKey: nil)
}
else {
if let completion = animation.completionClosure {
completion(true)
}
}
}
}
class func groupAnimationsForDifferences(_ oldLayer: CALayer, newLayer: CALayer) -> CAAnimationGroup? {
var animationGroup: CAAnimationGroup?
var animations = [CABasicAnimation]()
// 疊加了五種效果
if !CATransform3DEqualToTransform(oldLayer.transform, newLayer.transform) {
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: oldLayer.transform)
animation.toValue = NSValue(caTransform3D: newLayer.transform)
animations.append(animation)
}
if !oldLayer.bounds.equalTo(newLayer.bounds) {
let animation = CABasicAnimation(keyPath: "bounds")
animation.fromValue = NSValue(cgRect: oldLayer.bounds)
animation.toValue = NSValue(cgRect: newLayer.bounds)
animations.append(animation)
}
if !oldLayer.frame.equalTo(newLayer.frame) {
let animation = CABasicAnimation(keyPath: "frame")
animation.fromValue = NSValue(cgRect: oldLayer.frame)
animation.toValue = NSValue(cgRect: newLayer.frame)
animations.append(animation)
}
if !oldLayer.position.equalTo(newLayer.position) {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(cgPoint: oldLayer.position)
animation.toValue = NSValue(cgPoint: newLayer.position)
animations.append(animation)
}
if oldLayer.opacity != newLayer.opacity {
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = oldLayer.opacity
animation.toValue = newLayer.opacity
animations.append(animation)
}
if animations.count > 0 {
animationGroup = CAAnimationGroup()
animationGroup!.animations = animations
}
return animationGroup
}
例子十一:動(dòng)態(tài)圖動(dòng)畫(huà)
從 gif 文件里面取出每楨圖片铭拧,算出持續(xù)時(shí)間赃蛛,設(shè)置動(dòng)畫(huà)圖片
internal class func animatedImageWithSource(_ source: CGImageSource) -> UIImage? {
// 需要喂圖片恃锉,
// 喂動(dòng)畫(huà)持續(xù)時(shí)間
let count = CGImageSourceGetCount(source)
var data: (images: [CGImage], delays: [Int]) = ([CGImage](), [Int]())
// Fill arrays
for i in 0..<count {
// Add image
if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
data.images.append(image)
}
let delaySeconds = UIImage.delayForImageAtIndex(Int(i),
source: source)
data.delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
}
// Calculate full duration
let duration: Int = {
var sum = 0
for val: Int in data.delays {
sum += val
}
return sum
}()
let gcd = gcdForArray(data.delays)
var frames = [UIImage]()
var frame: UIImage
var frameCount: Int
for i in 0..<count {
frame = UIImage(cgImage: data.images[Int(i)])
frameCount = Int(data.delays[Int(i)] / gcd)
for _ in 0..<frameCount {
frames.append(frame)
}
}
let animation = UIImage.animatedImage(with: frames,
duration: Double(duration) / 1000.0)
return animation
}