Animating between two bezier path shapes

后端 未结 3 698
忘掉有多难
忘掉有多难 2021-01-31 12:39

I found it tricky to animate a UIImageView between two states: its original rectangle frame, and a new shape created with a UIBezierPath. There are man

相关标签:
3条回答
  • 2021-01-31 13:10

    The first important point is to construct the two bezier paths similarly, so the rectangle is a (trivial) analogue to the 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 a rectangle, is exactly the same but the controlPoint is set so that it appears to not be there:

    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.

    0 讨论(0)
  • 2021-01-31 13:10

    I was inspired by your example to try a circle to square animation using the techniques that are mentioned in your answer and some of the links. I intend to extend this to be a more general circle to polygon animation, but currently it only works for squares. I have a class called RDPolyCircle (a subclass of CAShapeLayer) that does the heavy lifting. Here is its 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;
    }
    

    From the view controller, I create an instance of this layer, and add it to a simple view's layer, then do the animations on a button push,

    #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.

    0 讨论(0)
  • 2021-01-31 13:14

    Another approach is to use a display link. It's like a timer, except it's coordinated with the update of the display. You then have the handler of the display link modify the view according to what it should look like at any particular point of the animation.

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

    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 virtue of the display link approach is that it can be used to animate some property that is otherwise unanimatable. It also lets you to precisely dictate the interim state during the animation.

    If you can use CAAnimation or UIKit block-based animation, that's probably the way to go. But the display link can sometimes be a good fallback approach.

    0 讨论(0)
提交回复
热议问题