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

时间:2016-03-01 01:47:23

标签: ios uiview

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

问题是当" CircleView"通过AutoLayout约束调整大小...例如在设备旋转 ...它会严重扭曲,直到调整大小,因为" cornerRadius"属性必须赶上,操作系统只发送一个"边界"改变视图的框架。

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

5 个答案:

答案 0 :(得分:38)

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

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

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

所以你想要这个:

smoothly resizing circle view

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

咆哮,随意跳过这一段:事实证明这比应该更难,因为iOS SDK没有制作参数(持续时间,时间曲线) )以方便的方式提供自动旋转动画。您可以(我认为)通过覆盖视图控制器上的-viewWillTransitionToSize:withTransitionCoordinator:来调用转发协调器上的-animateAlongsideTransition:completion:,并在您传递的回调中获取transitionDuration和{{1来自completionCurve。然后你需要将这些信息传递给你的UIViewControllerTransitionCoordinatorContext,它必须保存它(因为它尚未调整大小!)以及稍后当它收到CircleView时,它可以使用它使用这些已保存的动画参数为layoutSubviews创建CABasicAnimation。并且当动画动画调整大小时,不要意外创建动画...... 咆哮结束。

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

以下方法:在CircleView中覆盖-actionForLayer:forKey:以返回一个操作,该操作在运行时为CircleView安装动画。

这是两个假设:

  • cornerRadiusbounds.origin获得单独的动画。 (现在这是真的,但可能未来的iOS可以使用bounds.size的单个动画。如果没有找到bounds动画,检查bounds动画会很容易。)< / LI>
  • 在Core Animation请求bounds.size操作之前,bounds.size动画已添加到图层。

鉴于这些假设,当Core Animation请求cornerRadius操作时,我们可以从图层中获取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时,Core Animation会要求我们运行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上寻找CABasicAnimation。如果我们找到一个,我们会复制它,将关键路径更改为bounds.size,并将其保存在自定义cornerRadius(类CAAction中,我将在下面显示)。我们还保存Action属性的当前值,因为Core Animation在更新属性之前调用cornerRadius

actionForLayer:forKey:返回后,Core Animation会更新图层的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 方法设置动画的runActionForKey:object:arguments:fromValue属性,然后将动画添加到图层。有一个复杂的问题:UIKit使用“添加”动画,因为如果你在早期动画仍在运行的同时在属性上启动另一个动画,它们的效果会更好。所以我们的行动会检查它。

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

如果动画不是附加的(如果UIKit创建动画并不会发生,据我所知),它只是以明显的方式设置fromValuetoValue

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

fromValue

我的回答受到this answer by Simon的启发。

答案 1 :(得分:2)

此答案以the earlier answer rob mayoff为基础。基本上,我为我们的项目实现了它,它在iPhone(iOS 9和10)上运行得很好,但问题仍然存在于iPad(iOS 9或10)上。

调试,我发现if语句:

if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {

总是在iPad上失败。看起来动画在iPad上的构建方式与iPhone不同。回顾original answer by Simon,似乎之前的顺序发生了变化。所以我把两个答案结合起来给了我这样的东西:

override func action(for layer: CALayer, forKey event: String) -> CAAction? {
    let buildAction: (CABasicAnimation) -> Action = { boundsAnimation in
        let animation = boundsAnimation.copy() as! CABasicAnimation
        animation.keyPath = "cornerRadius"
        let action = Action()
        action.pendingAnimation = animation
        action.priorCornerRadius = layer.cornerRadius
        return action
    }

    if event == "cornerRadius" {
        if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
            return buildAction(boundsAnimation)
        } else if let boundsAnimation = self.action(for: layer, forKey: "bounds") as? CABasicAnimation {
            return buildAction(boundsAnimation)
        }
    }

    return super.action(for: layer, forKey: event)
}

通过结合这两个答案,它似乎在iOS 9和iOS下的iPhone和iPad上都能正常工作。我还没有进一步测试过,并且对CoreAnimation不太了解,无法完全理解这一变化。

答案 2 :(得分:1)

在iOS 10中,您不需要创建CAAction,它只需创建一个CABasicAnimation并在您的操作中提供此功能(对于图层:,对于密钥:) - &gt; CAAction? function(参见Swift示例):

private var currentBoundsAnimation: CABasicAnimation? {
    return layer.animation(forKey: "bounds.size") as? CABasicAnimation ?? layer.animation(forKey: "bounds") as? CABasicAnimation
}

override public var bounds: CGRect {
    didSet {
        layer.cornerRadius = min(bounds.width, bounds.height) / 2
    }
}

override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
    if(event == "cornerRadius"), let boundsAnimation = currentBoundsAnimation {
        let animation = CABasicAnimation(keyPath: "cornerRadius")
        animation.duration = boundsAnimation.duration
        animation.timingFunction = boundsAnimation.timingFunction
        return animation
    }

    return super.action(for: layer, forKey: event)
}

您也可以覆盖layoutSubviews:

,而不是覆盖bounds属性
override func layoutSubviews() {
    super.layoutSubviews()
    layer.cornerRadius = min(bounds.width, bounds.height) / 2
}

这很神奇,因为CABasicAnimation从模型和表示层中推断出缺少的值和来自的值。要正确设置时序,您需要私有的currentBoundsAnimation属性来获取当前动画(&#34;界限&#34;对于iPad和&#34; bounds.size&#34;对于iPhone),这是在设备轮换时添加的。

答案 3 :(得分:0)

这些翻译答案通常是Objective-c ==&gt;斯威夫特,但是如果有更多顽固的Objective-c作者离开,这里的@ Rob的答案已被翻译......

// see https://stackoverflow.com/a/35714554/294949

#import "RoundView.h"

@interface Action : NSObject<CAAction>
@property(strong,nonatomic) CABasicAnimation *pendingAnimation;
@property(assign,nonatomic) CGFloat priorCornerRadius;
@end

@implementation Action
- (void)runActionForKey:(NSString *)event object:(id)anObject
              arguments:(nullable NSDictionary *)dict {

    if ([anObject isKindOfClass:[CALayer self]]) {
        CALayer *layer = (CALayer *)anObject;
        if (self.pendingAnimation.isAdditive) {
            self.pendingAnimation.fromValue = @(self.priorCornerRadius - layer.cornerRadius);
            self.pendingAnimation.toValue = @(0);
        } else {
            self.pendingAnimation.fromValue = @(self.priorCornerRadius);
            self.pendingAnimation.toValue = @(layer.cornerRadius);
        }
        [layer addAnimation:self.pendingAnimation forKey:@"cornerRadius"];
    }
}
@end

@interface RoundView ()
@property(weak,nonatomic) UIImageView *imageView;
@end

@implementation RoundView

- (void)layoutSubviews {
    [super layoutSubviews];
    [self updateCornerRadius];
}

- (void)updateCornerRadius {
    self.layer.cornerRadius = MIN(self.bounds.size.width, self.bounds.size.height)/2.0;
}

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
    if ([event isEqualToString:@"cornerRadius"]) {
        CABasicAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds.size"];
        CABasicAnimation *animation = [boundsAnimation copy];
        animation.keyPath = @"cornerRadius";
        Action *action = [[Action alloc] init];
        action.pendingAnimation = animation;
        action.priorCornerRadius = layer.cornerRadius;
        return action;
    }
    return [super actionForLayer:layer forKey:event];;
}

@end

答案 4 :(得分:-1)

我建议不使用角半径,而是使用CAShapeLayer作为视图内容层的掩码。

您将填充360°圆弧CGPath作为形状图层的形状,并将其设置为图层视图的蒙版。

然后,您可以为遮罩层设置新的缩放变换动画,或者将更改设置为路径半径的动画。两种方法都应保持圆形,尽管缩放变换可能无法在较小像素尺寸下为您提供干净的形状。

时机将是棘手的部分(使用边界动画使掩模层的动画与锁步发生。)