使用 AutoLayout 调整圆形(圆形)UIView 的大小...如何在调整大小动画期间为cornerRadius 设置动画? [英] Circular (round) UIView resizing with AutoLayout... how to animate cornerRadius during the resize animation?
问题描述
我有一个子类 UIView,我们可以调用它 CircleView
.CircleView 自动将角半径设置为其宽度的一半,以使其成为圆形.
I have a subclassed UIView that we can call CircleView
. CircleView automatically sets a corner radius to half of its width in order for it to be a circle.
问题是,当CircleView"被自动布局约束调整大小时......例如在设备旋转时......它会严重扭曲,直到调整大小发生,因为cornerRadius"属性必须赶上,操作系统只向视图的框架发送一个边界"更改.
The problem is that when "CircleView" is resized by an AutoLayout constraint... for example on a device rotation... it distorts badly until the resize takes place because the "cornerRadius" property has to catch up, and the OS only sends a single "bounds" change to the view's frame.
我想知道是否有人有一个好的、清晰的策略来以在这种情况下不会扭曲的方式实现CircleView",但仍将其内容掩盖为圆形并允许边框存在于所述 UIView 周围.
I was wondering if anyone had a good, clear strategy for implementing "CircleView" in a way that won't distort in such instances, but will still mask its contents to the shape of a circle and allow for a border to exist around said UIView.
推荐答案
更新:如果您的部署目标是 iOS 11 或更高版本:
从 iOS 11 开始,如果您在动画块内更新 UIKit,它将为 cornerRadius
设置动画.只需在 UIView
动画块中设置视图的 layer.cornerRadius
,或者(以处理界面方向更改),在 layoutSubviews
或 中设置它>viewDidLayoutSubviews
.
UPDATE: If your deployment target is iOS 11 or later:
Starting in iOS 11, UIKit will animate cornerRadius
if you update it inside an animation block. Just set your view's layer.cornerRadius
in a UIView
animation block, or (to handle interface orientation changes), set it in layoutSubviews
or viewDidLayoutSubviews
.
所以你想要这个:
(我打开了调试 > 慢速动画以使平滑度更容易看到.)
(I turned on Debug > Slow Animations to make the smoothness easier to see.)
旁白,可以随意跳过这一段:事实证明这比应有的要困难得多,因为 iOS SDK 没有将参数(持续时间、时序曲线)以方便的方式提供自转动画.您可以(我认为)通过在您的视图控制器上覆盖 -viewWillTransitionToSize:withTransitionCoordinator:
以在转换协调器和回调中调用 -animateAlongsideTransition:completion:
来获得它们你通过,从 UIViewControllerTransitionCoordinatorContext
获取 transitionDuration
和 completionCurve
.然后你需要将该信息传递给你的 CircleView
,它必须保存它(因为它还没有调整大小!)然后当它收到 layoutSubviews
时,它可以使用它为 cornerRadius
创建一个 CABasicAnimation
和那些保存的动画参数.并且不要在不是动画调整大小时意外创建动画......侧面咆哮结束.
Side rant, feel free to skip this paragraph: This turns out to be a lot harder than it should be, because the iOS SDK doesn't make the parameters (duration, timing curve) of the autorotation animation available in a convenient way. You can (I think) get at them by overriding -viewWillTransitionToSize:withTransitionCoordinator:
on your view controller to call -animateAlongsideTransition:completion:
on the transition coordinator, and in the callback you pass, get the transitionDuration
and completionCurve
from the UIViewControllerTransitionCoordinatorContext
. And then you need to pass that information down to your CircleView
, which has to save it (because it hasn't been resized yet!) and later when it receives layoutSubviews
, it can use it to create a CABasicAnimation
for cornerRadius
with those saved animation parameters. And don't accidentally create an animation when it's not an animated resize… End of side rant.
哇,这听起来像是大量的工作,您必须涉及到视图控制器.这是另一种完全在 CircleView
内部实现的方法.它现在有效(在 iOS 9 中),但我不能保证它在未来总是有效,因为它做出了两个理论上可能在未来错误的假设.
Wow, that sounds like a ton of work, and you have to involve the view controller. Here's another approach that's entirely implemented inside CircleView
. It works now (in iOS 9) but I can't guarantee it'll always work in the future, because it makes two assumptions that could theoretically be wrong in the future.
方法如下:在 CircleView
中覆盖 -actionForLayer:forKey:
以返回一个动作,该动作在运行时为 cornerRadius
安装动画.
Here's the approach: override -actionForLayer:forKey:
in CircleView
to return an action that, when run, installs an animation for cornerRadius
.
这是两个假设:
bounds.origin
和bounds.size
获得单独的动画.(现在确实如此,但据推测,未来的 iOS 可以为bounds
使用单个动画.如果没有bounds,检查
动画被发现.)bounds
动画就很容易了.大小bounds.size
动画在 Core Animation 请求cornerRadius
动作之前被添加到层中.
bounds.origin
andbounds.size
get separate animations. (This is true now but presumably a future iOS could use a single animation forbounds
. It would be easy enough to check for abounds
animation if nobounds.size
animation were found.)- The
bounds.size
animation is added to the layer before Core Animation asks for thecornerRadius
action.
给定这些假设,当 Core Animation 要求 cornerRadius
动作时,我们可以从图层中获取 bounds.size
动画,将其复制,并将副本修改为动画 cornerRadius
代替.副本具有与原件相同的动画参数(除非我们对其进行修改),因此它具有正确的持续时间和时序曲线.
Given these assumptions, when Core Animation asks for the cornerRadius
action, we can get the bounds.size
animation from the layer, copy it, and modify the copy to animate cornerRadius
instead. The copy has the same animation parameters as the original (unless we modify them), so it has the correct duration and timing curve.
这是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 中.
Note that the view's bounds are set before the view receives layoutSubviews
, and therefore before we update cornerRadius
. This is why the bounds.size
animation is installed before the cornerRadius
animation is requested. Each property's animations are installed inside the property's setter.
当我们设置cornerRadius
时,Core Animation会要求我们为它运行一个CAAction
:
When we set cornerRadius
, Core Animation asks us for a CAAction
to run for it:
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:
.
In the code above, if we're asked for an action for cornerRadius
, we look for a CABasicAnimation
on bounds.size
. If we find one, we copy it, change the key path to cornerRadius
, and save it away in a custom CAAction
(of class Action
, which I will show below). We also save the current value of the cornerRadius
property, because Core Animation calls actionForLayer:forKey:
before updating the property.
actionForLayer:forKey:
返回后,Core Animation 更新图层的 cornerRadius
属性.然后它通过发送 runActionForKey:object:arguments:
来运行操作.动作的工作是安装任何合适的动画.这是 CAAction
的自定义子类,我嵌套在 CircleView
中:
After actionForLayer:forKey:
returns, Core Animation updates the cornerRadius
property of the layer. Then it runs the action by sending it runActionForKey:object:arguments:
. The job of the action is to install whatever animations are appropriate. Here's the custom subclass of CAAction
, which I've nested inside 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 使用附加"动画,因为如果您在一个属性上启动另一个动画而较早的动画仍在运行,它们会更好地工作.因此,我们的操作会对此进行检查.
The runActionForKey:object:arguments:
method sets the fromValue
and toValue
properties of the animation and then adds the animation to the layer. There's a complication: UIKit uses "additive" animations, because they work better if you start another animation on a property while an earlier animation is still running. So our action checks for that.
如果动画是可加的,它会将 fromValue
设置为新旧角半径之间的差值,并将 toValue
设置为零.由于层的 cornerRadius
属性在动画运行时已经更新,在动画开始时添加 fromValue
使它看起来像旧的角半径,并且在动画结束时添加为零的 toValue
使其看起来像新的圆角半径.
If the animation is additive, it sets fromValue
to the difference between the old and new corner radii, and sets toValue
to zero. Since the layer's cornerRadius
property has already been updated by the time the animation is running, adding that fromValue
at the start of the animation makes it look like the old corner radius, and adding the toValue
of zero at the end of the animation makes it look like the new corner radius.
如果动画不是可加的(据我所知,如果 UIKit 创建了动画就不会发生这种情况),那么它只设置 fromValue
和 toValue
以明显的方式.
If the animation is not additive (which doesn't happen if UIKit created the animation, as far as I know), then it just sets the fromValue
and toValue
in the obvious way.
为方便起见,这里是整个文件:
Here's the whole file for your convenience:
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
我的回答灵感来自 西蒙的这个答案.
这篇关于使用 AutoLayout 调整圆形(圆形)UIView 的大小...如何在调整大小动画期间为cornerRadius 设置动画?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!