我正在尝试创建一个动画,该动画应如下所示:
使用下面的startCircle()函数在单击后开始strokeEnd动画。
如果完成一个完整的圆,它将调用recursiveContinue(),该函数将strokeEnd从0继续到1,同时从头开始将strokeStart从0重置为1。动画被分组为可以同时运行。
如果未完成,则使用pauseCircle()暂停圆形动画,并使用resetCircle()函数在单击时重置。
如果完成一个完整的圆圈并触发重置动画,则recursiveContinue()函数会在完成时使用CATransaction完成块自行调用
重置后,运行次数将增加,以防止多次添加该层。
这是代码:
enum CircleAnimationState {
case running
case paused
}
var circleAnimState = CircleAnimationState.running
var totalLoops = 0
var solveNum = 0
func pauseCircle(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
circleAnimState = CircleAnimationState.paused
}
func startCircle(layer: CAShapeLayer, duration: Double) {
if totalLoops == 0 && solveNum == 0 {
print("layer added")
view.layer.addSublayer(layer)
} else {
return
}
circleAnimState = CircleAnimationState.running
CATransaction.begin()
CATransaction.setDisableActions(true)
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
layer.speed = 1.0
basicAnimation.fromValue = 0
basicAnimation.toValue = 1
basicAnimation.duration = duration
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
print("initial animation started")
CATransaction.setCompletionBlock{ [weak self] in
self?.totalLoops += 1
layer.removeAnimation(forKey: "startCircleStrokeEndAnim")
if self?.circleAnimState == CircleAnimationState.running {
self?.recursiveContinue(layer: layer, duration: duration, loops: self?.totalLoops ?? 0)
print("circle start block complete for .running")
} else if self?.circleAnimState == CircleAnimationState.paused {
self?.totalLoops = 0
print("circle start block complete for .paused")
}
}
layer.add(basicAnimation, forKey: "startCircleStrokeEndAnim")
CATransaction.commit()
}
func recursiveReset(layer: CAShapeLayer, duration: Double, circleDuration: Double) -> CABasicAnimation {
let recursiveResetAnim = CABasicAnimation(keyPath: "strokeStart")
layer.speed = 1.0
recursiveResetAnim.fromValue = 0
recursiveResetAnim.toValue = 1
recursiveResetAnim.duration = duration
recursiveResetAnim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
recursiveResetAnim.fillMode = CAMediaTimingFillMode.forwards
recursiveResetAnim.isRemovedOnCompletion = true
return recursiveResetAnim
}
func recursiveContinue(layer: CAShapeLayer, duration: Double, loops: Int) {
let recursiveResetAnim = recursiveReset(layer: layer, duration: 1.5, circleDuration: duration)
let recursiveContinueAnim = CABasicAnimation(keyPath: "strokeEnd")
layer.speed = 1.0
recursiveContinueAnim.fromValue = 0
recursiveContinueAnim.toValue = 1
recursiveContinueAnim.duration = duration
recursiveContinueAnim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
recursiveContinueAnim.fillMode = CAMediaTimingFillMode.forwards
recursiveContinueAnim.isRemovedOnCompletion = true
let group = CAAnimationGroup()
group.animations = [recursiveResetAnim, recursiveContinueAnim]
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
if self?.circleAnimState == .paused {
return
} else if self?.circleAnimState == .running {
self?.totalLoops += 1
print("continous continue complete")
print("\(String(describing: self?.totalLoops)) loops")
self?.recursiveContinue(layer: layer, duration: duration, loops: self?.totalLoops ?? 0)
}
}
layer.add(group, forKey: "recursiveContinue")
CATransaction.commit()
}
func resetCircle(layer: CALayer, duration: Double, circleDuration: Double) {
let resetStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
let time = stopwatch.elapsedTime
let rounded = round((time ?? 0) * 1000) / 1000
print(rounded)
let absoluteTime = (time ?? 0) - (circleDuration * Double(totalLoops))
resetStrokeEnd.fromValue = absoluteTime / circleDuration
layer.speed = 1.0
resetStrokeEnd.toValue = 1
resetStrokeEnd.duration = duration
resetStrokeEnd.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
resetStrokeEnd.fillMode = CAMediaTimingFillMode.forwards
resetStrokeEnd.isRemovedOnCompletion = true
let resetStrokeStart = CABasicAnimation(keyPath: "strokeStart")
layer.speed = 1.0
resetStrokeStart.toValue = 1
resetStrokeStart.duration = duration
resetStrokeStart.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
resetStrokeStart.fillMode = CAMediaTimingFillMode.forwards
resetStrokeStart.isRemovedOnCompletion = true
let group = CAAnimationGroup()
group.animations = [resetStrokeEnd, resetStrokeStart]
CATransaction.begin()
CATransaction.setCompletionBlock {
self.circleAnimState = .running
self.totalLoops = 0
self.solveNum += 1
print("reset circle complete")
layer.removeAnimation(forKey: "resetCircle")
}
layer.add(group, forKey: "resetCircle")
CATransaction.commit()
}
实际发生的情况
情况1:完成前触发了pauseCircle-> strokeEnd在第一次单击后在整个第一个循环中都可以正确向前移动。重置时,strokeEnd几乎立即向后移动,然后消失。打印语句显示startCircle和resetCircle均在正确的时间触发。
第二轮:在到达一个完整的圆圈之前暂停会短暂显示在给定时间内笔划的一部分,然后像以前一样消失。
第二轮:到达一个完整的圆圈后暂停会短暂显示整个圆圈,然后像以前一样消失。
情况2:完成后触发了pauseCircle-> strokeEnd在第一次单击后在整个第一个循环中正确向前移动,并在完成后消失。笔划在跑步时隐藏的部分会在消失之前短暂出现。完成第二个完整循环后,打印语句显示recursiveContinue已正确触发。
第二轮:在到达另一个完整的圆圈之前暂停会短暂显示在给定时间内到达的笔画部分,然后像以前一样消失。
第二次运行:让动画的运行时间超过单个循环的持续时间,会立即触发完成块,从而创建中断的重复循环。在到达另一个完整圆圈后暂停会短暂显示整个圆圈,然后像以前一样消失。
其工作方式示例
循环的持续时间设置为60秒。 用户单击开始动画-触发startCircle()。 strokeEnd在60秒内从0均匀移动到1。
然后,将strokeEnd设置回0(在视觉上与1相同的位置),并以相同的速率继续移动。完成前60秒后,strokeStart会从值0到1进行指定的1.5秒持续时间动画,然后再设置回0(可视1)。这些动画位于recursiveContinue()函数内部。
每次重复执行60秒时,都会执行相同的重置并继续过程,这由recursiveContinue完成块指定。每次,totalLoops计数器都会递增。
如果用户在例如2:15分钟时通过单击来停止动画,那么strokeEnd会停止移动并保持在其停止时的值。这是使用pauseCircle()函数完成的。
然后再次单击将调用resetCircle()函数,该函数以指定的持续时间同时将strokeEnd从其暂停位置动画为1,将strokeStart从0动画为1。逐步增加solveNum(运行次数),并将totalLoops设置回0。
然后,用户可以从头开始,方法是再次单击以调用startCircle()。 startCircle()函数中的第一个if语句可防止多次添加shapeLayer。
注意:点击由单独的UIButton处理,不是 CAShapeLayer。
我可以使用CATransactions创建上述动画吗?还是我必须直接修改图层路径?