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

查看:145
本文介绍了圆形(圆形)UIView使用AutoLayout调整大小...如何在调整大小动画期间为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通过AutoLayout约束调整大小时。 ..例如设备轮换上的 ...由于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.

所以你想要这个:

(我启用了Debug> Slow Animations以使平滑更容易看到。)

(I turned on Debug > Slow Animations to make the smoothness easier to see.)

一边咆哮,随意跳过这一段:事实证明这比应该更难,因为iOS SDK没有制作自转的参数(持续时间,时间曲线)动画以方便的方式提供。您可以(我认为)通过在您的视图控制器上覆盖 -viewWillTransitionToSize:withTransitionCoordinator:来调用 -animateAlongsideTransition:completion:在转换协调器上,在你传递的回调中,从<$获取 transitionDuration completionCurve C $ C> UIViewControllerTransitionCoordinatorContext 。然后你需要将这些信息传递给你的 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.

这是方法:覆盖 -actionForLayer:forKey: CircleView 中返回一个操作,在运行时安装 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.size 动画。)

  • 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.

这是<$ c $的开头c> 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 执行操作,我们会查找 CABasicAnimation on bounds.size 。如果我们找到一个,我们将其复制,将密钥路径更改为 cornerRadius ,并将其保存在自定义 CAAction 中(类动作,我将在下面展示)。我们还保存 cornerRadius 属性的当前值,因为Core Animation在<之前调用 actionForLayer:forKey: / strong>更新属性。

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:对象: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

我的回答是受到 Simon的回答

这篇关于圆形(圆形)UIView使用AutoLayout调整大小...如何在调整大小动画期间为cornerRadius设置动画?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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