引言
之前的博客我们已经提到过了模型图层和呈现图层,但是问题凸显的可能还是不够具象,本篇博客我们就来通过一个非常具体的案例来感受一下模型图层(Model Layer)和呈现图层(Presentation Layer),并且帮助你理解这 两个图层的区别,这对正确处理动画和截图至关重要。
截图问题
在实际开发中经常会遇到一个总结型的视图,我们就以驾驶安全类的APP为例,比如它会有一个每周的驾驶周报,然后周报上会有一个驾驶的评分视图,评分往往会以半圆的进度条显示并且伴随着动画从0开始到指定分数。
接着它还会有分享按钮,让用户可以将自己的周报分享给自己的好友或者其它社交APP,分享的时候我们就需要将当前的画面生成一整图片然后以卡片的形式分享给其他用户。
在实际开发中,虽然动画可能完成的很好,但是当截图分享时可能会出现这样的现象,明明是90分,画面上的进度也是90,可是分享出去的卡片进度确实100。这可能会让人有点摸不到头脑。
模型图层和呈现图层
在Core Animation中,每个CALayer对象都有两个状态:模型图层和呈现图层。
- 模型图层(Model Layer):模型图层代表图层的目标状态,是开发者在代码中直接修改的图层属性。这些属性包括位置、大小、颜色等。模型图层反映的是动画最终应该达到的状态。
- 呈现图层(Presentation Layer):呈现图层代表图层的当前状态,是正在屏幕上显示的内容。它反映了动画在某一时刻的实际状态。呈现图层是由Core Animation在后台自动管理的,用于在动画进行时展示图层的实时位置和属性。
为了更好地理解模型图层和呈现图层之间的区别,让我们来看一个简单的代码示例:
// 创建一个CALayer并设置初始位置
let layer = CALayer()
layer.position = CGPoint(x: 50, y: 50)
self.view.layer.addSublayer(layer)
// 动画移动图层到新位置
CATransaction.begin()
CATransaction.setAnimationDuration(2.0)
layer.position = CGPoint(x: 200, y: 200)
CATransaction.commit()
// 打印模型图层和呈现图层的位置
print("Model Layer Position: \(layer.position)")
if let presentationLayer = layer.presentation() {
print("Presentation Layer Position: \(presentationLayer.position)")
}
在这个例子中,我们创建了一个CALayer
对象,并将其初始位置设置为(50, 50)。然后,我们在2秒内将它移动到(200, 200)。在动画进行中,打印模型图层和呈现图层的位置,会发现:
- **模型图层(Model Layer)**的位置已经是目标位置(200, 200)。
- **呈现图层(Presentation Layer)**的位置在动画过程中会不断变化,反映动画的实时状态。
在实际开发中,理解模型图层和呈现图层的区别非常重要。例如:
- 截图问题:在动画过程中截图时,如果不使用呈现图层,截到的可能是模型图层的状态,而不是动画的实时状态。为了确保截图与看到的动画一致,我们需要使用呈现图层。
- 调试动画:调试动画时,查看呈现图层的属性可以帮助我们理解动画的实时行为,找出动画不如预期的原因。
创建动画显示分数视图
那我们就还以驾驶评分为例,首先我们来构建一个简单的名为PHScoreView的评分视图,代码如下:
1.基本属性:
/// 分数
var score: Int = 0 {
didSet {
self.scoreLabel.text = "\(score)"
duration = 2 * TimeInterval(score) / 100
drawPath()
}
}
/// 动画时间
var duration: TimeInterval = 2
/// 分数
private let scoreLabel = UILabel()
/// 渐变背景
private let gradientLayer = CAGradientLayer()
/// shapeLayer
private let shapeLayer = CAShapeLayer()
/// 路径
private var path = UIBezierPath()
2.添加图层:
override init(frame: CGRect) {
super.init(frame: frame)
addScoreLabel()
addGradientLayer()
}
private func addScoreLabel() {
scoreLabel.textAlignment = .center
scoreLabel.font = UIFont.systemFont(ofSize: 50, weight: .ultraLight)
scoreLabel.textColor = .black
addSubview(scoreLabel)
}
private func addGradientLayer() {
gradientLayer.frame = bounds
gradientLayer.colors = [UIColor.green.cgColor,UIColor.orange.cgColor,UIColor.red.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
layer.addSublayer(gradientLayer)
gradientLayer.mask = shapeLayer
}
3.绘制进度条:
/// 画一个半圆
private func drawPath() {
// 线宽
let lineWidth: CGFloat = 20
let radius = bounds.width / 2 - lineWidth / 2
path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height), radius: radius, startAngle: CGFloat.pi, endAngle: 0, clockwise: true)
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 20.0
}
4.开始动画:
func startAnimation() {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = CGFloat(score) / 100
animation.duration = duration
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
shapeLayer.add(animation, forKey: nil)
scoreLabel.text = "\(score)"
}
5.重写layoutSubviews:
override func layoutSubviews() {
super.layoutSubviews()
scoreLabel.frame = CGRect(x: 0.0, y: self.bounds.size.height - 60.0, width: self.bounds.size.width, height: 60.0)
gradientLayer.frame = bounds
drawPath()
}
6.生成截图:
/// 生成截图
func snapshotImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
一个简单的动画显示评分的视图就创建好了,接下来我们开始使用它。
在ViewController中,创建了一个开始按钮,截图按钮,还有刚刚分数的视图以及一个显示截图的UIImageView,代码如下:
/// 开始按钮
private let startButton = UIButton()
/// 截图按钮
private let screenShotButton = UIButton()
/// 分数视图
private let scoreView = PHScoreView()
/// 显示截图
private let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
addScoreView()
addScreenImageView()
addStartButton()
addScreenShotButton()
}
func addScoreView() {
let scoreViewWidth = 300
let scoreViewHeight = 150
let scoreViewX = (Int(view.frame.width) - scoreViewWidth) / 2
let scoreViewY = 100
self.view.addSubview(scoreView)
scoreView.frame = CGRect(x: scoreViewX, y: scoreViewY, width: scoreViewWidth, height: scoreViewHeight)
}
func addScreenImageView() {
let imageViewWidth = 300
let imageViewHeight = 150
let imageViewX = (Int(view.frame.width) - imageViewWidth) / 2
let imageViewY = 400
self.view.addSubview(imageView)
imageView.frame = CGRect(x: imageViewX, y: imageViewY, width: imageViewWidth, height: imageViewHeight)
}
func addStartButton() {
let buttonWidth = 100
let buttonHeight = 50
let buttonX = (Int(view.frame.width) - buttonWidth) / 2
self.view.addSubview(startButton)
startButton.frame = CGRect(x: buttonX, y: 600, width: buttonWidth, height: buttonHeight)
startButton.setTitle("开始", for: .normal)
startButton.setTitleColor(.black, for: .normal)
startButton.backgroundColor = .yellow
startButton.addTarget(self, action: #selector(startButtonAction), for: .touchUpInside)
startButton.layer.cornerRadius = 10
}
func addScreenShotButton() {
let buttonWidth = 100
let buttonHeight = 50
let buttonX = (Int(view.frame.width) - buttonWidth) / 2
self.view.addSubview(screenShotButton)
screenShotButton.frame = CGRect(x: buttonX, y: 700, width: buttonWidth, height: buttonHeight)
screenShotButton.setTitle("截图", for: .normal)
screenShotButton.setTitleColor(.black, for: .normal)
screenShotButton.backgroundColor = .yellow
screenShotButton.addTarget(self, action: #selector(screenShotButtonAction), for: .touchUpInside)
screenShotButton.layer.cornerRadius = 10
}
运行后效果如下:
效果很好,在手机上看的话效果会更好一些,稍后我会把源码粘在下面。
好,既然已经有了结果,那么我们就开始截图分享,点击截图按钮,效果如下:
会发现截图显得进度是100%,也就对应了我们上面提到的,截图截的是模型图层的内容。
优化动画(使截图与画面保持一致)
想要解决上面的问题,其实我们只需要稍微做一点改动,让模型图层来与我们的最终结果保持一致,这样截图的画面就是我们想要的结果。
修改代码如下:
/// 画一个半圆
private func drawPath() {
// 线宽
let lineWidth: CGFloat = 20
let radius = bounds.width / 2 - lineWidth / 2
path = UIBezierPath()
let endAngle = CGFloat.pi * (CGFloat(score) / 100 - 1.0)
path.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height), radius: radius, startAngle: CGFloat.pi, endAngle: endAngle, clockwise: true)
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 20.0
}
func startAnimation() {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = duration
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
shapeLayer.add(animation, forKey: nil)
scoreLabel.text = "\(score)"
}
运行之后我们再来看一下截图效果:
可以看见,现在的截图结果已经和我们期待的结果一致了。
结语
理解模型图层和呈现图层的区别对于正确处理iOS动画和截图至关重要。通过明确这两个图层的作用,开发者可以更好地控制动画效果,并确保截图与实际看到的动画一致。希望这篇博客能帮助你在iOS开发中更加自信地处理动画相关的问题。