这个问题在这里已有答案:
我试图动画CAShapeLayer的路径,以便我得到一个圆圈“填充”达到特定数量的效果。
问题
它“有效”,但不像我想的那样顺畅,我想向它介绍一些缓和措施。但是因为我是单独制作每个%的动画,所以我不确定现在是否可能。所以我正在寻找可以解决这个问题的替代方案!
视觉解释
首先 - 这里是我想要实现的动画的一些“帧”(参见图片 - 圈子填充从0%到25%)
码
我创建绿色笔划(外部):
let ovalPath = UIBezierPath(ovalInRect: CGRectMake(20, 20, 240, 240))
let circleStrokeLayer = CAShapeLayer()
circleStrokeLayer.path = ovalPath.CGPath
circleStrokeLayer.lineWidth = 20
circleStrokeLayer.fillColor = UIColor.clearColor().CGColor
circleStrokeLayer.strokeColor = colorGreen.CGColor
containerView.layer.addSublayer(circleStrokeLayer)
我创建填充形状的初始路径(内部):
let filledPathStart = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: Math().percentToRadians(0), clockwise: true)
filledPathStart.addLineToPoint(CGPoint(x: 140, y: 140))
filledLayer = CAShapeLayer()
filledLayer.path = filledPathStart.CGPath
filledLayer.fillColor = colorGreen.CGColor
containerView.layer.addSublayer(filledLayer)
然后我使用for循环和CABasicAnimations数组进行动画处理。
var animations: [CABasicAnimation] = [CABasicAnimation]()
func animate() {
for val in 0...25 {
let morph = CABasicAnimation(keyPath: "path")
morph.duration = 0.01
let filledPathEnd = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: Math().percentToRadians(CGFloat(val)), clockwise: true)
filledPathEnd.addLineToPoint(CGPoint(x: 140, y: 140))
morph.delegate = self
morph.toValue = filledPathEnd.CGPath
animations.append(morph)
}
applyNextAnimation()
}
func applyNextAnimation() {
if animations.count == 0 {
return
}
let nextAnimation = animations[0]
animations.removeAtIndex(0)
filledLayer.addAnimation(nextAnimation, forKey: nil)
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
filledLayer.path = (anim as! CABasicAnimation).toValue as! CGPath
applyNextAnimation()
}
不要试图创建一大堆单独的动画。 CAShapeLayer有一个strokeStart和strokeEnd属性。动画那个。
诀窍是在形状图层中安装整个圆弧,然后创建一个CABasicAnimation,将shapeEnd从0设置为1(将填充从0%设置为100%)或您需要的任何值。
您可以在该动画上应用您想要的任何时间。
我在Github上有一个项目(在Objective-C,我很害怕),包括使用这种技术的“时钟擦除”动画。以下是它的外观:
(这是一个gif,看起来有点粗糙。实际的iOS动画非常流畅。)
链接如下。在自述文件中查找“时钟擦除”动画。
时钟擦除动画将形状图层安装为图像视图图层上的蒙版。您可以直接绘制形状图层,如果这是您想要做的。
所以我在他的链接GitHub项目(在Objective-C中)中使用了Duncan C所拥有的内容并将其应用到我的场景中(也将其转换为Swift) - 它的效果非常好!
这是最终的解决方案:
// ---
// In some ViewController
// ---
let animatedCircle = AnimatedCircle()
self.addSubview(animatedCircle)
animatedCircle.runAnimation()
// ---
// The Animated Circle Class
// ---
import UIKit
import Darwin
let pi: CGFloat = CGFloat(M_PI)
let startAngle: CGFloat = (3.0 * pi) / 2.0
let colorGray = UIColor(red: 0.608, green: 0.608, blue: 0.608, alpha: 1.000)
let colorGreen = UIColor(red: 0.482, green: 0.835, blue: 0.000, alpha: 1.000)
// ----
// Math class to handle fun circle forumals
// ----
class Math {
func percentToRadians(percentComplete: CGFloat) -> CGFloat {
let degrees = (percentComplete/100) * 360
return startAngle + (degrees * (pi/180))
}
}
class AnimatedCircle: UIView {
let percentComplete: CGFloat = 25
var containerView: UIView!
var filledLayer: CAShapeLayer!
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 260, height: 260))
let endAngle = Math().percentToRadians(percentComplete)
// ----
// Create oval bezier path and layer
// ----
let ovalPath = UIBezierPath(ovalInRect: CGRectMake(10, 10, 240, 240))
let circleStrokeLayer = CAShapeLayer()
circleStrokeLayer.path = ovalPath.CGPath
circleStrokeLayer.lineWidth = 20
circleStrokeLayer.fillColor = UIColor.clearColor().CGColor
circleStrokeLayer.strokeColor = colorGreen.CGColor
// ----
// Create filled bezier path and layer
// ----
let filledPathStart = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: endAngle, clockwise: true)
filledPathStart.addLineToPoint(CGPoint(x: 140, y: 140))
filledLayer = CAShapeLayer()
filledLayer.path = filledPathStart.CGPath
filledLayer.fillColor = colorGreen.CGColor
// ----
// Add any layers to container view
// ----
containerView = UIView(frame: self.frame)
containerView.backgroundColor = UIColor.whiteColor()
containerView.layer.addSublayer(circleStrokeLayer)
containerView.layer.addSublayer(filledLayer)
// Set the frame of the filledLayer to match our view
filledLayer.frame = containerView.frame
}
func runAnimation() {
let endAngle = Math().percentToRadians(percentComplete)
let maskLayer = CAShapeLayer()
let maskWidth: CGFloat = filledLayer.frame.size.width
let maskHeight: CGFloat = filledLayer.frame.size.height
let centerPoint: CGPoint = CGPointMake(maskWidth / 2, maskHeight / 2)
let radius: CGFloat = CGFloat(sqrtf(Float(maskWidth * maskWidth + maskHeight * maskHeight)) / 2)
maskLayer.fillColor = UIColor.clearColor().CGColor
maskLayer.strokeColor = UIColor.blackColor().CGColor
maskLayer.lineWidth = radius
let arcPath: CGMutablePathRef = CGPathCreateMutable()
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y - radius / 2)
CGPathAddArc(arcPath, nil, centerPoint.x, centerPoint.y, radius / 2, 3*pi/2, endAngle, false)
maskLayer.path = arcPath
maskLayer.strokeEnd = 0
filledLayer.mask = maskLayer
filledLayer.mask!.frame = filledLayer.frame
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.duration = 3
anim.delegate = self
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
anim.fillMode = kCAFillModeForwards
anim.removedOnCompletion = false
anim.autoreverses = false
anim.toValue = 1.0
maskLayer.addAnimation(anim, forKey: "strokeEnd")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
谢谢,邓肯C - 这是非常有帮助的。
以下是使用您的建议的进度循环示例。视图绘制了一个随时间变化的进度循环,从一个完整的圆开始并擦除自己以显示圆后面的视图。有@IBInspectable
属性可以修改以填充圆而不是擦除。
这是项目的链接。该观点位于CircleTimer.view
。如果运行项目,则可以输入秒数并查看指定时间内的圆圈擦除。
https://github.com/riley2012/circle-timer-ios
这是执行动画的方法:
open func runMaskAnimation(duration: CFTimeInterval) {
if let parentLayer = filledLayer {
let maskLayer = CAShapeLayer()
maskLayer.frame = parentLayer.frame
let circleRadius = timerFillDiameter * 0.5
let circleHalfRadius = circleRadius * 0.5
let circleBounds = CGRect(x: parentLayer.bounds.midX - circleHalfRadius, y: parentLayer.bounds.midY - circleHalfRadius, width: circleRadius, height: circleRadius)
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = circleRadius
let path = UIBezierPath(roundedRect: circleBounds, cornerRadius: circleBounds.size.width * 0.5)
maskLayer.path = path.reversing().cgPath
maskLayer.strokeEnd = 0
parentLayer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 1.0
animation.toValue = 0.0
maskLayer.add(animation, forKey: "strokeEnd")
}
}