CABasicAnimation đặt lại giá trị ban đầu sau khi hoạt ảnh hoàn tất


143

Tôi đang xoay một CALayer và cố gắng dừng nó ở vị trí cuối cùng sau khi hoạt ảnh hoàn thành.

Nhưng sau khi hoạt hình hoàn thành, nó đặt lại vị trí ban đầu.

(tài liệu xcode nói rõ ràng rằng hình ảnh động sẽ không cập nhật giá trị của thuộc tính.)

bất kỳ đề xuất làm thế nào để đạt được điều này.


1
Đây là một trong những câu hỏi SO kỳ lạ trong đó hầu hết tất cả các câu trả lời đều hoàn toàn sai - hoàn toàn SAI . Bạn chỉ đơn giản nhìn vào .presentation()để có được giá trị "cuối cùng, nhìn thấy". Tìm kiếm các câu trả lời chính xác dưới đây để giải thích nó được thực hiện với lớp trình bày.
Fattie

Câu trả lời:


285

Đây là câu trả lời, đó là sự kết hợp giữa câu trả lời của tôi và của Krishnan.

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

Giá trị mặc định là kCAFillModeRemoved. (Đó là hành vi thiết lập lại mà bạn đang thấy.)


6
Đây có lẽ KHÔNG phải là câu trả lời chính xác - hãy xem nhận xét về câu trả lời của Krishnan. Bạn thường cần cái này VÀ của Krishnan; chỉ một hoặc khác sẽ không làm việc.
Adam

19
Đây không phải là câu trả lời chính xác, xem câu trả lời của @Leslie Godwin bên dưới.
Đánh dấu Ingram

6
Giải pháp @Leslie tốt hơn bởi vì nó lưu trữ giá trị cuối cùng trong lớp chứ không phải trong hình ảnh động.
Matthieu Rouif

1
Hãy cẩn thận khi bạn cài đặt removeOnCompletion thành NO. Nếu bạn đang gán đại biểu hoạt hình cho "tự", thì thể hiện của bạn sẽ được giữ lại bởi hình ảnh động. Vì vậy, sau khi gọi removeFromSuperview nếu bạn quên tự xóa / hủy hoạt hình, ví dụ dealloc sẽ không được gọi. Bạn sẽ bị rò rỉ bộ nhớ.
emrahgunduz

1
Câu trả lời hơn 200 điểm này là hoàn toàn, hoàn toàn, hoàn toàn không chính xác.
Fattie

83

Vấn đề với removeOnCompletion là phần tử UI không cho phép người dùng tương tác.

Kỹ thuật tôi là đặt giá trị TỪ trong hình động và giá trị TO trên đối tượng. Hoạt hình sẽ tự động điền giá trị TO trước khi bắt đầu và khi bị xóa sẽ để đối tượng ở trạng thái chính xác.

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

7
Không hoạt động nếu bạn đặt độ trễ bắt đầu. vd: alphaAnimation.beginTime = 1; Ngoài ra, fillModekhông cần thiết như tôi có thể nói ...?
Sam

mặt khác, đây là một câu trả lời hay, câu trả lời được chấp nhận sẽ không cho phép bạn thay đổi giá trị sau khi hoạt ảnh hoàn tất ...
Sam

2
tôi cũng cần lưu ý rằng beginTimenó không tương đối, bạn nên sử dụng, ví dụ:CACurrentMediaTime()+1;
Sam

Đó là 'nó' không phải là 'nó' - it-not-its.info. Xin lỗi nếu đó chỉ là một lỗi đánh máy.
fatuhoku

6
câu trả lời đúng, nhưng không cần thiết fillMode, xem thêm trong Ngăn chặn các lớp quay lại giá trị ban đầu khi sử dụng CAAnimations rõ ràng
likeid1412

16

Đặt thuộc tính sau:

animationObject.removedOnCompletion = NO;

@Nilesh: Chấp nhận câu trả lời của bạn. Điều đó sẽ khiến người dùng SO khác tin tưởng vào câu trả lời của bạn.
KRGAN Nam

7
Tôi đã phải sử dụng cả .fillMode = kCAFillModeForwards và .remondOnCompletion = NO. Cảm ơn!
William Rust

12

chỉ cần đặt nó vào trong mã của bạn

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

12

Bạn chỉ có thể thiết lập phím của CABasicAnimationđể positionkhi bạn thêm nó vào lớp. Bằng cách này, nó sẽ ghi đè hình ảnh động được thực hiện trên vị trí cho đường chuyền hiện tại trong vòng lặp chạy.

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

Bạn có thể có thêm thông tin về Phiên bản video 421 của Apple WWDC - Các yếu tố cần thiết cho hoạt hình cốt lõi (giữa video)


1
Điều này trông giống như cách chính xác để làm điều này. Hoạt ảnh được loại bỏ một cách tự nhiên (mặc định) và khi hoạt ảnh hoàn tất, nó sẽ trở về các giá trị được đặt trước khi hoạt ảnh bắt đầu, cụ thể là dòng này : someLayer.position = endPosition;. Và cảm ơn các ref cho WWDC!
bauerMusic

10

Một CALayer có một lớp mô hình và một lớp trình bày. Trong một hình ảnh động, lớp trình bày cập nhật độc lập với mô hình. Khi hoạt ảnh hoàn tất, lớp trình bày được cập nhật với giá trị từ mô hình. Nếu bạn muốn tránh một cú nhảy chói tai sau khi hoạt hình kết thúc, điều quan trọng là giữ cho hai lớp được đồng bộ hóa.

Nếu bạn biết giá trị cuối, bạn chỉ có thể đặt mô hình trực tiếp.

self.view.layer.opacity = 1;

Nhưng nếu bạn có một hình ảnh động mà bạn không biết vị trí kết thúc (ví dụ: độ mờ chậm mà người dùng có thể tạm dừng rồi đảo ngược), thì bạn có thể truy vấn trực tiếp lớp trình bày để tìm giá trị hiện tại, sau đó cập nhật mô hình.

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

Kéo giá trị từ lớp trình bày cũng đặc biệt hữu ích cho việc mở rộng hoặc xoay các phím bấm. (ví dụ transform.scale, transform.rotation)


8

Không sử dụng removedOnCompletion

Bạn có thể thử kỹ thuật này:

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

3
Đây dường như là câu trả lời hoạt động tốt nhất
rambossa

Đã đồng ý. Lưu ý nhận xét của @ ayman rằng bạn phải xác định thuộc tính .fromValue cho giá trị bắt đầu. Nếu không, bạn sẽ ghi đè lên giá trị bắt đầu trong mô hình và sẽ có hình động.
Jeff Collier

Câu trả lời này và câu trả lời của @ jason-moore tốt hơn câu trả lời được chấp nhận. Trong câu trả lời được chấp nhận, hình ảnh động chỉ bị tạm dừng nhưng giá trị mới không thực sự được đặt thành thuộc tính. Điều này có thể dẫn đến hành vi không mong muốn.
laka

8

Vì vậy, vấn đề của tôi là tôi đã cố gắng xoay một đối tượng trên cử chỉ pan và do đó tôi có nhiều hình động giống hệt nhau trên mỗi lần di chuyển. Tôi đã có cả hai fillMode = kCAFillModeForwardsisRemovedOnCompletion = falsenó không giúp được gì. Trong trường hợp của tôi, tôi phải đảm bảo rằng phím hoạt hình khác nhau mỗi lần tôi thêm một hình động mới :

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 = CAMediaTimingFillMode.forwards

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

7

Đơn giản chỉ cần thiết lập fillModeremovedOnCompletionkhông làm việc cho tôi. Tôi đã giải quyết vấn đề bằng cách đặt tất cả các thuộc tính bên dưới cho đối tượng 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];

Mã này biến đổi myViewthành 85% kích thước của nó (chiều thứ 3 không thay đổi).


7

Hoạt hình lõi duy trì hai cấu trúc phân cấp lớp: lớp mô hìnhlớp trình bày . Khi hoạt ảnh đang diễn ra, lớp mô hình thực sự còn nguyên vẹn và giữ nguyên giá trị ban đầu. Theo mặc định, hình ảnh động được xóa sau khi hoàn thành. Sau đó, lớp trình bày rơi trở lại giá trị của lớp mô hình.

Đơn giản chỉ cần đặt removedOnCompletionthành NOcó nghĩa là hình ảnh động sẽ không bị xóa và lãng phí bộ nhớ. Ngoài ra, lớp mô hình và lớp trình bày sẽ không còn đồng bộ nữa, điều này có thể dẫn đến các lỗi tiềm ẩn.

Vì vậy, nó sẽ là một giải pháp tốt hơn để cập nhật thuộc tính trực tiếp trên lớp mô hình lên giá trị cuối cùng.

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

Nếu có bất kỳ hình ảnh động ngầm nào gây ra bởi dòng đầu tiên của đoạn mã trên, hãy thử tắt nếu tắt:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.view.layer.opacity = 1;
[CATransaction commit];

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

Cảm ơn bạn! Đây thực sự nên là câu trả lời được chấp nhận, vì nó giải thích chính xác chức năng này thay vì chỉ sử dụng một thiết bị hỗ trợ ban nhạc để làm cho nó hoạt động
bplattenburg

6

Những công việc này:

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

Hoạt hình lõi hiển thị một 'lớp trình bày' trên đỉnh lớp bình thường của bạn trong khi hoạt hình. Vì vậy, đặt độ mờ (hoặc bất cứ thứ gì) thành những gì bạn muốn thấy khi hoạt ảnh kết thúc và lớp trình bày biến mất. Làm điều này trên dòng trước khi bạn thêm hình ảnh động để tránh nhấp nháy khi nó hoàn thành.

Nếu bạn muốn có một sự chậm trễ, hãy làm như sau:

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

Cuối cùng, nếu bạn sử dụng 'removeOnCompletion = false', nó sẽ rò rỉ CAAnimations cho đến khi lớp cuối cùng được xử lý - tránh.


Câu trả lời tuyệt vời, lời khuyên tuyệt vời. Cảm ơn đã gỡ bỏ bình luận OnCompletion.
iWheelBuy

4

Câu trả lời của @Leslie Godwin không thực sự tốt, "self.view.layer.opacity = 1;" được thực hiện ngay lập tức (mất khoảng một giây), vui lòng sửa alphaAnimation.duration thành 10.0, nếu bạn có nghi ngờ. Bạn phải loại bỏ dòng này.

Vì vậy, khi bạn sửa fillMode thành kCAFillModeForwards và removeOnCompletion thành NO, bạn hãy để hình ảnh động ở lại trong lớp. Nếu bạn sửa lỗi ủy nhiệm hoạt hình và thử một cái gì đó như:

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

... lớp phục hồi ngay lập tức tại thời điểm bạn thực hiện dòng này. Đó là những gì chúng tôi muốn tránh.

Bạn phải sửa thuộc tính layer trước khi xóa hình ảnh động khỏi nó. Thử cái này:

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

1

Có vẻ như cờ removeOnCompletion được đặt thành false và fillMode được đặt thành kCAFillModeForwards cũng không hoạt động đối với tôi.

Sau khi tôi áp dụng hoạt hình mới trên một lớp, một đối tượng hoạt hình sẽ đặt lại trạng thái ban đầu và sau đó hoạt ảnh từ trạng thái đó. Những gì phải được thực hiện bổ sung là đặt thuộc tính mong muốn của lớp mô hình theo thuộc tính của lớp trình bày trước khi thiết lập hoạt ảnh mới như vậy:

someLayer.path = ((CAShapeLayer *)[someLayer presentationLayer]).path;
[someLayer addAnimation:someAnimation forKey:@"someAnimation"];

0

Giải pháp đơn giản nhất là sử dụng hình ảnh động ngầm. Điều này sẽ xử lý tất cả những rắc rối đó cho bạn:

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

Nếu bạn muốn tùy chỉnh, ví dụ như thời lượng, bạn có thể sử dụng NSAnimationContext:

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

Lưu ý: Điều này chỉ được thử nghiệm trên macOS.

Tôi ban đầu không thấy bất kỳ hình ảnh động khi làm điều này. Vấn đề là lớp của lớp được hỗ trợ xem không ẩn ý. Để giải quyết vấn đề này, hãy đảm bảo bạn tự thêm một lớp (trước khi đặt chế độ xem thành lớp được hỗ trợ).

Một ví dụ làm thế nào để làm điều này sẽ là:

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

Việc sử dụng self.wantsLayerkhông tạo ra bất kỳ sự khác biệt nào trong thử nghiệm của tôi, nhưng nó có thể có một số tác dụng phụ mà tôi không biết.


0

Đây là một mẫu từ sân chơi:

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. Tạo một mô hình hoạt hình
  2. Đặt vị trí bắt đầu của hình ảnh động (có thể bỏ qua, nó phụ thuộc vào bố cục lớp hiện tại của bạn)
  3. Đặt vị trí kết thúc của hình ảnh động
  4. Đặt thời lượng hoạt hình
  5. Trì hoãn hoạt hình trong một giây
  6. Không được đặt falsethành isRemovedOnCompletion- hãy để Core Animation sạch sau khi hoạt ảnh kết thúc
  7. Đây là phần đầu tiên của mẹo - bạn nói với Core Animation để đặt hoạt hình của bạn đến vị trí bắt đầu (bạn đã đặt ở bước # 2) trước khi hoạt ảnh thậm chí được bắt đầu - kéo dài thời gian ngược lại
  8. Sao chép đối tượng hoạt hình đã chuẩn bị, thêm nó vào lớp và bắt đầu hoạt hình sau khi trì hoãn (bạn đặt ở bước # 5)
  9. Phần thứ hai là đặt vị trí kết thúc chính xác của lớp - sau khi xóa hình động, lớp của bạn sẽ được hiển thị ở đúng vị trí
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.