How to do a curve/arc animation with CAAnimation?

前端 未结 5 656
离开以前
离开以前 2021-01-30 09:35

I have an user interface where an item get deleted, I would like to mimic the \"move to folder\" effect in iOS mail. The effect where the little letter icon is \"thrown\" into t

相关标签:
5条回答
  • 2021-01-30 10:00

    I have had a bit similar question several days ago and I implemented it with timer, as Brad says, but not NSTimer. CADisplayLink - that is the timer which should be used for this purpose, as it is synchronized with the frameRate of the application and provides smoother and more natural animation. You can look at my implementation of it in my answer here. This technique really gives much more control on animation than CAAnimation, and is not much more complicated. CAAnimation can't draw anything since it doesn't even redraw the view. It only moves, deforms and fades what is already drawn.

    0 讨论(0)
  • 2021-01-30 10:08

    Using UIBezierPath

    (Don't forget to link and then import QuartzCore, if you're using iOS 6 or prior)

    Example code

    You could use an animation that will follow a path, conveniently enough, CAKeyframeAnimation supports a CGPath, which can be obtained from an UIBezierPath. Swift 3

    func animate(view : UIView, fromPoint start : CGPoint, toPoint end: CGPoint)
    {
        // The animation
        let animation = CAKeyframeAnimation(keyPath: "position")
    
        // Animation's path
        let path = UIBezierPath()
    
        // Move the "cursor" to the start
        path.move(to: start)
    
        // Calculate the control points
        let c1 = CGPoint(x: start.x + 64, y: start.y)
        let c2 = CGPoint(x: end.x,        y: end.y - 128)
    
        // Draw a curve towards the end, using control points
        path.addCurve(to: end, controlPoint1: c1, controlPoint2: c2)
    
        // Use this path as the animation's path (casted to CGPath)
        animation.path = path.cgPath;
    
        // The other animations properties
        animation.fillMode              = kCAFillModeForwards
        animation.isRemovedOnCompletion = false
        animation.duration              = 1.0
        animation.timingFunction        = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn)
    
        // Apply it
        view.layer.add(animation, forKey:"trash")
    }
    

    Understanding UIBezierPath

    Bezier paths (or Bezier Curves, to be accurate) work exactly like the ones you'd find in photoshop, fireworks, sketch... They have two "control points", one for each vertex. For example, the animation I just made:

    enter image description here

    Works the bezier path like that. See the documentation on the specifics, but it's basically two points that "pull" the arc towards a certain direction.

    Drawing a path

    One cool feature about UIBezierPath, is that you can draw them on screen with CAShapeLayer, thus, helping you visualise the path that it will follow.

    // Drawing the path
    let *layer          = CAShapeLayer()
    layer.path          = path.cgPath
    layer.strokeColor   = UIColor.black.cgColor
    layer.lineWidth     = 1.0
    layer.fillColor     = nil
    
    self.view.layer.addSublayer(layer)
    

    Improving the original example

    The idea of calculating your own bezier path, is that you can make the completely dynamic, thus, the animation can change the curve it's going to do, based on multiple factors, instead of just hard-coding as I did in the example, for instance, the control points could be calculated as follows:

    // Calculate the control points
    let factor : CGFloat = 0.5
    
    let deltaX : CGFloat = end.x - start.x
    let deltaY : CGFloat = end.y - start.y
    
    let c1 = CGPoint(x: start.x + deltaX * factor, y: start.y)
    let c2 = CGPoint(x: end.x                    , y: end.y - deltaY * factor)
    

    This last bit of code makes it so that the points are like the previous figure, but in a variable amount, respect to the triangle that the points form, multiplied by a factor which would be the equivalent of a "tension" value.

    0 讨论(0)
  • 2021-01-30 10:10

    Try this it will solve your problem definitely, I have used this in my project:

    UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(10, 126, 320, 24)] autorelease];
    label.text = @"Animate image into trash button";
    label.textAlignment = UITextAlignmentCenter;
    [label sizeToFit];
    [scrollView addSubview:label];
    
    UIImageView *icon = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"carmodel.png"]] autorelease];
    icon.center = CGPointMake(290, 150);
    icon.tag = ButtonActionsBehaviorTypeAnimateTrash;
    [scrollView addSubview:icon];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.center = CGPointMake(40, 200);
    button.tag = ButtonActionsBehaviorTypeAnimateTrash;
    [button setTitle:@"Delete Icon" forState:UIControlStateNormal];
    [button sizeToFit];
    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [scrollView addSubview:button];
    [scrollView bringSubviewToFront:icon];
    
    - (void)buttonClicked:(id)sender {
        UIView *senderView = (UIView*)sender;
        if (![senderView isKindOfClass:[UIView class]])
            return;
    
        switch (senderView.tag) {
            case ButtonActionsBehaviorTypeExpand: {
                CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
                anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
                anim.duration = 0.125;
                anim.repeatCount = 1;
                anim.autoreverses = YES;
                anim.removedOnCompletion = YES;
                anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.2, 1.2, 1.0)];
                [senderView.layer addAnimation:anim forKey:nil];
    
                break;
            }
    
            case ButtonActionsBehaviorTypeAnimateTrash: {
                UIView *icon = nil;
                for (UIView *theview in senderView.superview.subviews) {
                    if (theview.tag != ButtonActionsBehaviorTypeAnimateTrash)
                        continue;
                    if ([theview isKindOfClass:[UIImageView class]]) {
                        icon = theview;
                        break;
                    }
                }
    
                if (!icon)
                    return;
    
                UIBezierPath *movePath = [UIBezierPath bezierPath];
                [movePath moveToPoint:icon.center];
                [movePath addQuadCurveToPoint:senderView.center
                                 controlPoint:CGPointMake(senderView.center.x, icon.center.y)];
    
                CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
                moveAnim.path = movePath.CGPath;
                moveAnim.removedOnCompletion = YES;
    
                CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
                scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
                scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
                scaleAnim.removedOnCompletion = YES;
    
                CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"alpha"];
                opacityAnim.fromValue = [NSNumber numberWithFloat:1.0];
                opacityAnim.toValue = [NSNumber numberWithFloat:0.1];
                opacityAnim.removedOnCompletion = YES;
    
                CAAnimationGroup *animGroup = [CAAnimationGroup animation];
                animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
                animGroup.duration = 0.5;
                [icon.layer addAnimation:animGroup forKey:nil];
    
                break;
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-30 10:17

    You are absolutely correct that animating the position with a CABasicAnimation causes it to go in a straight line. There is another class called CAKeyframeAnimation for doing more advanced animations.

    An array of values

    Instead of toValue, fromValue and byValue for basic animations you can either use an array of values or a complete path to determine the values along the way. If you want to animate the position first to the side and then down you can pass an array of 3 positions (start, intermediate, end).

    CGPoint startPoint = myView.layer.position;
    CGPoint endPoint   = CGPointMake(512.0f, 800.0f); // or any point
    CGPoint midPoint   = CGPointMake(endPoint.x, startPoint.y);
    
    CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    move.values = @[[NSValue valueWithCGPoint:startPoint],
                    [NSValue valueWithCGPoint:midPoint],
                    [NSValue valueWithCGPoint:endPoint]];
    move.duration = 2.0f;
    
    myView.layer.position = endPoint; // instead of removeOnCompletion
    [myView.layer addAnimation:move forKey:@"move the view"];
    

    If you do this you will notice that the view moves from the start point in a straight line to the mid point and in another straight line to the end point. The part that is missing to make it arc from start to end via the mid point is to change the calculationMode of the animation.

    move.calculationMode = kCAAnimationCubic;
    

    You can control it arc by changing the tensionValues, continuityValues and biasValues properties. If you want finer control you can define your own path instead of the values array.

    A path to follow

    You can create any path and specify that the property should follow that. Here I'm using a simple arc

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL,
                      startPoint.x, startPoint.y);
    CGPathAddCurveToPoint(path, NULL,
                          controlPoint1.x, controlPoint1.y,
                          controlPoint2.x, controlPoint2.y,
                          endPoint.x, endPoint.y);
    
    CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    move.path = path;
    move.duration = 2.0f;
    
    myView.layer.position = endPoint; // instead of removeOnCompletion
    [myView.layer addAnimation:move forKey:@"move the view"];
    
    0 讨论(0)
  • 2021-01-30 10:26

    I found out how to do it. It's really possible to animate X and Y separately. If you animate them over the same time (2.0 seconds below) and set a different timing function, it will make it look like it moves in an arc instead of a straight line from start to finish values. To adjust the arc you'd need to play around with setting a different timing function. Not sure if CAAnimation supports any "sexy" timing functions however.

            const CFTimeInterval DURATION = 2.0f;
            CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.y"];
            [animation setDuration:DURATION];
            [animation setRemovedOnCompletion:NO];
            [animation setFillMode:kCAFillModeForwards];    
            [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
            [animation setFromValue:[NSNumber numberWithDouble:400.0]];
            [animation setToValue:[NSNumber numberWithDouble:0.0]];
            [animation setRepeatCount:1.0];
            [animation setDelegate:self];
            [myview.layer addAnimation:animation forKey:@"animatePositionY"];
    
            animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
            [animation setDuration:DURATION];
            [animation setRemovedOnCompletion:NO];
            [animation setFillMode:kCAFillModeForwards];    
            [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
            [animation setFromValue:[NSNumber numberWithDouble:300.0]];
            [animation setToValue:[NSNumber numberWithDouble:0.0]];
            [animation setRepeatCount:1.0];
            [animation setDelegate:self];
            [myview.layer addAnimation:animation forKey:@"animatePositionX"];
    

    Edit:

    Should be possible to change timing function by using https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html (CAMediaTimingFunction inited by functionWithControlPoints:::: ) It's a "cubic Bezier curve". I'm sure Google has answers there. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves :-)

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