使用圆形 UIBezierPath 和 CABasicAnimation 为 CAShapeLayer 设置动画
Animate CAShapeLayer with circle UIBezierPath and CABasicAnimation
我想在 15 秒内为一个从 0 度到 360 度的圆制作动画。
动画很奇怪。我知道这可能是一个 start/end 角度问题,我已经遇到过圆形动画的那种问题,但我不知道如何解决这个问题。
var circle_layer=CAShapeLayer()
var circle_anim=CABasicAnimation(keyPath: "path")
func init_circle_layer(){
let w=circle_view.bounds.width
let center=CGPoint(x: w/2, y: w/2)
//initial path
let start_angle:CGFloat = -0.25*360*CGFloat.pi/180
let initial_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: start_angle, clockwise: true)
initial_path.addLine(to: center)
//final path
let end_angle:CGFloat=start_angle+360*CGFloat(CGFloat.pi/180)
let final_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: end_angle, clockwise: true)
final_path.addLine(to: center)
//init layer
circle_layer.path=initial_path.cgPath
circle_layer.fillColor=UIColor(hex_code: "EA535D").cgColor
circle_view.layer.addSublayer(circle_layer)
//init anim
circle_anim.duration=15
circle_anim.fromValue=initial_path.cgPath
circle_anim.toValue=final_path.cgPath
circle_anim.isRemovedOnCompletion=false
circle_anim.fillMode=kCAFillModeForwards
circle_anim.delegate=self
}
func start_circle_animation(){
circle_layer.add(circle_anim, forKey: "circle_anim")
}
我想从 0 度开始并在完整游览后在顶部结束:
您无法轻松地为 UIBezierPath
的填充设置动画(或者至少在不引入奇怪的人工制品的情况下,除非在控制良好的情况下)。但是您可以为 CAShapeLayer
的 path
的 strokeEnd
制作动画。而且,如果您使描边路径的线宽非常宽(即最后一个圆的半径),并将路径的半径设置为圆的一半,您会得到您正在寻找的东西。
private var circleLayer = CAShapeLayer()
private func configureCircleLayer() {
let radius = min(circleView.bounds.width, circleView.bounds.height) / 2
circleLayer.strokeColor = UIColor(hexCode: "EA535D").cgColor
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineWidth = radius
circleView.layer.addSublayer(circleLayer)
let center = CGPoint(x: circleView.bounds.width/2, y: circleView.bounds.height/2)
let startAngle: CGFloat = -0.25 * 2 * .pi
let endAngle: CGFloat = startAngle + 2 * .pi
circleLayer.path = UIBezierPath(arcCenter: center, radius: radius / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
circleLayer.strokeEnd = 0
}
private func startCircleAnimation() {
circleLayer.strokeEnd = 1
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 15
circleLayer.add(animation, forKey: nil)
}
为了最终控制,在制作复杂的 UIBezierPath
动画时,您可以使用 CADisplayLink
,避免使用 path
的 CABasicAnimation
时有时会产生的伪像:
private var circleLayer = CAShapeLayer()
private weak var displayLink: CADisplayLink?
private var startTime: CFTimeInterval!
private func configureCircleLayer() {
circleLayer.fillColor = UIColor(hexCode: "EA535D").cgColor
circleView.layer.addSublayer(circleLayer)
updatePath(percent: 0)
}
private func startCircleAnimation() {
startTime = CACurrentMediaTime()
displayLink = {
let _displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
_displayLink.add(to: .current, forMode: .commonModes)
return _displayLink
}()
}
@objc func handleDisplayLink(_ displayLink: CADisplayLink) { // the @objc qualifier needed for Swift 4 @objc inference
let percent = CGFloat(CACurrentMediaTime() - startTime) / 15.0
updatePath(percent: min(percent, 1.0))
if percent > 1.0 {
displayLink.invalidate()
}
}
private func updatePath(percent: CGFloat) {
let w = circleView.bounds.width
let center = CGPoint(x: w/2, y: w/2)
let startAngle: CGFloat = -0.25 * 2 * .pi
let endAngle: CGFloat = startAngle + percent * 2 * .pi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: w/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.close()
circleLayer.path = path.cgPath
}
那么你可以这样做:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
configureCircleLayer()
startCircleAnimation()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
displayLink?.invalidate() // to avoid displaylink keeping a reference to dismissed view during animation
}
结果:
我想在 15 秒内为一个从 0 度到 360 度的圆制作动画。
动画很奇怪。我知道这可能是一个 start/end 角度问题,我已经遇到过圆形动画的那种问题,但我不知道如何解决这个问题。
var circle_layer=CAShapeLayer()
var circle_anim=CABasicAnimation(keyPath: "path")
func init_circle_layer(){
let w=circle_view.bounds.width
let center=CGPoint(x: w/2, y: w/2)
//initial path
let start_angle:CGFloat = -0.25*360*CGFloat.pi/180
let initial_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: start_angle, clockwise: true)
initial_path.addLine(to: center)
//final path
let end_angle:CGFloat=start_angle+360*CGFloat(CGFloat.pi/180)
let final_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: end_angle, clockwise: true)
final_path.addLine(to: center)
//init layer
circle_layer.path=initial_path.cgPath
circle_layer.fillColor=UIColor(hex_code: "EA535D").cgColor
circle_view.layer.addSublayer(circle_layer)
//init anim
circle_anim.duration=15
circle_anim.fromValue=initial_path.cgPath
circle_anim.toValue=final_path.cgPath
circle_anim.isRemovedOnCompletion=false
circle_anim.fillMode=kCAFillModeForwards
circle_anim.delegate=self
}
func start_circle_animation(){
circle_layer.add(circle_anim, forKey: "circle_anim")
}
我想从 0 度开始并在完整游览后在顶部结束:
您无法轻松地为 UIBezierPath
的填充设置动画(或者至少在不引入奇怪的人工制品的情况下,除非在控制良好的情况下)。但是您可以为 CAShapeLayer
的 path
的 strokeEnd
制作动画。而且,如果您使描边路径的线宽非常宽(即最后一个圆的半径),并将路径的半径设置为圆的一半,您会得到您正在寻找的东西。
private var circleLayer = CAShapeLayer()
private func configureCircleLayer() {
let radius = min(circleView.bounds.width, circleView.bounds.height) / 2
circleLayer.strokeColor = UIColor(hexCode: "EA535D").cgColor
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineWidth = radius
circleView.layer.addSublayer(circleLayer)
let center = CGPoint(x: circleView.bounds.width/2, y: circleView.bounds.height/2)
let startAngle: CGFloat = -0.25 * 2 * .pi
let endAngle: CGFloat = startAngle + 2 * .pi
circleLayer.path = UIBezierPath(arcCenter: center, radius: radius / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
circleLayer.strokeEnd = 0
}
private func startCircleAnimation() {
circleLayer.strokeEnd = 1
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 15
circleLayer.add(animation, forKey: nil)
}
为了最终控制,在制作复杂的 UIBezierPath
动画时,您可以使用 CADisplayLink
,避免使用 path
的 CABasicAnimation
时有时会产生的伪像:
private var circleLayer = CAShapeLayer()
private weak var displayLink: CADisplayLink?
private var startTime: CFTimeInterval!
private func configureCircleLayer() {
circleLayer.fillColor = UIColor(hexCode: "EA535D").cgColor
circleView.layer.addSublayer(circleLayer)
updatePath(percent: 0)
}
private func startCircleAnimation() {
startTime = CACurrentMediaTime()
displayLink = {
let _displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
_displayLink.add(to: .current, forMode: .commonModes)
return _displayLink
}()
}
@objc func handleDisplayLink(_ displayLink: CADisplayLink) { // the @objc qualifier needed for Swift 4 @objc inference
let percent = CGFloat(CACurrentMediaTime() - startTime) / 15.0
updatePath(percent: min(percent, 1.0))
if percent > 1.0 {
displayLink.invalidate()
}
}
private func updatePath(percent: CGFloat) {
let w = circleView.bounds.width
let center = CGPoint(x: w/2, y: w/2)
let startAngle: CGFloat = -0.25 * 2 * .pi
let endAngle: CGFloat = startAngle + percent * 2 * .pi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: w/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.close()
circleLayer.path = path.cgPath
}
那么你可以这样做:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
configureCircleLayer()
startCircleAnimation()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
displayLink?.invalidate() // to avoid displaylink keeping a reference to dismissed view during animation
}
结果: