更新:如果您的部署目标是 iOS 11 或更高版本:
从 iOS 11 开始,如果您在动画块中更新 UIKit,它将为 cornerRadius 设置动画。只需将视图的layer.cornerRadius 设置在UIView 动画块中,或者(以处理界面方向更改),将其设置在layoutSubviews 或viewDidLayoutSubviews 中。
ORIGINAL:如果您的部署目标早于 iOS 11:
所以你想要这个:
(我打开了 Debug > Slow Animations 以使平滑度更容易看到。)
旁白,随意跳过这一段:事实证明这比它应该做的要困难得多,因为 iOS SDK 没有使参数(持续时间、时序曲线)以方便的方式提供自动旋转动画。你可以(我认为)通过覆盖视图控制器上的-viewWillTransitionToSize:withTransitionCoordinator: 以在转换协调器上调用-animateAlongsideTransition:completion: 来获取它们,并在你传递的回调中,从UIViewControllerTransitionCoordinatorContext 获取transitionDuration 和completionCurve。然后您需要将该信息传递给您的CircleView,它必须保存它(因为它还没有调整大小!)然后当它收到layoutSubviews时,它可以使用它来创建一个CABasicAnimation对于 cornerRadius 和那些保存的动画参数。当它不是动画调整大小时,不要意外创建动画...... 侧面咆哮结束。
哇,这听起来像是大量的工作,而且您必须让视图控制器参与进来。这是完全在CircleView 内部实现的另一种方法。它现在可以工作(在 iOS 9 中),但我不能保证它在未来总是可以工作,因为它提出了两个理论上将来可能是错误的假设。
方法如下:在CircleView 中覆盖-actionForLayer:forKey: 以返回一个动作,该动作在运行时会为cornerRadius 安装动画。
这是两个假设:
-
bounds.origin 和 bounds.size 获得单独的动画。 (现在确实如此,但大概未来的 iOS 可以为 bounds 使用单个动画。如果没有找到 bounds.size 动画,检查 bounds 动画就很容易了。)
-
bounds.size 动画在 Core Animation 请求 cornerRadius 动作之前添加到图层。
鉴于这些假设,当 Core Animation 请求 cornerRadius 动作时,我们可以从图层中获取 bounds.size 动画,将其复制,然后将副本修改为动画 cornerRadius。副本具有与原始相同的动画参数(除非我们修改它们),因此它具有正确的持续时间和时序曲线。
这是CircleView的开头:
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
请注意,视图的边界是在视图接收layoutSubviews 之前设置的,因此在我们更新cornerRadius 之前。这就是为什么在请求cornerRadius 动画之前安装bounds.size 动画的原因。每个属性的动画都安装在属性的 setter 中。
当我们设置cornerRadius 时,Core Animation 要求我们为它运行CAAction:
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
在上面的代码中,如果我们被要求对cornerRadius 执行操作,我们会在bounds.size 上查找CABasicAnimation。如果找到,我们复制它,将密钥路径更改为cornerRadius,并将其保存在自定义CAAction(属于Action 类,我将在下面显示)中。我们还保存了cornerRadius 属性的当前值,因为Core Animation 在更新属性之前调用了actionForLayer:forKey:。
actionForLayer:forKey: 返回后,Core Animation 更新图层的cornerRadius 属性。然后它通过发送runActionForKey:object:arguments: 来运行该操作。动作的工作是安装任何合适的动画。这是CAAction 的自定义子类,我已将其嵌套在CircleView 中:
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
runActionForKey:object:arguments: 方法设置动画的fromValue 和toValue 属性,然后将动画添加到图层。有一个复杂的地方:UIKit 使用“附加”动画,因为如果你在一个属性上启动另一个动画,而早期的动画仍在运行,它们会更好地工作。因此,我们的操作会对此进行检查。
如果动画是相加的,它会将fromValue 设置为新旧角半径之间的差值,并将toValue 设置为零。由于该层的cornerRadius 属性在动画运行时已经更新,因此在动画开始处添加fromValue 使其看起来像旧的圆角半径,并在末尾添加为零的toValue的动画使它看起来像新的圆角半径。
如果动画不是附加的(据我所知,如果 UIKit 创建了动画,则不会发生这种情况),那么它只会以明显的方式设置 fromValue 和 toValue。
为方便起见,这是整个文件:
import UIKit
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
我的回答灵感来自this answer by Simon。