The circular (round) size of a UIView with AutoLayout ... how is the cornerRadius animation during the resize animation?

I have a subclass of UIView that can be called CircleView . CircleView automatically sets the radius of the corner to half its width so that it is a circle.

The problem is that when you resize the CircleView using an AutoLayout restriction ... for example, when the device rotates ... it gets very distorted until it resizes, because the cornerRadius property should catch up, and the OS sends only one change " frames "to the presentation frame.

I was wondering if anyone had a good, clear strategy for implementing "CircleView" in such a way that they would not be distorted in such cases , but would still mask its contents in the form of a circle and allow the border to exist around the specified UIView.

+9
ios uiview
Mar 01 '16 at 1:47
source share
4 answers

UPDATE: if the deployment target is iOS 11 or later:

Starting with iOS 11, UIKit will animate cornerRadius if you update it inside the animation block. Just set your layer.cornerRadius view in the UIView animation UIView or (to handle interface orientation changes), set it to layoutSubviews or viewDidLayoutSubviews .

ORIGINAL: If the deployment target is older than iOS 11:

So you want this:

smooth resizing of a circle

(I have included Debug> Slow Animations to make viewing smoothness easier.)

Problem, do not skip this paragraph: it turns out to be much more complicated than it should be, because the iOS SDK does not make parameters (duration, time curve) autorotation animation is available in a convenient way. You can (I think) get on them by overriding -viewWillTransitionToSize:withTransitionCoordinator: on your view controller to call -animateAlongsideTransition:completion: in the transition coordinator, and in the callback you get transitionDuration and completionCurve from the UIViewControllerTransitionCoordinatorContext . And then you need to transfer this information to your CircleView , which should save it (since it has not yet been changed!), And later, when it receives layoutSubviews , it can use it to create a CABasicAnimation for cornerRadius with saved animation parameters. And do not accidentally create an animation when the size is not animated ... The end of a side statement.

Wow, that sounds like a ton of work, and you need to bring in a view controller. Here is another approach fully implemented inside CircleView . It works now (in iOS 9), but I can’t guarantee that it will always work in the future, because it makes two assumptions that could theoretically be wrong in the future.

Here's the approach: override -actionForLayer:forKey: in CircleView to return an action that sets up animation for cornerRadius at startup.

These are two assumptions:

  • bounds.origin and bounds.size get separate animations. (This is true now, but presumably future iOS could use single animation for bounds . It would be enough to just check the bounds animation if the bounds.size animation bounds.size not found.)
  • The bounds.size animation bounds.size added per layer before Core Animation requests a cornerRadius action.

Given these assumptions, when Core Animation requests a cornerRadius action, we can get the bounds.size animation from the layer, copy it, and change the copy for the cornerRadius animation. The copy has the same animation parameters as the original (if we do not modify them), so it has the correct duration and time curve.

Here's the start of CircleView :

 class CircleView: UIView { override func layoutSubviews() { super.layoutSubviews() updateCornerRadius() } private func updateCornerRadius() { layer.cornerRadius = min(bounds.width, bounds.height) / 2 } 

Note that the boundaries of the view are set before the view receives layoutSubviews , and therefore, before updating cornerRadius . This is why the bounds.size animation bounds.size set before the cornerRadius animation cornerRadius . Each property animation is set inside the property setting tool.

When we install cornerRadius , Core Animation asks us to launch 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) } 

In the above code, if we are asked to perform an action for cornerRadius , we will search for CABasicAnimation on bounds.size . If we find it, we copy it, change the key path to cornerRadius and save it in a user-defined CAAction ( Action class, which I will show below). We also save the current value of the cornerRadius property, because Core Animation calls actionForLayer:forKey: to , updating the property.

After returning actionForLayer:forKey: Core Animation updates the cornerRadius property of this layer. Then it launches the action by sending it runActionForKey:object:arguments: The task of the action is to install any animation. Here's the custom subclass of CAAction , which I CircleView inside of 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 

The runActionForKey:object:arguments: method sets animation properties fromValue and toValue , and then adds animation to the layer. There is a complication: UIKit uses “additive” animations because they work better if you run another animation in the property while the previous animation is still working. Therefore, our action verifies this.

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 cornerRadius property cornerRadius already been updated by the time the animation starts, adding that fromValue at the beginning of the animation makes it look like the old corner radius and adding a toValue zero at the end of the animation makes it look like the new corner radius.

If the animation is not additive (which does not happen if UIKit created the animation, as far as I know), then it simply sets fromValue and toValue obvious way.

Here is 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 

My answer was inspired by this answer of Simon .

+33
Mar 01 '16 at 4:16
source share

This answer is based on an earlier rob mayoff answer . Basically, I implemented it for our project, and it worked fine on the iPhone (iOS 9 and 10), but the problem remained on the iPad (iOS 9 or 10).

Debugging, I found that the if statement:

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

always played iPad. The animations seem to be built in a different order on the iPad than the iPhone. Looking back at Simon's original answer , it seems that the sequence has changed earlier. So I combined both answers, giving me something like this:

 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) } 

By combining both answers, it seems to work correctly on both the iPhone and iPad under iOS 9 and 10. I have not tested further and do not know enough about CoreAnimation to fully understand this change.

+2
Mar 01 '17 at 15:10
source share

These answers to translation usually go Objective-c ==> Swift, but in case there are more stubborn Objective-c authors left, here @Rob the answer is translated ...

 // 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 
0
Sep 30 '17 at 16:59
source share

I would suggest not using an angular radius, but instead using CAShapeLayer as a mask for your content level.

You set the filled 360-degree CGPath arc as the shape of the shape layer and set it as the mask of your layer view.

You can then either animate the new scale transformation for the mask layer, or animate the change in the radius of the path. Both methods should remain round, although scaled conversion may not give you a clean shape with smaller pixel sizes.

Timing would be the tricky part (getting the mask layer animation to happen in lockstep with border animation)

-one
Mar 01 '16 at 2:09 on
source share



All Articles