圆形(圆形)UIView使用AutoLayout调整大小...如何在调整大小动画期间为cornerRadius设置动画?

Ben*_*ild 13 uiview ios

我有一个可以调用的子类UIView CircleView.CircleView会自动将圆角半径设置为其宽度的一半,以使其成为圆形.

问题是当"CircleView"通过AutoLayout约束调整大小时......例如在设备轮换上 ......由于"cornerRadius"属性必须赶上,并且操作系统仅发送,因此在调整大小之前它会严重失真单个"边界"更改为视图的框架.

我想知道是否有人有一个好的,明确的策略来实现"CircleView"的方式在这种情况下不会扭曲,但仍然会将其内容掩盖为圆形并允许在所述UIView周围存在边框.

rob*_*off 39

更新:如果您的部署目标是iOS 11或更高版本:

从iOS 11开始,cornerRadius如果您在动画块中更新它,UIKit将动画.只需layer.cornerRadiusUIView动画块中设置视图,或(以处理界面方向更改),将其设置为layoutSubviewsviewDidLayoutSubviews.

ORIGINAL:如果您的部署目标早于iOS 11:

所以你想要这个:

顺利调整圆形视图

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

旁边的咆哮,随意跳过这一段:事实证明这比应该更难,因为iOS SDK没有以方便的方式提供自转旋转动画的参数(持续时间,时间曲线).您可以(我认为)通过覆盖-viewWillTransitionToSize:withTransitionCoordinator:您的视图控制器来调用-animateAlongsideTransition:completion:转换协调器,并在您通过的回调中获取transitionDurationcompletionCurve从中获取UIViewControllerTransitionCoordinatorContext.然后你需要将这些信息传递给你CircleView,它必须保存它(因为它还没有被调整大小!),然后当它收到时layoutSubviews,它可以用它来创建一个带有那些保存的动画参数的CABasicAnimationfor cornerRadius.并且当它不是动画调整大小时不要意外地创建动画...... 结束了咆哮.

哇,这听起来像是大量的工作,你必须让视图控制器参与其中.这是另一种完全在里面实现的方法CircleView.它现在可以运行(在iOS 9中),但我不能保证它将来总是可以工作,因为它做了两个假设,理论上将来可能是错误的.

这里的做法:覆盖-actionForLayer:forKey:CircleView返回的动作是,在运行时,安装了一个动画cornerRadius.

这是两个假设:

  • bounds.originbounds.size获得单独的动画.(现在这是真的,但可能未来iOS可以使用单个动画bounds.bounds如果没有bounds.size找到动画,检查动画会很容易.)
  • 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
    }
Run Code Online (Sandbox Code Playgroud)

请注意,视图的边界在视图接收之前设置layoutSubviews,因此在我们更新之前设置cornerRadius.这就是在请求动画bounds.size之前安装cornerRadius动画的原因.每个属性的动画都安装在属性的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)
    }
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,如果我们被要求采取行动cornerRadius,我们会寻找一个CABasicAnimation开启bounds.size.如果我们找到一个,我们将其复制,更改密钥路径cornerRadius,并将其保存在自定义CAAction(类Action,我将在下面显示)中.我们还保存cornerRadius属性的当前值,因为Core Animation actionForLayer:forKey: 更新属性之前调用.

之后actionForLayer:forKey:的回报,核心动画更新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
Run Code Online (Sandbox Code Playgroud)

runActionForKey:object:arguments:方法设置fromValuetoValue动画的属性,然后添加动画层.有一个复杂因素:UIKit使用"添加"动画,因为如果在早期动画仍在运行时在属性上启动另一个动画,它们的效果会更好.所以我们的行动检查了这一点.

如果动画是加法的,则设置fromValue为新旧角半径之间的差异,并设置toValue为零.由于图层的cornerRadius属性在动画运行时已经更新,因此在动画fromValue开始时添加它使其看起来像旧的角半径,并且toValue在动画结束时添加零使其看起来像新的角半径.

如果动画没有添加剂(如UIKit中创建动画,因为我知道这不会发生,因为远),那么它只是设置fromValuetoValue在明显的方式.

为方便起见,这是整个文件:

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
Run Code Online (Sandbox Code Playgroud)

我的回答是受到西蒙这个答案的启发.