จะสร้างฟังก์ชันการค่อยๆเปลี่ยนแบบกำหนดเองด้วย Core Animation ได้อย่างไร


115

ฉันกำลังสร้างภาพเคลื่อนไหวCALayerตามCGPath(QuadCurve) ได้ดีทีเดียวใน iOS แต่ฉันต้องการใช้ฟังก์ชั่นการค่อยๆเปลี่ยนที่น่าสนใจกว่าที่Apple มีให้ (EaseIn / EaseOut ฯลฯ ) ตัวอย่างเช่นฟังก์ชันตีกลับหรือยืดหยุ่น

สิ่งเหล่านี้สามารถทำได้กับ MediaTimingFunction (bezier):

ใส่คำอธิบายภาพที่นี่

แต่ฉันต้องการสร้างฟังก์ชันจับเวลาที่ซับซ้อนกว่านี้ ปัญหาคือเวลาของสื่อดูเหมือนจะต้องใช้ลูกบาศก์เบซิเอร์ซึ่งไม่มีประสิทธิภาพเพียงพอที่จะสร้างเอฟเฟกต์เหล่านี้:

ใส่คำอธิบายภาพที่นี่
(ที่มา: sparrow-framework.org )

รหัสเพื่อสร้างดังกล่าวข้างต้นก็เพียงพอที่เรียบง่ายในกรอบอื่น ๆ ซึ่งจะทำให้เรื่องนี้น่าผิดหวังมาก โปรดทราบว่าเส้นโค้งกำลังทำแผนที่เวลาอินพุตกับเวลาเอาต์พุต (เส้นโค้ง Tt) ไม่ใช่เส้นโค้งตำแหน่งเวลา ยกตัวอย่างเช่นeaseOutBounce (T) = Tส่งกลับใหม่ที จากนั้นtจะถูกใช้เพื่อพล็อตการเคลื่อนไหว (หรือคุณสมบัติใด ๆ ที่เราควรทำให้เคลื่อนไหว)

ดังนั้นฉันต้องการสร้างแบบกำหนดเองที่ซับซ้อนCAMediaTimingFunctionแต่ฉันไม่รู้ว่าจะทำอย่างไรหรือถ้าเป็นไปได้? มีทางเลือกอื่นหรือไม่?

แก้ไข:

นี่คือตัวอย่างที่เป็นรูปธรรมในขั้นตอนต่างๆ น่าศึกษามาก :)

  1. ฉันต้องการทำให้วัตถุเคลื่อนไหวตามเส้นจากจุดaถึงbแต่ฉันต้องการให้วัตถุนั้น "เด้ง" การเคลื่อนที่ไปตามเส้นโดยใช้เส้นโค้งความสะดวกออกด้านบน ซึ่งหมายความว่ามันจะเป็นไปตามเส้นตรงจากaถึงbแต่จะเร่งและชะลอตัวลงด้วยวิธีที่ซับซ้อนกว่าสิ่งที่เป็นไปได้โดยใช้ CAMediaTimingFunction ที่ใช้ bezier ในปัจจุบัน

  2. ช่วยให้เส้นนั้นมีการเคลื่อนไหวของเส้นโค้งตามอำเภอใจที่ระบุด้วย CGPath มันควรจะยังคงเคลื่อนที่ไปตามเส้นโค้งนั้น แต่ควรเร่งความเร็วและชะลอตัวลงเช่นเดียวกับในตัวอย่างเส้น

ในทางทฤษฎีฉันคิดว่ามันควรจะเป็นดังนี้:

ช่วยอธิบายเส้นโค้งการเคลื่อนไหวเป็นคีย์เฟรมภาพเคลื่อนไหวย้าย (t) = Pที่เสื้อเป็นเวลา [0..1], Pคือตำแหน่งคำนวณได้ในเวลาที ดังนั้นย้าย (0)คืนตำแหน่งที่จุดเริ่มต้นของเส้นโค้งย้าย (0.5)ตรงกลางที่แน่นอนและย้าย (1)ที่จุดสิ้นสุด การใช้ฟังก์ชันจับเวลาtime (T) = tเพื่อให้ค่าtสำหรับการเคลื่อนที่ควรให้สิ่งที่ฉันต้องการ สำหรับเอฟเฟกต์การตีกลับฟังก์ชันจับเวลาควรส่งคืนค่าtเดียวกันสำหรับเวลา (0.8)และเวลา (0.8)(เป็นเพียงตัวอย่าง) เพียงแค่เปลี่ยนฟังก์ชันจับเวลาเพื่อให้ได้เอฟเฟกต์ที่แตกต่างกัน

(ใช่เป็นไปได้ที่จะทำการตีกลับบรรทัดโดยการสร้างและรวมส่วนของบรรทัดสี่ส่วนซึ่งกลับไปกลับมา แต่ก็ไม่ควรจำเป็นท้ายที่สุดมันเป็นเพียงฟังก์ชันเชิงเส้นธรรมดาที่จะจับคู่ค่าเวลากับตำแหน่ง)

ฉันหวังว่าฉันจะเข้าใจที่นี่


โปรดทราบว่า (เช่นเดียวกับใน SO) คำถามเก่า ๆ นี้มีคำตอบที่ล้าสมัยมากแล้ว .. อย่าลืมเลื่อนลงไปที่คำตอบปัจจุบันที่น่าสนใจ (เด่นมาก: cubic-bezier.com !)
Fattie

คำตอบ:


48

ฉันพบสิ่งนี้:

Cocoa with Love - เส้นโค้งการเร่งความเร็วพาราเมตริกใน Core Animation

แต่ฉันคิดว่ามันสามารถทำให้ง่ายขึ้นและอ่านง่ายขึ้นโดยใช้บล็อก ดังนั้นเราจึงสามารถกำหนดหมวดหมู่บน CAKeyframeAnimation ที่มีลักษณะดังนี้:

CAKeyframeAnimation + Parametric.h:

// this should be a function that takes a time value between 
//  0.0 and 1.0 (where 0.0 is the beginning of the animation
//  and 1.0 is the end) and returns a scale factor where 0.0
//  would produce the starting value and 1.0 would produce the
//  ending value
typedef double (^KeyframeParametricBlock)(double);

@interface CAKeyframeAnimation (Parametric)

+ (id)animationWithKeyPath:(NSString *)path 
      function:(KeyframeParametricBlock)block
      fromValue:(double)fromValue
      toValue:(double)toValue;

CAKeyframeAnimation + Parametric.m:

@implementation CAKeyframeAnimation (Parametric)

+ (id)animationWithKeyPath:(NSString *)path 
      function:(KeyframeParametricBlock)block
      fromValue:(double)fromValue
      toValue:(double)toValue {
  // get a keyframe animation to set up
  CAKeyframeAnimation *animation = 
    [CAKeyframeAnimation animationWithKeyPath:path];
  // break the time into steps
  //  (the more steps, the smoother the animation)
  NSUInteger steps = 100;
  NSMutableArray *values = [NSMutableArray arrayWithCapacity:steps];
  double time = 0.0;
  double timeStep = 1.0 / (double)(steps - 1);
  for(NSUInteger i = 0; i < steps; i++) {
    double value = fromValue + (block(time) * (toValue - fromValue));
    [values addObject:[NSNumber numberWithDouble:value]];
    time += timeStep;
  }
  // we want linear animation between keyframes, with equal time steps
  animation.calculationMode = kCAAnimationLinear;
  // set keyframes and we're done
  [animation setValues:values];
  return(animation);
}

@end

ตอนนี้การใช้งานจะมีลักษณะดังนี้:

// define a parametric function
KeyframeParametricBlock function = ^double(double time) {
  return(1.0 - pow((1.0 - time), 2.0));
};

if (layer) {
  [CATransaction begin];
    [CATransaction 
      setValue:[NSNumber numberWithFloat:2.5]
      forKey:kCATransactionAnimationDuration];

    // make an animation
    CAAnimation *drop = [CAKeyframeAnimation 
      animationWithKeyPath:@"position.y"
      function:function fromValue:30.0 toValue:450.0];
    // use it
    [layer addAnimation:drop forKey:@"position"];

  [CATransaction commit];
}

ฉันรู้ว่ามันอาจจะไม่ง่ายอย่างที่คุณต้องการ แต่มันเป็นการเริ่มต้น


1
ฉันตอบสนองของเจสซีครอสเซนและขยายความอีกเล็กน้อย คุณสามารถใช้เพื่อทำให้ CGPoints เคลื่อนไหวและ CGSize เช่น ใน iOS 7 คุณยังสามารถใช้ฟังก์ชันเวลาตามอำเภอใจกับภาพเคลื่อนไหว UIView คุณสามารถตรวจสอบผลลัพธ์ได้ที่github.com/jjackson26/JMJParametricAnimation
JJ Jackson

@JJJackson คอลเลกชันภาพเคลื่อนไหวที่ยอดเยี่ยม! ขอบคุณที่รวบรวม
Codey

33

จาก iOS 10 คุณสามารถสร้างฟังก์ชันจับเวลาแบบกำหนดเองได้ง่ายขึ้นโดยใช้วัตถุจับเวลาใหม่สองชิ้น

1) UICubicTimingParametersอนุญาตให้กำหนดเส้นโค้งลูกบาศก์เบซิเอร์เป็นฟังก์ชันการค่อยๆเปลี่ยน

let cubicTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.25, y: 0.1), controlPoint2: CGPoint(x: 0.25, y: 1))
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTimingParameters)

หรือเพียงแค่ใช้จุดควบคุมในการเริ่มต้นอนิเมเตอร์

let controlPoint1 = CGPoint(x: 0.25, y: 0.1)
let controlPoint2 = CGPoint(x: 0.25, y: 1)
let animator = UIViewPropertyAnimator(duration: 0.3, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 

บริการที่ยอดเยี่ยมนี้จะช่วยในการเลือกจุดควบคุมสำหรับเส้นโค้งของคุณ

2) UISpringTimingParametersช่วยให้นักพัฒนาจัดการหมาดอัตราส่วน , มวล , ตึงและความเร็วเริ่มต้นในการสร้างพฤติกรรมที่ต้องการในฤดูใบไม้ผลิ

let velocity = CGVector(dx: 1, dy: 0)
let springParameters = UISpringTimingParameters(mass: 1.8, stiffness: 330, damping: 33, initialVelocity: velocity)
let springAnimator = UIViewPropertyAnimator(duration: 0.0, timingParameters: springParameters)

พารามิเตอร์ Duration ยังคงแสดงอยู่ใน Animator แต่จะถูกละเว้นสำหรับเวลาฤดูใบไม้ผลิ

หากตัวเลือกทั้งสองนี้ไม่เพียงพอคุณสามารถใช้เส้นเวลาของคุณเองได้โดยการยืนยันกับโปรโตคอลUITimingCurveProvider

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

นอกจากนี้โปรดดูความก้าวหน้าในการนำเสนอ UIKit Animations and Transitionsจาก WWDC 2016


คุณสามารถหาตัวอย่างของการใช้ฟังก์ชั่นการกำหนดเวลาในiOS10-ภาพเคลื่อนไหวสาธิตพื้นที่เก็บข้อมูล
Olga Konoreva

คุณช่วยอธิบายเพิ่มเติมเกี่ยวกับ "หากตัวเลือกทั้งสองนี้ไม่เพียงพอคุณสามารถใช้เส้นเวลาของคุณเองได้โดยการยืนยันกับโปรโตคอล UITimingCurveProvider" ?
Christian Schnorr

! น่ากลัว และCoreAnimationรุ่นของUISpringTimingParameters( CASpringAnimation) ได้รับการสนับสนุนกลับไปที่ iOS 9!
Rhythmic Fistman

@OlgaKonoreva บริการCubic-bezier.comนั้นไร้ราคาและยอดเยี่ยมมาก หวังว่าคุณจะเป็นผู้ใช้ SO - ส่งเงินรางวัลให้คุณเพื่อกล่าวขอบคุณ :)
Fattie

@ChristianSchnorr ผมคิดว่าสิ่งที่คำตอบคือข้ออ้างที่จะเป็นความสามารถในการที่UITimingCurveProviderจะไม่เพียง แต่เป็นตัวแทนของลูกบาศก์หรือนิเมชั่นฤดูใบไม้ผลิ แต่ยังจะเป็นตัวแทนของภาพเคลื่อนไหวที่รวมทั้งลูกบาศก์และUITimingCurveType.composedฤดูใบไม้ผลิที่ผ่าน
David James

14

วิธีสร้างฟังก์ชันจับเวลาแบบกำหนดเองโดยใช้เมธอดfunctionWithControlPoints :::: จากโรงงานใน CAMediaTimingFunction (มีวิธีการ initWithControlPoints :::: init ที่เกี่ยวข้องด้วย) สิ่งนี้คือการสร้างเส้นโค้งเบเซียร์สำหรับฟังก์ชันจับเวลาของคุณ ไม่ใช่เส้นโค้งตามอำเภอใจ แต่เส้นโค้งBézierนั้นทรงพลังและยืดหยุ่นมาก ต้องใช้เวลาฝึกฝนเล็กน้อยเพื่อให้ได้จุดควบคุม เคล็ดลับ: โปรแกรมวาดรูปส่วนใหญ่สามารถสร้างเส้นโค้งเบซิเอร์ได้ การเล่นกับสิ่งเหล่านี้จะให้ข้อเสนอแนะที่เป็นภาพเกี่ยวกับเส้นโค้งที่คุณแสดงด้วยจุดควบคุม

ลิงค์นี้จุดที่เอกสารของแอปเปิ้ล มีส่วนสั้น ๆ แต่มีประโยชน์เกี่ยวกับวิธีสร้างฟังก์ชันก่อนสร้างจากเส้นโค้ง

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

- (void)viewDidLoad {
    UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)];

    v.backgroundColor = [UIColor redColor];
    CGFloat y = self.view.bounds.size.height;
    v.center = CGPointMake(self.view.bounds.size.width/2.0, 50.0/2.0);
    [self.view addSubview:v];

    //[CATransaction begin];

    CAKeyframeAnimation * animation; 
    animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"]; 
    animation.duration = 3.0; 
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;

    NSMutableArray *values = [NSMutableArray array];
    NSMutableArray *timings = [NSMutableArray array];
    NSMutableArray *keytimes = [NSMutableArray array];

    //Start
    [values addObject:[NSNumber numberWithFloat:25.0]];
    [timings addObject:GetTiming(kCAMediaTimingFunctionEaseIn)];
    [keytimes addObject:[NSNumber numberWithFloat:0.0]];


    //Drop down
    [values addObject:[NSNumber numberWithFloat:y]];
    [timings addObject:GetTiming(kCAMediaTimingFunctionEaseOut)];
    [keytimes addObject:[NSNumber numberWithFloat:0.6]];


    // bounce up
    [values addObject:[NSNumber numberWithFloat:0.7 * y]];
    [timings addObject:GetTiming(kCAMediaTimingFunctionEaseIn)];
    [keytimes addObject:[NSNumber numberWithFloat:0.8]];


    // fihish down
    [values addObject:[NSNumber numberWithFloat:y]];
    [keytimes addObject:[NSNumber numberWithFloat:1.0]];
    //[timings addObject:GetTiming(kCAMediaTimingFunctionEaseIn)];



    animation.values = values;
    animation.timingFunctions = timings;
    animation.keyTimes = keytimes;

    [v.layer addAnimation:animation forKey:nil];   

    //[CATransaction commit];

}

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

ขอบคุณสำหรับความพยายามและคะแนนสำหรับการลอง :) แนวทางดังกล่าวอาจใช้ได้ผลในทางทฤษฎี แต่จำเป็นต้องปรับแต่งสำหรับการใช้งานแต่ละครั้ง ฉันกำลังมองหาวิธีแก้ปัญหาทั่วไปที่ใช้ได้กับภาพเคลื่อนไหวทั้งหมดเป็นฟังก์ชันจับเวลา พูดตามตรงว่ามันไม่ได้ดูดีที่การปะติดปะต่อเส้นและส่วนโค้งเข้าด้วยกัน ตัวอย่าง "แจ็คในกล่อง" ก็มีปัญหาเช่นกัน
Martin Wickman

1
คุณสามารถระบุ CGPathRef แทนอาร์เรย์ของค่าให้กับภาพเคลื่อนไหวคีย์เฟรมได้ แต่ในที่สุด Felz ก็ถูกต้อง - CAKeyframeAnimation และ timingFunctions จะให้ทุกสิ่งที่คุณต้องการ ฉันไม่แน่ใจว่าทำไมคุณถึงคิดว่านั่นไม่ใช่ "โซลูชันทั่วไป" ที่ต้องมีการ "ปรับแต่งสำหรับการใช้งานแต่ละครั้ง" คุณสร้างแอนิเมชั่นคีย์เฟรมหนึ่งครั้งด้วยวิธีการจากโรงงานหรือบางอย่างจากนั้นคุณสามารถเพิ่มแอนิเมชั่นนั้นลงในเลเยอร์ใดก็ได้หลาย ๆ ครั้งตามที่คุณต้องการ ทำไมจึงต้องปรับแต่งสำหรับการใช้งานแต่ละครั้ง สำหรับฉันแล้วดูเหมือนว่ามันไม่ต่างจากความคิดของคุณที่ว่าฟังก์ชันจับเวลาควรทำงานอย่างไร
Matt Long

1
โอ้พวกนี้ตอนนี้ช้าไป 4 ปีแล้ว แต่ถ้า functionWithControlPoints :::: สามารถทำแอนิเมชั่นเด้ง / สปริงได้โดยสิ้นเชิง ใช้ร่วมกับCubic-bezier.com/#.6,1.87,.63,.74
Matt Foley

10

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


2

การใช้งานเวอร์ชันที่รวดเร็วคือTFAnimationการสาธิตเป็นภาพเคลื่อนไหวเส้นโค้งบาปใช้TFBasicAnimationเหมือนกับCABasicAnimationยกเว้นกำหนดtimeFunctionกับบล็อกอื่นที่ไม่ใช่timingFunction.

จุดสำคัญคือประเภทรองCAKeyframeAnimationตำแหน่งและการคำนวณเฟรมโดยtimeFunctionใน1 / 60fpss ช่วงหลังจากทั้งหมดเพิ่มทุกค่าคำนวณvaluesของCAKeyframeAnimationและเวลาโดยช่วงเวลาที่จะkeyTimesมากเกินไป


2

ฉันสร้างวิธีการตามบล็อกซึ่งสร้างกลุ่มแอนิเมชั่นที่มีภาพเคลื่อนไหวหลายภาพ

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

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

สามารถดูกรอบงานได้ที่นี่FlightAnimator

นี่คือตัวอย่างด้านล่าง:

struct AnimationKeys {
    static let StageOneAnimationKey  = "StageOneAnimationKey"
    static let StageTwoAnimationKey  = "StageTwoAnimationKey"
}

...

view.registerAnimation(forKey: AnimationKeys.StageOneAnimationKey, maker:  { (maker) in

    maker.animateBounds(toValue: newBounds,
                     duration: 0.5,
                     easingFunction: .EaseOutCubic)

    maker.animatePosition(toValue: newPosition,
                     duration: 0.5,
                     easingFunction: .EaseOutCubic)

    maker.triggerTimedAnimation(forKey: AnimationKeys.StageTwoAnimationKey,
                           onView: self.secondaryView,
                           atProgress: 0.5, 
                           maker: { (makerStageTwo) in

        makerStageTwo.animateBounds(withDuration: 0.5,
                             easingFunction: .EaseOutCubic,
                             toValue: newSecondaryBounds)

        makerStageTwo.animatePosition(withDuration: 0.5,
                             easingFunction: .EaseOutCubic,
                             toValue: newSecondaryCenter)
     })                    
})

เพื่อทริกเกอร์ภาพเคลื่อนไหว

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.