Как сделать анимацию кривой / дуги с помощью CAAnimation? - PullRequest
17 голосов
/ 27 октября 2010

У меня есть пользовательский интерфейс, в котором элемент удаляется, я хотел бы имитировать эффект «переместить в папку» в почте iOS.Эффект, при котором значок маленькой буквы «брошен» в папку.Мой вместо этого будет сброшен в корзину.

Я попытался реализовать его, используя CAAnimation на слое.Насколько я могу прочитать в документации, я должен иметь возможность установить byValue и toValue, а 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];

Я играл с этим некоторое время, но, похоже,для меня это Apple означает линейную интерполяцию.Добавление byValue не вычисляет красивую дугу или кривую и не анимирует элемент через нее.

Как мне сделать такую ​​анимацию?

Спасибо за любую помощь.

Ответы [ 5 ]

30 голосов
/ 11 июля 2013

Использование UIBezierPath

(Не забудьте добавить ссылку, а затем import QuartzCore, если вы используете iOS 6 или более раннюю версию)

Пример кода

Вы можете использовать анимацию, которая будет следовать по пути, достаточно удобно, CAKeyframeAnimation поддерживает CGPath, который можно получить из UIBezierPath. Свифт 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

Пути Безье (или Кривые Безье , если быть точным) работают точно так же, как те, которые вы найдете в фотошопе, фейерверке, эскизе ... У них есть две "контрольные точки", по одной на каждую вершину , Например, анимация, которую я только что сделал:

enter image description here

Так работает путь Безье. См. документацию по специфике , но в основном это две точки, которые "тянут" дугу в определенном направлении.

Рисование дорожки

Одна замечательная особенность 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)

Этот последний бит кода делает так, чтобы точки были похожи на предыдущий рисунок, но в переменном количестве по отношению к треугольнику, который формируют точки, умноженному на коэффициент, который был бы эквивалентен значению "натяжения" .

7 голосов
/ 11 июля 2013

Вы абсолютно правы, что анимация позиции с помощью CABasicAnimation заставляет ее двигаться по прямой линии. Есть еще один класс под названием CAKeyframeAnimation для создания более сложных анимаций.

Массив values

Вместо toValue, fromValue и byValue для основных анимаций вы можете использовать массив values или полный path для определения значений на этом пути. Если вы хотите анимировать позицию сначала в сторону, а затем вниз, вы можете передать массив из 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;

Дугой можно управлять, изменив свойства tensionValues, continuityValues и biasValues. Если вам нужно более точное управление, вы можете определить собственный путь вместо массива values.

A 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"];
3 голосов
/ 11 июля 2013

Попробуйте, это определенно решит вашу проблему, я использовал это в своем проекте:

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 голос
/ 13 февраля 2012

Я узнал, как это сделать. Действительно возможно анимировать 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"];

Edit:

Должно быть возможно изменить функцию синхронизации с помощью https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html (CAMediaTimingFunction, инициированная functionWithControlPoints ::::) Это «кубическая кривая Безье». Я уверен, что у Google есть ответы там. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves: -)

0 голосов
/ 16 апреля 2012

У меня был несколько похожий вопрос несколько дней назад, и я реализовал его с таймером, как говорит Брэд , но не NSTimer. CADisplayLink - это таймер, который следует использовать для этой цели, поскольку он синхронизируется с frameRate приложения и обеспечивает более плавную и естественную анимацию. Вы можете посмотреть мою реализацию этого в моем ответе здесь . Эта техника действительно дает гораздо больше контроля над анимацией, чем CAAnimation, и не намного сложнее. CAAnimation не может ничего нарисовать, поскольку он даже не перерисовывает вид. Он только двигает, деформирует и затухает то, что уже нарисовано.

...