更新: 如果您的部署目标为iOS 11或更高版本:
从iOS 11开始,如果在动画块内更新,则cornerRadius
将以动画方式显示。只需在UIView
动画块中设置视图的layer.cornerRadius
,或者(为了处理界面方向更改)在layoutSubviews
或viewDidLayoutSubviews
中设置。
原始内容:如果您的部署目标早于iOS 11:
所以你想要这个:
![smoothly resizing circle view](https://istack.dev59.com/UN498.gif)
(我打开了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
之前。这就是为什么先安装 bounds.size
动画,然后才请求 cornerRadius
动画。每个属性的动画都安装在属性的setter中。
当我们设置 cornerRadius
时,核心动画会要求我们运行一个 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")
}
}
}
}
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")
}
}
}
}
我的回答受到Simon的这个回答的启发。
layoutSubviews()
方法怎么样? - J.WangSGBRoundView
,但它的效果不如他的答案好。 - Ben Guild