Как сделать анимацию кривой/дуги с помощью CAAnimation?

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

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

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

Спасибо за предоставленную помощь.

Ответы

Ответ 1

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

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

Пример кода

Вы можете использовать анимацию, которая будет следовать по пути, достаточно удобно, CAKeyframeAnimation поддерживает CGPath, который можно получить из UIBezierPath. Swift 3

func animate(view : UIView, fromPoint start : CGPoint, toPoint end: CGPoint)
{
    // The animation
    let animation = CAKeyframeAnimation(keyPath: "position")

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

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

Ответ 2

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

Массив values

Вместо toValue, fromValue и byValue для базовых анимаций вы можете использовать массив values или полный path для определения значений на этом пути. Если вы хотите анимировать позицию сначала в сторону, а затем вниз, вы можете передать массив из трех позиций (начальный, средний, конечный).

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

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

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

}

Ответ 4

Я узнал, как это сделать. Вполне возможно анимировать 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, введенная функциейWithControlPoints::) Это "кубическая кривая Безье". Я уверен, что у Google есть ответы. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves: -)

Ответ 5

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