如何使用CAAnimation创建曲线/弧形动画?

18

我有一个用户界面,在该界面中会删除一个项目,我想要模仿iOS邮件中的"移动到文件夹"效果。即当小信封图标被"扔"进文件夹时的效果。我的图标将被丢入垃圾箱。

我尝试使用CAAnimation在图层上实现它。根据文档,我应该可以设置byValuetoValue,然后CAAnimation应该会插值这些值。我想要做一个小曲线,使得该项目经过一个略高于并稍微向左移的位置。

    CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position"];
[animation setDuration:2.0f];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];    
[animation setTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]];
[animation setFromValue:[NSValue valueWithCGPoint:fromPoint]];
[animation setByValue:[NSValue valueWithCGPoint:byPoint]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(512.0f, 800.0f)]];
[animation setRepeatCount:1.0];

我尝试了一段时间,但我认为苹果公司的意思是线性插值。 添加byValue不会计算出一个漂亮的弧线或曲线,并通过它使项目动画化。

我该如何实现这样的动画效果呢?

感谢提供任何帮助。


我来晚了,但还是试着做类似的事情。最近我在一个WP7(Silverlight)应用程序中设置它非常容易。你只需要分别对X和Y进行动画处理,就可以得到一条弧线。可能是因为CAAnimation不支持分别对X和Y进行动画处理。 - Jonny
@Jonny 如果你觉得你有一个不同的问题,那么你可以创建自己的问题,并解释为什么它与这个问题不同。 - David Rönnqvist
5个回答

32

使用UIBezierPath

(如果您使用iOS 6或更早版本,请不要忘记链接并导入QuartzCore)

示例代码

你可以使用一个动画来沿着路径移动,方便的是,CAKeyframeAnimation支持CGPath,它可以从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")
}

理解UIBezierPath

贝塞尔路径(或准确地说是贝塞尔曲线)的工作方式与在Photoshop、Fireworks、Sketch等软件中找到的路径完全相同。它们有两个“控制点”,每个顶点一个。例如,我刚刚制作的动画:

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.
绘制路径 UIBezierPath 的一个很酷的功能是,您可以使用 CAShapeLayer 在屏幕上绘制它们,从而帮助您可视化它将要遵循的路径。
// 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)

改进原始示例

计算自己的贝塞尔路径的想法是,您可以使其完全动态化,因此,动画可以根据多个因素更改要执行的曲线,而不仅仅像我在示例中所做的那样硬编码。例如,控制点可以按以下方式计算:

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

这段代码使得点的分布类似于前面的图形,但是会根据三角形的大小变化,并乘以一个因子,该因子相当于“张力”值。

“需要”一个路径有点过于强硬。传递3个值和一个立方计算模式将会给您相同的结果,并且在我自己的意见中更容易理解。 - David Rönnqvist
是的,对于简单的情况来说是可以的。我有点避免使用三次方模式,因为我无法直观地理解偏差/连续性/张力的三重要素,贝塞尔点方法更简单,因为我一直在使用很多带有贝塞尔曲线的矢量程序,并且我对这些很熟悉。 - Can

8

您说得非常正确,使用CABasicAnimation来动画化位置会使其直线运动。还有一个叫做CAKeyframeAnimation的类可以用于进行更高级的动画。

values数组

对于基本动画,您可以使用values数组或完整的path来确定沿途的值,而不是使用toValuefromValuebyValue。如果您想先将位置向一侧移动,然后再向下移动,则可以传递一个由3个位置(起始、中间、结束)组成的数组。

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"];

如果你这样做,你会注意到视图从起点沿着直线移动到中点,然后沿着另一条直线移动到终点。使其从起点经过中点弧形到达终点需要改变动画的calculationMode
move.calculationMode = kCAAnimationCubic;

通过更改tensionValuescontinuityValuesbiasValues属性,您可以控制它的弧度。如果您想要更精细的控制,可以定义自己的路径而不是values数组。

跟随一个path

您可以创建任何路径并指定属性应该遵循该路径。在这里,我正在使用一个简单的弧线。

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"];

4

尝试这个,它一定会解决你的问题,我在我的项目中使用过:

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

1
我找到了如何做到这一点。实际上可以分别对X和Y进行动画处理。如果在相同的时间内(下面是2.0秒),并设置不同的时间函数,它将使其看起来像是从起始值到结束值沿弧线移动,而不是直线移动。要调整弧线,您需要尝试设置不同的时间函数。不确定CAAnimation是否支持任何“性感”的时间函数。
        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"];

编辑:

可以使用https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html(通过functionWithControlPoints::::初始化的CAMediaTimingFunction)更改时间函数。 这是一个“三次贝塞尔曲线”。 我相信谷歌上有答案。 http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves :-)


0

几天前我也有一个类似的问题,我用了计时器来实现,就像Brad所说的那样,但不是NSTimerCADisplayLink - 这是应该用于此目的的计时器,因为它与应用程序的帧速率同步,并提供更平滑、更自然的动画效果。您可以在我的答案这里中查看其实现。这种技术确实比CAAnimation更具控制力,而且并不复杂。 CAAnimation不能绘制任何东西,因为它甚至不重新绘制视图。它只移动、变形和淡化已经绘制好的内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接