圆形(圆形)UIView使用AutoLayoutresize…如何在resize动画期间为cornerRadius设置动画?

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

问题是当“CircleView”通过AutoLayout约束resize时…例如在设备轮换上 …它会严重扭曲,直到resize,因为“cornerRadius”属性必须赶上,并且操作系统只发送单个“边界”更改为视图的框架。

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

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

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

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

所以你想要这个:

顺利调整圆形视图

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

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

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

这是方法:覆盖-actionForLayer:forKey:CircleView中返回一个动作,当运行时,为cornerRadius安装动画。

这是两个假设:

  • bounds.originbounds.size获取单独的动画。 (现在这是真的,但可能未来的iOS可以使用单个动画进行bounds 。如果没有找到bounds.size动画,检查bounds动画会很容易。)
  • 在Core Animation请求cornerRadius操作之前, bounds.size动画将添加到图层。

鉴于这些假设,当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 } 

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

在上面的代码中,如果我们要求对cornerRadius执行操作,我们会在bounds.size上查找CABasicAnimation 。 如果我们找到一个,我们复制它,将关键路径更改为cornerRadius ,并将其保存在自定义CAAction (类Action ,我将在下面显示)。 我们还保存了cornerRadius属性的当前值,因为Core Animation 更新属性之前调用actionForLayer:forKey: .

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

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

如果动画不是附加的(如果UIKit创建动画时没有发生,据我所知),那么它只是以明显的方式设置fromValuefromValue

以下是为方便起见的整个文件:

 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的回答的启发。

这个答案建立在rob mayoff 的早期答案 之上 。 基本上,我为我们的项目实现了它,它在iPhone(iOS 9和10)上工作得很好,但问题仍然存在于iPad(iOS 9或10)上。

调试,我发现if语句:

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

总是在iPad上失败。 看起来动画在iPad上以不同于iPhone的顺序构建。 回顾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和10下的iPhone和iPad上都能正常工作。我还没有真正进一步测试,并且对CoreAnimation不够了解,无法完全理解这一变化。

在iOS 10中,您不需要创建CAAction,它只是创建一个CABasicAnimation,并在您的操作中提供此function(对于图层:,对于密钥:) – > 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属性来获取当前动画(iPad的“边界”和iPhone的“bounds.size”),这些设置在设备轮换时添加。

这些翻译答案通常是Objective-c ==> Swift,但是如果还有更顽固的Objective-c作者,这里的@ Rob的答案被翻译了……

 // see https://stackoverflow.com/a/35714554/294949 #import "RoundView.h" @interface Action : NSObject @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)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 

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

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

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

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

Interesting Posts