CABasicAnimation รีเซ็ตเป็นค่าเริ่มต้นหลังจากที่การเคลื่อนไหวเสร็จสมบูรณ์


143

ฉันหมุน CALayer และพยายามหยุดที่ตำแหน่งสุดท้ายหลังจากที่ภาพเคลื่อนไหวเสร็จสมบูรณ์

แต่หลังจากที่ภาพเคลื่อนไหวเสร็จสมบูรณ์มันจะรีเซ็ตเป็นตำแหน่งเริ่มต้น

(เอกสาร xcode กล่าวอย่างชัดเจนว่าแอนิเมชันจะไม่อัปเดตมูลค่าของทรัพย์สิน)

ข้อเสนอแนะใด ๆ วิธีการบรรลุสิ่งนี้


1
นี้เป็นหนึ่งในคำถามเพื่อให้ผู้แปลกที่เกือบทุกคำตอบไม่ถูกต้องอย่างเต็มที่ - เพียงแค่ผิดทั้งหมด คุณเพียงแค่ดู.presentation()เพื่อรับค่า "ขั้นสุดท้ายที่เห็น" ค้นหาคำตอบที่ถูกต้องด้านล่างซึ่งอธิบายด้วยเลเยอร์การนำเสนอ
Fattie

คำตอบ:


285

นี่คือคำตอบมันเป็นการผสมผสานระหว่างคำตอบของฉันกับของกฤษณะ

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

kCAFillModeRemovedค่าเริ่มต้นคือ (ซึ่งเป็นพฤติกรรมการรีเซ็ตที่คุณเห็น)


6
นี่อาจไม่ใช่คำตอบที่ถูกต้อง - ดูความคิดเห็นต่อคำตอบของ Krishnan คุณมักจะต้องการสิ่งนี้และ Krishnan เพียงอันใดอันหนึ่งจะไม่ทำงาน
Adam

19
นี่ไม่ใช่คำตอบที่ถูกต้องโปรดดูคำตอบของ @Leslie Godwin ด้านล่าง
ทำเครื่องหมายอินแกรม

6
@Leslie solution ดีกว่าเพราะเก็บค่าสุดท้ายไว้ในเลเยอร์ไม่ใช่ในแอนิเมชั่น
Matthieu Rouif

1
ระวังเมื่อคุณตั้งค่าลบเมื่อการคอมไพล์เป็นไม่ หากคุณกำลังมอบหมายตัวแทนภาพเคลื่อนไหวให้กับ "ตัวเอง" อินสแตนซ์ของคุณจะถูกเก็บไว้โดยภาพเคลื่อนไหว ดังนั้นหลังจากโทร removeFromSuperview หากคุณลืมที่จะลบ / ทำลายภาพเคลื่อนไหวด้วยตัวเอง dealloc เช่นจะไม่ถูกเรียก คุณจะมีหน่วยความจำรั่ว
emrahgunduz

1
คำตอบ 200 คะแนนขึ้นไปทั้งหมดไม่ถูกต้องสมบูรณ์ที่สุด
Fattie

83

ปัญหาของ RemoveOnCompletion คือองค์ประกอบ UI ไม่อนุญาตให้ผู้ใช้โต้ตอบ

ฉันเทคนิคคือการตั้งค่าจากในภาพเคลื่อนไหวและค่าถึงบนวัตถุ แอนิเมชันจะเติมค่า 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"];

7
ไม่ทำงานหากคุณตั้งความล่าช้าในการเริ่มต้น เช่นalphaAnimation.beginTime = 1; ยังfillModeไม่จำเป็นเท่าที่ฉันสามารถบอกได้ ... ?
Sam

มิฉะนั้นนี่เป็นคำตอบที่ดีคำตอบที่ได้รับการยอมรับจะไม่ยอมให้คุณเปลี่ยนค่าหลังจากที่ภาพเคลื่อนไหวเสร็จสิ้น ...
Sam

2
ฉันยังควรทราบว่าbeginTimeไม่ได้เป็นญาติคุณควรใช้เช่น:CACurrentMediaTime()+1;
แซม

มันคือ "มัน" ไม่ใช่ "มัน" - its-not-its.info ขออภัยถ้ามันเป็นเพียงการพิมพ์ผิด
fatuhoku

6
คำตอบที่ถูกต้อง แต่ไม่จำเป็นต้องfillModeดูเพิ่มเติมในการป้องกันเลเยอร์จากการจัดเรียงกลับไปที่ค่าดั้งเดิมเมื่อใช้ CAAnimations อย่างชัดเจน
likid1412

16

ตั้งค่าคุณสมบัติต่อไปนี้:

animationObject.removedOnCompletion = NO;

@Nilesh: ยอมรับคำตอบของคุณ นั่นจะทำให้ผู้ใช้ SO รายอื่นมั่นใจในคำตอบของคุณ
Krishnan

7
ฉันต้องใช้ทั้ง. fillMode = kCAFillModeForwards และ .removedOnCompletion = ไม่ ขอบคุณ!
William Rust

12

เพียงวางไว้ในรหัสของคุณ

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

12

คุณก็สามารถตั้งค่าที่สำคัญของ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

คุณสามารถมีข้อมูลเพิ่มเติมเกี่ยวกับเซสชันวิดีโอ WWDC Apple 421 - สิ่งจำเป็นสำหรับภาพเคลื่อนไหวหลัก (กลางวิดีโอ)


1
ดูเหมือนว่าวิธีที่ถูกต้องในการทำเช่นนี้ นิเมชั่นจะถูกลบออกตามธรรมชาติ (เริ่มต้น) someLayer.position = endPosition;และเมื่อภาพเคลื่อนไหวเสร็จสมบูรณ์ก็จะกลับสู่ค่าที่ตั้งก่อนที่จะเริ่มต้นการเคลื่อนไหวโดยเฉพาะบรรทัดนี้ และขอบคุณสำหรับการอ้างอิงถึง WWDC!
bauerMusic

10

CALayer มีเลเยอร์โมเดลและเลเยอร์การนำเสนอ ระหว่างภาพเคลื่อนไหวเลเยอร์การนำเสนอจะอัปเดตโดยอิสระจากโมเดล เมื่อภาพเคลื่อนไหวเสร็จสมบูรณ์เลเยอร์การนำเสนอจะได้รับการอัปเดตด้วยค่าจากแบบจำลอง หากคุณต้องการหลีกเลี่ยงการกระโดดสั่นสะเทือนหลังจากที่ภาพเคลื่อนไหวสิ้นสุดลงกุญแจสำคัญคือการทำให้ทั้งสองเลเยอร์ซิงค์กัน

หากคุณทราบค่าสิ้นสุดคุณสามารถตั้งค่ารุ่นได้โดยตรง

self.view.layer.opacity = 1;

แต่ถ้าคุณมีภาพเคลื่อนไหวที่คุณไม่ทราบตำแหน่งสิ้นสุด (เช่นจางช้าที่ผู้ใช้สามารถหยุดชั่วคราวแล้วย้อนกลับ) คุณสามารถสอบถามเลเยอร์การนำเสนอโดยตรงเพื่อค้นหาค่าปัจจุบันแล้วอัปเดตโมเดล

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

การดึงค่าจากเลเยอร์การนำเสนอนั้นมีประโยชน์อย่างยิ่งสำหรับการปรับขนาดหรือการหมุนเวียนคีย์พา ธ (เช่นtransform.scale, transform.rotation)


8

โดยไม่ต้องใช้ 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)
}

3
นี่น่าจะเป็นคำตอบที่ใช้งานได้ดีที่สุด
แรมบัสซ่า

ตกลง หมายเหตุความคิดเห็น @ ayman ที่คุณต้องกำหนดคุณสมบัติ. fromValue ให้เป็นค่าเริ่มต้น มิฉะนั้นคุณจะเขียนทับค่าเริ่มต้นในโมเดลและจะมีภาพเคลื่อนไหว
Jeff Collier

คำตอบเดียวและ @ jason-moore นี้ดีกว่าคำตอบที่ยอมรับ ในคำตอบที่ยอมรับการเคลื่อนไหวจะหยุดชั่วคราว แต่ค่าใหม่ไม่ได้ถูกตั้งค่าเป็นคุณสมบัติ สิ่งนี้อาจส่งผลให้เกิดพฤติกรรมที่ไม่พึงประสงค์
laka

8

ดังนั้นปัญหาของฉันคือฉันพยายามหมุนวัตถุบนท่าทางแพนและดังนั้นฉันจึงมีภาพเคลื่อนไหวที่เหมือนกันหลายอย่างในแต่ละการเคลื่อนไหว ผมมีทั้ง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 = CAMediaTimingFillMode.forwards

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

7

เพียงตั้งค่า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 ไม่เปลี่ยนแปลง)


7

ภาพเคลื่อนไหวแกนรักษาสองลำดับชั้นชั้น: ชั้นรุ่นและชั้นนำเสนอ เมื่อภาพเคลื่อนไหวกำลังดำเนินการเลเยอร์โมเดลจะไม่เสียหายและทำให้เป็นค่าเริ่มต้น โดยค่าเริ่มต้นภาพเคลื่อนไหวจะถูกลบออกเมื่อมันเสร็จสมบูรณ์ จากนั้นเลเยอร์การนำเสนอจะกลับไปเป็นค่าของเลเยอร์โมเดล

เพียงตั้งค่า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;
[CATransaction commit];

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

ขอบคุณ! นี่ควรเป็นคำตอบที่ได้รับการยอมรับเพราะมันอธิบายฟังก์ชันการทำงานนี้ได้อย่างถูกต้องแทนที่จะใช้เพียงเครื่องช่วยฟังเพื่อให้ทำงานได้
bplattenburg

6

งานนี้:

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

สุดท้ายถ้าคุณใช้ 'removedOnCompletion = false' มันจะรั่ว CAAnimations จนกว่าเลเยอร์จะถูกกำจัดในที่สุด - หลีกเลี่ยง


คำตอบที่ดีและเคล็ดลับที่ดี ขอบคุณสำหรับความคิดเห็นที่ถูกลบออก
iWheelBuy

4

@Leslie Godwin คำตอบนั้นไม่ค่อยดีนัก "self.view.layer.opacity = 1;" เสร็จสิ้นทันที (ใช้เวลาประมาณหนึ่งวินาที) โปรดแก้ไข alphaAnimation.duration เป็น 10.0 ถ้าคุณมีข้อสงสัย คุณต้องลบบรรทัดนี้

ดังนั้นเมื่อคุณแก้ไข fillMode เป็น kCAFillModeForwards และ removeOnCompletion เป็น 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];
        }
    }
}

1

ดูเหมือนว่าการตั้งค่าสถานะ RemoveOnCompletion ถูกตั้งค่าเป็นเท็จและ fillMode ตั้งค่าเป็น kCAFillModeForwards ไม่ทำงานสำหรับฉันเช่นกัน

หลังจากที่ฉันใช้แอนิเมชั่นใหม่บนเลเยอร์แล้วอนิเมชั่นอ็อบเจ็กต์จะถูกรีเซ็ตเป็นสถานะเริ่มต้นจากนั้นก็เคลื่อนไหวจากสถานะนั้น สิ่งที่ต้องทำเพิ่มเติมคือการตั้งค่าคุณสมบัติที่ต้องการของเลเยอร์โมเดลตามคุณสมบัติของเลเยอร์การนำเสนอก่อนที่จะตั้งค่าภาพเคลื่อนไหวใหม่ดังนี้:

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

0

ทางออกที่ง่ายที่สุดคือการใช้ภาพเคลื่อนไหวโดยนัย สิ่งนี้จะจัดการกับปัญหาทั้งหมดให้คุณ:

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ไม่ได้สร้างความแตกต่างในการทดสอบของฉัน แต่อาจมีผลข้างเคียงบางอย่างที่ฉันไม่ทราบ


0

นี่คือตัวอย่างจากสนามเด็กเล่น:

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. ส่วนที่สองคือการกำหนดตำแหน่งสิ้นสุดที่ถูกต้องของเลเยอร์ - หลังจากที่ภาพเคลื่อนไหวถูกลบเลเยอร์ของคุณจะแสดงในตำแหน่งที่ถูกต้อง
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.