CABasicAnimation сбрасывается к начальному значению после завершения анимации

Я вращаю CALayer и пытаюсь остановить его в его окончательном положении после завершения анимации.

Но после завершения анимации он сбрасывается в исходное положение.

(Документы xcode явно говорят, что анимация не будет обновлять значение свойства.)

любые предложения, как этого добиться.

14 ответов

Решение

Изменить: вот ответ, это сочетание моего ответа и Кришнана.

cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;

Значением по умолчанию является kCAFillModeRemoved, (Какое поведение сброса вы видите.)

Проблема с removeOnCompletion заключается в том, что элемент пользовательского интерфейса не позволяет взаимодействовать с пользователем.

Я использую технику для установки значения FROM в анимации и значения TO для объекта. Анимация автоматически заполняет значение TO до ее запуска, а когда она удаляется, объект остается в правильном состоянии.

// fade in
CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath: @"opacity"];
alphaAnimation.fillMode = kCAFillModeForwards;

alphaAnimation.fromValue = NUM_FLOAT(0);
self.view.layer.opacity = 1;

[self.view.layer addAnimation: alphaAnimation forKey: @"fade"];

Базовая анимация поддерживает две иерархии слоев: уровень модели и уровень представления. Когда анимация выполняется, слой модели фактически не поврежден и сохраняет свое первоначальное значение. По умолчанию анимация удаляется после ее завершения. Затем уровень представления возвращается к значению уровня модели.

Просто настройка removedOnCompletion в NO означает, что анимация не будет удалена и тратит впустую память. Кроме того, уровень модели и уровень представления больше не будут синхронными, что может привести к потенциальным ошибкам.

Так что было бы лучшим решением обновить свойство непосредственно на уровне модели до конечного значения.

self.view.layer.opacity = 1;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];

Если есть неявная анимация, вызванная первой строкой кода выше, попробуйте отключить, если:

[CATransaction begin];
[CATransaction setDisableActions:YES];

self.view.layer.opacity = 1;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];

[CATransaction commit];

Установите следующее свойство:

animationObject.removedOnCompletion = NO;

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

CGFloat yOffset = 30;
CGPoint endPosition = CGPointMake(someLayer.position.x,someLayer.position.y + yOffset);

someLayer.position = endPosition; // Implicit animation for position

CABasicAnimation * animation =[CABasicAnimation animationWithKeyPath:@"position.y"]; 

animation.fromValue = @(someLayer.position.y);
animation.toValue = @(someLayer.position.y + yOffset);

[someLayer addAnimation:animation forKey:@"position"]; // The explicit animation 'animation' override implicit animation

Вы можете получить больше информации о Apple WWDC Video Session 421 - Основные принципы анимации (середина видео).

Это работает:

let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3

someLayer.opacity = 1 // important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")

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

Если вы хотите задержать, сделайте следующее:

let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3
animation.beginTime = someLayer.convertTime(CACurrentMediaTime(), fromLayer: nil) + 1
animation.fillMode = kCAFillModeBackwards // So the opacity is 0 while the animation waits to start.

someLayer.opacity = 1 // <- important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")

И, наконец, если вы используете "removeOnCompletion = false", это будет пропускать CAAnimations до тех пор, пока слой не будет в конечном итоге удален - избегайте

CALayer имеет уровень модели и уровень представления. Во время анимации уровень представления обновляется независимо от модели. Когда анимация завершена, уровень представления обновляется в соответствии со значением из модели. Если вы хотите избежать резкого скачка после окончания анимации, ключ состоит в том, чтобы синхронизировать два слоя.

Если вы знаете конечное значение, вы можете просто установить модель напрямую.

self.view.layer.opacity = 1;

Но если у вас есть анимация, в которой вы не знаете конечную позицию (например, медленное затухание, которое пользователь может приостановить, а затем развернуть), то вы можете напрямую запросить уровень представления, чтобы найти текущее значение, а затем обновить модель.

NSNumber *opacity = [self.layer.presentationLayer valueForKeyPath:@"opacity"];
[self.layer setValue:opacity forKeyPath:@"opacity"];

Извлечение значения из уровня представления также особенно полезно для масштабирования или вращения путей. (например transform.scale, transform.rotation)

Просто поместите это в свой код

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

Поэтому моя проблема заключалась в том, что я пытался вращать объект при жесте панорамирования, поэтому у меня было несколько одинаковых анимаций при каждом движении. У меня были оба fillMode = kCAFillModeForwards а также isRemovedOnCompletion = false но это не помогло В моем случае я должен был убедиться, что ключ анимации отличается каждый раз, когда я добавляю новую анимацию:

let angle = // here is my computed angle
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.toValue = angle
rotate.duration = 0.1
rotate.isRemovedOnCompletion = false
rotate.fillMode = kCAFillModeForwards

head.layer.add(rotate, forKey: "rotate\(angle)")

Без использования removedOnCompletion

Вы можете попробовать эту технику:

self.animateOnX(item: shapeLayer)

func animateOnX(item:CAShapeLayer)
{
    let endPostion = CGPoint(x: 200, y: 0)
    let pathAnimation = CABasicAnimation(keyPath: "position")
    //
    pathAnimation.duration = 20
    pathAnimation.fromValue = CGPoint(x: 0, y: 0)//comment this line and notice the difference
    pathAnimation.toValue =  endPostion
    pathAnimation.fillMode = kCAFillModeBoth

    item.position = endPostion//prevent the CABasicAnimation from resetting item's position when the animation finishes

    item.add(pathAnimation, forKey: nil)
}

Просто настройка fillMode а также removedOnCompletion не работал для меня Я решил проблему, установив все свойства ниже для объекта CABasicAnimation:

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.duration = 0.38f;
ba.fillMode = kCAFillModeForwards;
ba.removedOnCompletion = NO;
ba.autoreverses = NO;
ba.repeatCount = 0;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.85f, 0.85f, 1.0f)];
[myView.layer addAnimation:ba forKey:nil];

Этот код преобразует myView до 85% его размера (3-е измерение без изменений).

@ Лесли Годвин не очень хороший ответ: "self.view.layer.opacity = 1;" выполняется немедленно (это занимает около одной секунды), пожалуйста, исправьте alphaAnimation.duration на 10.0, если у вас есть сомнения. Вы должны удалить эту строку.

Таким образом, когда вы фиксируете fillMode в kCAFillModeForwards и удаляете OnCompletion в NO, вы разрешаете анимации оставаться в слое. Если вы исправите делегат анимации и попробуете что-то вроде:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
 [theLayer removeAllAnimations];
}

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

Вы должны исправить свойство слоя перед удалением анимации из него. Попробуй это:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
     if([anim isKindOfClass:[CABasicAnimation class] ]) // check, because of the cast
    {
        CALayer *theLayer = 0;
        if(anim==[_b1 animationForKey:@"opacity"])
            theLayer = _b1; // I have two layers
        else
        if(anim==[_b2 animationForKey:@"opacity"])
            theLayer = _b2;

        if(theLayer)
        {
            CGFloat toValue = [((CABasicAnimation*)anim).toValue floatValue];
            [theLayer setOpacity:toValue];

            [theLayer removeAllAnimations];
        }
    }
}

Самое простое решение - использовать неявную анимацию. Это решит все эти проблемы для вас:

self.layer?.backgroundColor = NSColor.red.cgColor;

Если вы хотите настроить, например, продолжительность, вы можете использовать NSAnimationContext:

    NSAnimationContext.beginGrouping();
    NSAnimationContext.current.duration = 0.5;
    self.layer?.backgroundColor = NSColor.red.cgColor;
    NSAnimationContext.endGrouping();

Примечание: это проверено только на macOS.

Я изначально не видел никакой анимации при этом. Проблема заключается в том, что уровень слоя с поддержкой представления не подразумевает анимацию. Чтобы решить эту проблему, убедитесь, что вы добавляете слой самостоятельно (перед установкой представления на слой-основу).

Пример того, как это сделать:

override func awakeFromNib() {
    self.layer = CALayer();
    //self.wantsLayer = true;
}

С помощью self.wantsLayer не имел никакого значения в моем тестировании, но у него могли быть некоторые побочные эффекты, о которых я не знаю.

Вот образец с детской площадки:

import PlaygroundSupport
import UIKit

let resultRotation = CGFloat.pi / 2
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 300.0))
view.backgroundColor = .red

//--------------------------------------------------------------------------------

let rotate = CABasicAnimation(keyPath: "transform.rotation.z") // 1
rotate.fromValue = CGFloat.pi / 3 // 2
rotate.toValue = resultRotation // 3
rotate.duration = 5.0 // 4
rotate.beginTime = CACurrentMediaTime() + 1.0 // 5
// rotate.isRemovedOnCompletion = false // 6
rotate.fillMode = .backwards // 7

view.layer.add(rotate, forKey: nil) // 8
view.layer.setAffineTransform(CGAffineTransform(rotationAngle: resultRotation)) // 9

//--------------------------------------------------------------------------------

PlaygroundPage.current.liveView = view
  1. Создать анимационную модель
  2. Установите начальную позицию анимации (можно пропустить, это зависит от вашего текущего макета слоя)
  3. Установить конечную позицию анимации
  4. Установить продолжительность анимации
  5. Задержка анимации на секунду
  6. Не устанавливать false к isRemovedOnCompletion - позвольте Core Animation очистить после завершения анимации
  7. Вот первая часть трюка - вы говорите Core Animation поместить вашу анимацию в начальную позицию (вы устанавливаете на шаге № 2) еще до того, как анимация будет запущена - растяните ее назад во времени.
  8. Скопируйте подготовленный объект анимации, добавьте его на слой и запустите анимацию после задержки (вы установили на шаге 5)
  9. Вторая часть - установить правильное конечное положение слоя - после удаления анимации ваш слой будет показан в правильном месте.

Кажется, что установлен флаг falseOnCompletion, установленный в false, и fillMode, установленный в kCAFillModeForwards, также не работает для меня.

После того, как я наложил новую анимацию на слой, анимируемый объект сбрасывается в исходное состояние и затем анимируется из этого состояния. Что необходимо сделать дополнительно, это установить желаемое свойство слоя модели в соответствии со свойством его уровня представления перед установкой новой анимации, например, так:

someLayer.path = ((CAShapeLayer *)[someLayer presentationLayer]).path;
[someLayer addAnimation:someAnimation forKey:@"someAnimation"];
Другие вопросы по тегам