使用 AutoLayout 调整圆形(圆形)UIView 的大小...如何在调整大小动画期间为cornerRadius 设置动画? [英] Circular (round) UIView resizing with AutoLayout... how to animate cornerRadius during the resize animation?

查看:16
本文介绍了使用 AutoLayout 调整圆形(圆形)UIView 的大小...如何在调整大小动画期间为cornerRadius 设置动画?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个子类 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 获取 transitionDurationcompletionCurve.然后你需要将该信息传递给你的 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.originbounds.size 获得单独的动画.(现在确实如此,但据推测,未来的 iOS 可以为 bounds 使用单个动画.如果没有 bounds,检查 bounds 动画就很容易了.大小动画被发现.)
  • bounds.size 动画在 Core Animation 请求 cornerRadius 动作之前被添加到层中.
  • bounds.origin and bounds.size get separate animations. (This is true now but presumably a future iOS could use a single animation for bounds. It would be easy enough to check for a bounds animation if no bounds.size animation were found.)
  • The bounds.size animation is added to the layer before Core Animation asks for the cornerRadius 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: 方法设置动画的fromValuetoValue 属性,然后将动画添加到图层.有一个复杂的问题: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 创建了动画就不会发生这种情况),那么它只设置 fromValuetoValue以明显的方式.

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆