Animation between two forms of a bezier trajectory

I found it difficult to animate a UIImageView between two states: its original rectangle rectangle and a new shape created using UIBezierPath . There are many different techniques, most of which do not work for me.

At first there was the realization that using the animation of a UIView block would not work; obviously, you cannot animate sublevels in the animateWithDuration: block. (see here and here )

There is CAAnimation , with specific subclasses like CABasicAnimation . I soon realized that it was impossible to characterize a view that CAShapeLayer does not have what it does (for example, here ).

And they can’t be just any ways of two layers of a layer, but rather, "Animation of the path of a form layer is guaranteed only when animating from similar ones" (see here )

This causes more mundane problems, for example, what to use for fromValue and toValue (should they be CAShapeLayer or CGPath ?), What to add animation to ( layer or mask ?), Etc.

There seemed to be so many variables; what combination will give me the animation i was looking for?

+6
source share
3 answers

The first important point is the construction of two Bezier trajectories is similar, so the rectangle is an (trivial) analog of a more complex shape.

 // the complex bezier path let initialPoint = CGPoint(x: 0, y: 0) let curveStart = CGPoint(x: 0, y: (rect.size.height) * (0.2)) let curveControl = CGPoint(x: (rect.size.width) * (0.6), y: (rect.size.height) * (0.5)) let curveEnd = CGPoint(x: 0, y: (rect.size.height) * (0.8)) let firstCorner = CGPoint(x: 0, y: rect.size.height) let secondCorner = CGPoint(x: rect.size.width, y: rect.size.height) let thirdCorner = CGPoint(x: rect.size.width, y: 0) var myBezierArc = UIBezierPath() myBezierArc.moveToPoint(initialPoint) myBezierArc.addLineToPoint(curveStart) myBezierArc.addQuadCurveToPoint(curveEnd, controlPoint: curveControl) myBezierArc.addLineToPoint(firstCorner) myBezierArc.addLineToPoint(secondCorner) myBezierArc.addLineToPoint(thirdCorner) 

The simpler β€œtrivial” bezier path that creates the rectangle is exactly the same, but controlPoint set so that it doesn't seem to exist:

 let curveControl = CGPoint(x: 0, y: (rect.size.height) * (0.5)) 

(Try removing the addQuadCurveToPoint line to get a very strange animation!)

And finally, the animation commands:

 let myAnimation = CABasicAnimation(keyPath: "path") if (isArcVisible == true) { myAnimation.fromValue = myBezierArc.CGPath myAnimation.toValue = myBezierTrivial.CGPath } else { myAnimation.fromValue = myBezierTrivial.CGPath myAnimation.toValue = myBezierArc.CGPath } myAnimation.duration = 0.4 myAnimation.fillMode = kCAFillModeForwards myAnimation.removedOnCompletion = false myImageView.layer.mask.addAnimation(myAnimation, forKey: "animatePath") 

If anyone is interested, the project is here .

+11
source

Another approach is to use an image link. This is similar to a timer, except that it is consistent with updating the display. Then you have a screen link handler that changes the presentation according to how it should look at any particular point in the animation.

For example, if you want to animate the rounding of the corners of the mask from 0 to 50 points, you can do something like the following, where percent is a value between 0.0 and 1.0 indicating what percentage of the animation:

 let path = UIBezierPath(rect: imageView.bounds) let mask = CAShapeLayer() mask.path = path.CGPath imageView.layer.mask = mask let animation = AnimationDisplayLink(duration: 0.5) { percent in let cornerRadius = percent * 50.0 let path = UIBezierPath(roundedRect: self.imageView.bounds, cornerRadius: cornerRadius) mask.path = path.CGPath } 

Where:

 class AnimationDisplayLink : NSObject { var animationDuration: CGFloat var animationHandler: (percent: CGFloat) -> () var completionHandler: (() -> ())? private var startTime: CFAbsoluteTime! private var displayLink: CADisplayLink! init(duration: CGFloat, animationHandler: (percent: CGFloat)->(), completionHandler: (()->())? = nil) { animationDuration = duration self.animationHandler = animationHandler self.completionHandler = completionHandler super.init() startDisplayLink() } private func startDisplayLink () { startTime = CFAbsoluteTimeGetCurrent() displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:") displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) } private func stopDisplayLink() { displayLink.invalidate() displayLink = nil } func handleDisplayLink(displayLink: CADisplayLink) { let elapsed = CFAbsoluteTimeGetCurrent() - startTime var percent = CGFloat(elapsed) / animationDuration if percent >= 1.0 { stopDisplayLink() animationHandler(percent: 1.0) completionHandler?() } else { animationHandler(percent: percent) } } } 

The advantage of the approach to the display line is that it can be used to animate some property that otherwise cannot be used. It also allows you to accurately determine the temporary state during animation.

If you can use CAAnimation or UIKit block animation, this is probably the way to go. But linking to an image can sometimes be a good approach.

+9
source

I was inspired by your example to try out the circular animation using the methods mentioned in your answer and some links. I intend to extend this to a more general circle on polygonal animation, but currently it only works for squares. I have a class called RDPolyCircle (a subclass of CAShapeLayer) that does a heavy lift. Here is his code,

 @interface RDPolyCircle () @property (strong,nonatomic) UIBezierPath *polyPath; @property (strong,nonatomic) UIBezierPath *circlePath; @end @implementation RDPolyCircle { double cpDelta; double cosR; } -(instancetype)initWithFrame:(CGRect) frame numberOfSides:(NSInteger)sides isPointUp:(BOOL) isUp isInitiallyCircle:(BOOL) isCircle { if (self = [super init]) { self.frame = frame; _isPointUp = isUp; _isExpandedPolygon = !isCircle; double radius = (frame.size.width/2.0); cosR = sin(45 * M_PI/180.0) * radius; double fractionAlongTangent = 4.0*(sqrt(2)-1)/3.0; cpDelta = fractionAlongTangent * radius * sin(45 * M_PI/180.0); _circlePath = [self createCirclePathForFrame:frame]; _polyPath = [self createPolygonPathForFrame:frame numberOfSides:sides]; self.path = (isCircle)? self.circlePath.CGPath : self.polyPath.CGPath; self.fillColor = [UIColor clearColor].CGColor; self.strokeColor = [UIColor blueColor].CGColor; self.lineWidth = 6.0; } return self; } -(UIBezierPath *)createCirclePathForFrame:(CGRect) frame { CGPoint ctr = CGPointMake(frame.origin.x + frame.size.width/2.0, frame.origin.y + frame.size.height/2.0); // create a circle using 4 arcs, with the first one symmetrically spanning the y-axis CGPoint leftUpper = CGPointMake(ctr.x - cosR, ctr.y - cosR); CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y - cpDelta); CGPoint rightUpper = CGPointMake(ctr.x + cosR, ctr.y - cosR); CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y - cpDelta); CGPoint cp3 = CGPointMake(rightUpper.x + cpDelta, rightUpper.y + cpDelta); CGPoint rightLower = CGPointMake(ctr.x + cosR, ctr.y + cosR); CGPoint cp4 = CGPointMake(rightLower.x + cpDelta, rightLower.y - cpDelta); CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y + cpDelta); CGPoint leftLower = CGPointMake(ctr.x - cosR, ctr.y + cosR); CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y + cpDelta); CGPoint cp7 = CGPointMake(leftLower.x - cpDelta, leftLower.y - cpDelta); CGPoint cp8 = CGPointMake(leftUpper.x - cpDelta, leftUpper.y + cpDelta); UIBezierPath *circle = [UIBezierPath bezierPath]; [circle moveToPoint:leftUpper]; [circle addCurveToPoint:rightUpper controlPoint1:cp1 controlPoint2:cp2]; [circle addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4]; [circle addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6]; [circle addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8]; [circle closePath]; circle.lineCapStyle = kCGLineCapRound; return circle; } -(UIBezierPath *)createPolygonPathForFrame:(CGRect) frame numberOfSides:(NSInteger) sides { CGPoint leftUpper = CGPointMake(self.frame.origin.x, self.frame.origin.y); CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y); CGPoint rightUpper = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y); CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y); CGPoint cp3 = CGPointMake(rightUpper.x, rightUpper.y + cpDelta); CGPoint rightLower = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height); CGPoint cp4 = CGPointMake(rightLower.x , rightLower.y - cpDelta); CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y); CGPoint leftLower = CGPointMake(self.frame.origin.x, self.frame.origin.y + self.frame.size.height); CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y); CGPoint cp7 = CGPointMake(leftLower.x, leftLower.y - cpDelta); CGPoint cp8 = CGPointMake(leftUpper.x, leftUpper.y + cpDelta); UIBezierPath *square = [UIBezierPath bezierPath]; [square moveToPoint:leftUpper]; [square addCurveToPoint:rightUpper controlPoint1:cp1 controlPoint2:cp2]; [square addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4]; [square addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6]; [square addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8]; [square closePath]; square.lineCapStyle = kCGLineCapRound; return square; } -(void)toggleShape { if (self.isExpandedPolygon) { [self restore]; }else{ CABasicAnimation *expansionAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; expansionAnimation.fromValue = (__bridge id)(self.circlePath.CGPath); expansionAnimation.toValue = (__bridge id)(self.polyPath.CGPath); expansionAnimation.duration = 0.5; expansionAnimation.fillMode = kCAFillModeForwards; expansionAnimation.removedOnCompletion = NO; [self addAnimation:expansionAnimation forKey:@"Expansion"]; self.isExpandedPolygon = YES; } } -(void)restore { CABasicAnimation *contractionAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; contractionAnimation.fromValue = (__bridge id)(self.polyPath.CGPath); contractionAnimation.toValue = (__bridge id)(self.circlePath.CGPath); contractionAnimation.duration = 0.5; contractionAnimation.fillMode = kCAFillModeForwards; contractionAnimation.removedOnCompletion = NO; [self addAnimation:contractionAnimation forKey:@"Contraction"]; self.isExpandedPolygon = NO; } 

In the view manager, create an instance of this layer and add it to a simple view level, then animate it with the click of a button,

 #import "ViewController.h" #import "RDPolyCircle.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIView *circleView; // a plain UIView 150 x 150 centered in the superview @property (strong,nonatomic) RDPolyCircle *shapeLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // currently isPointUp and numberOfSides are not implemented (the shape created has numberOfSides=4 and isPointUp=NO) // isInitiallyCircle is implemented self.shapeLayer = [[RDPolyCircle alloc] initWithFrame:self.circleView.bounds numberOfSides: 4 isPointUp:NO isInitiallyCircle:YES]; [self.circleView.layer addSublayer:self.shapeLayer]; } - (IBAction)toggleShape:(UIButton *)sender { [self.shapeLayer toggleShape]; } 

The project can be found here http://jmp.sh/iK3kuVs .

+3
source

All Articles