本文是系列文章的第二篇。git
看过上一篇文章的同窗,已经知道标题中的“景”指代 view,“窗”指代 view.mask,窗景篇就是在梳理 mask 及 mask 动画。若是你还不熟悉 iOS 的 mask,建议先看一下第一篇。github
相对于景来讲,窗的变化更多样一些,因此本文咱们重点来看一下窗的效果。swift
咱们从3个维度来看:窗在动吗?窗在变吗?有几个窗?ide
不少动画就是这3个维度的单独体现,或者组合后的效果。咱们先看一下各个维度的单独效果,而后再来看一下它们的组合效果。布局
前文中,咱们用一个圆做为窗,先贴张图回忆一下:post
咱们大都作过基本的动画,所以能够想到,只要动画地改变圆 mask 的中心位置,就可让窗动起来。动画
效果以下面的动图所示:ui
示意代码以下:spa
/// viewDidLoad
// 景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 圆窗
let mask = CircleView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: 100, y: 100)
self.mask = mask
frontView.mask = mask
// 窗动
startAnimation()
/// startAnimation
// 动画地改变 mask 的中心
private func startAnimation() {
mask.layer.removeAllAnimations()
let anim = CAKeyframeAnimation(keyPath: "position")
let bound = UIScreen.main.bounds
anim.values = [CGPoint(x: 100, y: 250), CGPoint(x:bound.width - 100 , y: 250), CGPoint(x: bound.midX, y: 450), CGPoint(x: 100, y: 250)]
anim.duration = 4
anim.repeatCount = Float.infinity
mask.layer.add(anim, forKey: nil)
}
复制代码
让窗动起来很是简单,这简单的效果也能够成为其余效果的基础。设计
好比咱们加入一个 pan(拖动) 手势,实现这样一个效果:
思路很简单:
示意代码以下:
// 在刚才窗动的代码基础上
// 添加 pan 手势来控制 mask 的 center
@objc func onPan(_ pan: UIPanGestureRecognizer) {
switch pan.state {
case .began:
// 拖动开始,显示窗
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = pan.location(in: pan.view)
case .changed:
// 拖动过程,移动窗
mask.center = pan.location(in: pan.view)
default:
// 其余,隐藏窗
mask.frame = CGRect.zero
}
}
复制代码
好了,“窗动”先看到这,接下来,咱们看一下“窗变”这个维度。
咱们仍是用圆窗示例,此次使用先后两个 view,圆做为 frontView 的 mask;
仍是看一下前文的一张图:
此次咱们让圆窗动态的变大(缩放),缩放也是基本的动画,效果以下面的动图所示:
示意代码以下:
/// viewDidLoad
// back view
backView.frame = UIScreen.main.bounds
view.addSubview(backView)
// 景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 圆窗
let mask = CircleView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
self.mask = mask
frontView.mask = mask
// 窗变
startAnimation()
/// startAnimation
// 动画改变 mask 的大小
private func startAnimation() {
mask.layer.removeAllAnimations()
let scale: CGFloat = 5.0
let anim = CABasicAnimation(keyPath: "transform.scale.xy")
anim.fromValue = 1.0
anim.toValue = scale
anim.duration = 1
mask.layer.add(anim, forKey: nil)
// 真正改变 layer 的 transform,防止动画结束后恢复原状
mask.layer.transform = CATransform3DMakeScale(scale, scale, 1)
}
复制代码
我想你已经发现了,将这个效果和 iOS 转场机制结合起来,就是一种很常见的转场效果。
关于窗变,咱们再举一个常见的例子:进度环效果。 先看一下效果,以下面的动图所示:
其实就是一个渐变的景,加一个圆环的窗,和前文咱们看过的文字窗没有什么区别,以下图所示:
只不过是窗从无逐渐地变化成了完整的圆环;最适合这种变化的,是 stroke 动画。
stroke,也就是 CAShapeLayer 的 strokeStart 和 strokeEnd 属性,网上有成熟的教程
为了方便理解这个效果,本文只对 stroke 作个基本的介绍:
示意代码以下:
/// ViewController
// 渐变景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 环窗
let mask = RingView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
self.mask = mask
frontView.mask = mask
// 窗变
// 动画地改变 mask 圆环的完成度(从圆环未开始,到圆环彻底闭合)
startAnimation()
/// RingView(环 view)
// 为环 view 设置 progress 能够改变 它的 strokeEnd
var progress: CGFloat = 0 {
didSet {
if progress < 0 {
progress = 0
} else if progress > 1 {
progress = 1
} else {}
(layer as! CAShapeLayer).strokeEnd = progress
}
}
复制代码
咱们可使用 CAShapeLayer 的 path 画出各式各样的窗,配合 strokeStart、strokeEnd, 会有不少有趣的 stroke 窗变更画。
接下来,咱们看一下“多窗”这个维度, 因为单纯的多窗没有什么效果,此次咱们直接和“窗动”或者“窗变”组合起来看。
因为 view 只有一个 mask 属性,因此咱们所说的多窗,不是多个 mask,而是在 mask 上作文章。 好比,咱们能够用一种粗糙但直观的方式,来实现这样一个效果:
实现思路以下:
示意代码以下:
/// ViewController
// back view
backView.frame = UIScreen.main.bounds
view.addSubview(backView)
// 景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 多窗(百叶窗)
// mask 的 子 view,依次隐藏
let mask = ShutterView(frame: frontView.bounds, count: 8)
frontView.mask = mask
mask.startAnimation()
/// ShutterView(多窗 view)
func startAnimation() {
layers.enumerated().forEach {
let anim = CABasicAnimation(keyPath: "opacity")
anim.duration = 0.5
anim.beginTime = $0.element.convertTime(CACurrentMediaTime(), from: nil) + anim.duration * Double($0.offset)
anim.fromValue = 1
anim.toValue = 0
// 因为 layer 的动画用 beginTime 作了延迟
// 后面代码修改 opacity 的真实值后,layer 开始就会显示(opacity == 0)的状态
// 因此咱们使用 backwards,来保证动画执行前,layer 显示 fromValue(opacity == 1) 的状态
anim.fillMode = CAMediaTimingFillMode.backwards
$0.element.add(anim, forKey: nil)
// 修改 opacity 的真实值,防止动画完成后恢复原样
CATransaction.begin()
CATransaction.setDisableActions(true)
$0.element.opacity = 0
CATransaction.commit()
}
}
复制代码
以上是“多窗”和“窗变”(透明度变化)的组合,
看到一组相似的小窗,有的同窗可能就想到了 CAReplicatorLayer 这种专精于复制子 layer 的类,
那接下来,咱们用 CAReplicatorLayer 当窗试试,来实现一个“多窗”和“窗动”的组合。
网上已经有 CAReplicatorLayer 的成熟教程,在此咱们只作个简单的类比,让没接触过的同窗有个印象。
CAReplicatorLayer 就比如 UITableView,你能够给它指定一个 subLayer 和 数量,它能够把 subLayer 复制到你指定的数量,就像 UITableView 根据你指定的 Cell 类建立并管理一组 Cell 同样。
UITableView 能够管理 Cell 的布局,可让 Cell 一个接一个的排列,相似地,CAReplicatorLayer 也能够根据你的设置,让 一组 subLayer 按规则地排列。
CAReplicatorLayer 还能够根据设置,让一组 subLayer 有各类过渡效果,好比第一个 subLayer 背景色为白色,中间的subLayer 背景色递减,直到最后一个 subLayer 为黑色。本文的效果只涉及 subLayer 位置,所以再也不讨论其余设置。
本例中,咱们依然用渐变 view 做为景,让 CAReplicatorLayer 复制 3个子 layer(圆) 做为窗,来实现一个 loading 动画,效果以下面的动图所示:
有了前面的经验,你们很容易发现,这个动画就是 3个小圆窗,在渐变景上面不断交换各自的位置,以下图所示:
示意代码以下:
/// ViewController
// 渐变景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 多窗(3球窗,CAReplicatorLayer 窗)
let mask = TriangleLoadingView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
self.mask = mask
frontView.mask = mask
// 窗动(3球旋转)
mask.startAnimation()
/// TriangleLoadingView
// 建立3球窗
override init(frame: CGRect) {
super.init(frame: frame)
let layer = (self.layer as! CAReplicatorLayer)
layer.backgroundColor = UIColor.clear.cgColor
layer.instanceCount = 3
// 3个小球
// 每一个以本 view 的 layer(CAReplicatorLayer)中心为原点,以 z 轴为旋转轴
// 以上一个 cellLayer 的状态 为初始态,顺时针旋转 120°
// 造成一个等边三角形
layer.instanceTransform = CATransform3DMakeRotation(CGFloat.pi / 3 * 2, 0, 0, 1)
layer.addSublayer(cellLayer)
}
// 定位小球
override func layoutSubviews() {
super.layoutSubviews()
// 第1个小球,在本 view 的顶部,水平居中
cellLayer.position = CGPoint(x: bounds.midX, y: Constants.cellRadius)
}
// 执行动画(3球旋转)
func startAnimation() {
cellLayer.removeAllAnimations()
let anim = CABasicAnimation(keyPath: "position")
let from = cellLayer.position
anim.fromValue = from
// 使用一点等边三角形的知识
// r:等边三角形的外径(外接圆的半径)
let r = bounds.midY - Constants.cellRadius
// 根据等边三角形的上顶点的坐标和外径,求右下顶点的坐标
let radian = CGFloat.pi / 6
anim.toValue = CGPoint(x: from.x + r * cos(radian), y: from.y + r + r * sin(radian))
anim.duration = 1
anim.repeatCount = Float.infinity
cellLayer.add(anim, forKey: nil)
// 注:咱们实现了圆窗从上顶点到右下顶点的移动
// CAReplicatorLayer 就能够根据咱们以前设置的 instanceTransform,自动帮咱们完成其余两种顶点间的移动
}
复制代码
看到 CAReplicatorLayer,有的同窗就想到了 CAEmitterLayer,也就是实现粒子效果的 layer, 粒子也能当窗吗?
固然能,一切 view(layer)均可以当作窗,接下来咱们来看一个 CAEmitterLayer 实现的 “多窗”、“窗动”、“窗变” 3个维度的组合。
CAEmitterLayer 的知识咱们也不展开了,直接看一个效果,以下面动图所示:
实现思路很简单:
关于CAEmitterLayer的使用 网上有成熟的教程
示意代码以下:
/// ViewController
// back view
backView.frame = UIScreen.main.bounds
view.addSubview(backView)
// 景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 粒子窗
let mask = EmitterView()
mask.frame = frontView.bounds
frontView.mask = mask
/// EmitterView
/// 配置粒子窗
private func configLayer() {
// 心形粒子
let cell = CAEmitterCell()
// 样式
cell.contents = UIImage(named: "love")?.cgImage
cell.scale = 0.5
cell.scaleSpeed = 2
// 产生粒子的速率
cell.birthRate = 20
// 存活时长
cell.lifetime = 3
// 方向
cell.emissionLongitude = CGFloat(Float.pi / 2)
cell.emissionRange = CGFloat.pi / 3
// 速度
cell.velocity = -250
cell.velocityRange = 50
// 发射器
let emitterLayer = (layer as! CAEmitterLayer)
emitterLayer.emitterPosition = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.height)
emitterLayer.birthRate = 1
emitterLayer.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 0)
emitterLayer.emitterShape = CAEmitterLayerEmitterShape.point
emitterLayer.emitterCells = [cell]
}
复制代码
这一篇,咱们以窗为例,从“窗动”、“窗变”、“多窗” 3个维度入手,梳理了一些 mask 动画的例子。 窗的思路已经打开,那么更为简单的景,咱们就再也不单独开篇。
在下一篇文章里,咱们将一块儿看一个初看复杂、其实简单的效果。文章的重点并非讲效果自己,而是想帮你们回忆起一个道理:看上去复杂的东西,未必就真的复杂。
本文全部示例,在 GitHub 库 里都有完整的代码。
感谢您的阅读,咱们下篇文章见。