Làm thế nào để tạo chức năng nới lỏng tùy chỉnh với Core Animation?


115

Tôi đang tạo hoạt ảnh CALayerdọc theo CGPath(QuadCurve) khá độc đáo trong iOS. Nhưng tôi muốn sử dụng một chức năng nới lỏng thú vị hơn một vài chức năng được cung cấp bởi Apple (EaseIn / EaseOut, v.v.). Ví dụ, một chức năng trả lại hoặc đàn hồi.

Những điều này có thể thực hiện với MediaTimingFunction (bezier):

nhập mô tả hình ảnh ở đây

Nhưng tôi muốn tạo các hàm định thời phức tạp hơn. Vấn đề là thời gian phương tiện dường như yêu cầu một bezier khối không đủ mạnh để tạo ra những hiệu ứng này:

nhập mô tả hình ảnh ở đây
(nguồn: sparrow-framework.org )

Các đang tạo ra ở trên là đủ đơn giản trong các khuôn khổ khác, mà làm cho bực bội này rất. Lưu ý rằng các đường cong ánh xạ thời gian đầu vào với thời gian đầu ra (đường cong Tt) chứ không phải đường cong vị trí thời gian. Ví dụ, easyOutBounce (T) = t trả về một t mới . Sau đó, t đó được sử dụng để vẽ biểu đồ chuyển động (hoặc bất kỳ thuộc tính nào mà chúng ta nên tạo hiệu ứng).

Vì vậy, tôi muốn tạo một tùy chỉnh phức tạp CAMediaTimingFunctionnhưng tôi không biết làm thế nào để làm điều đó, hoặc nếu nó thậm chí có thể? Có bất kỳ lựa chọn thay thế nào không?

BIÊN TẬP:

Đây là một ví dụ cụ thể về các bước. Rất giáo dục :)

  1. Tôi muốn tạo hoạt ảnh cho một đối tượng dọc theo một đường từ điểm a đến b , nhưng tôi muốn nó "bật" chuyển động của nó dọc theo đường bằng cách sử dụng đường cong easyOutBounce ở trên. Điều này có nghĩa là nó sẽ đi theo đường chính xác từ a đến b , nhưng sẽ tăng tốc và giảm tốc theo cách phức tạp hơn những gì có thể sử dụng CAMediaTimingFunction dựa trên bezier hiện tại.

  2. Cho phép tạo đường đó bất kỳ chuyển động đường cong tùy ý nào được chỉ định với CGPath. Nó vẫn sẽ di chuyển dọc theo đường cong đó, nhưng nó sẽ tăng tốc và giảm tốc theo cùng một cách như trong ví dụ về đường thẳng.

Về lý thuyết, tôi nghĩ nó sẽ hoạt động như thế này:

Cho phép mô tả đường cong chuyển động như một hoạt ảnh khung hình bàn phím di chuyển (t) = p , trong đó t là thời gian [0..1], p là vị trí được tính tại thời điểm t . Vì vậy, di chuyển (0) trả lại vị trí ở đầu đường cong, di chuyển (0,5) chính xác giữa và di chuyển (1) ở cuối. Sử dụng hàm định thời time (T) = t để cung cấp giá trị t cho việc di chuyển sẽ mang lại cho tôi những gì tôi muốn. Đối với hiệu ứng nảy, hàm thời gian phải trả về cùng các giá trị t cho thời gian (0,8)thời gian (0,8)(chỉ là một ví dụ). Chỉ cần thay thế chức năng định thời để có được hiệu ứng khác.

(Có, bạn có thể thực hiện điều chỉnh dòng bằng cách tạo và nối bốn đoạn thẳng qua lại, nhưng điều đó không cần thiết. Xét cho cùng, nó chỉ là một hàm tuyến tính đơn giản ánh xạ các giá trị thời gian đến các vị trí.)

Tôi hy vọng tôi có ý nghĩa ở đây.


lưu ý rằng (như thường lệ trên SO), câu hỏi rất cũ này hiện có các câu trả lời rất lỗi thời .. hãy nhớ cuộn xuống các câu trả lời hiện tại. (Rất đáng chú ý: Cub-bezier.com !)
Fattie

Câu trả lời:


48

Tôi đã tìm thấy cái này:

Cocoa with Love - Đường cong gia tốc tham số trong Core Animation

Nhưng tôi nghĩ nó có thể được làm cho đơn giản hơn một chút và dễ đọc hơn bằng cách sử dụng các khối. Vì vậy, chúng tôi có thể xác định một danh mục trên CAKeyframeAnimation trông giống như sau:

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

Bây giờ việc sử dụng sẽ giống như sau:

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

Tôi biết nó có thể không hoàn toàn đơn giản như những gì bạn muốn, nhưng đó là một sự khởi đầu.


1
Tôi tiếp thu câu trả lời của Jesse Crossen và mở rộng nó một chút. Bạn có thể sử dụng nó để tạo hiệu ứng cho CGPoint và CGSize chẳng hạn. Kể từ iOS 7, bạn cũng có thể sử dụng các chức năng thời gian tùy ý với hình ảnh động UIView. Bạn có thể xem kết quả tại github.com/jjackson26/JMJParametricAnimation
JJ Jackson

@JJJackson Bộ sưu tập hình ảnh động tuyệt vời! Cảm ơn bạn đã thu thập chúng
Codey

33

Từ iOS 10, có thể tạo chức năng thời gian tùy chỉnh dễ dàng hơn bằng cách sử dụng hai đối tượng thời gian mới.

1) UICubicTimingParameters cho phép xác định đường cong Bézier lập phương như một hàm nới lỏng.

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)

hoặc chỉ đơn giản là sử dụng các điểm kiểm soát khi khởi chạy trình tạo hoạt ảnh

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) 

Dịch vụ tuyệt vời này sẽ giúp chọn điểm kiểm soát cho các đường cong của bạn.

2) UISpringTimingParameters cho phép các nhà phát triển thao tác tỷ lệ giảm chấn , khối lượng , độ cứngvận tốc ban đầu để tạo ra hành vi lò xo mong muốn.

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)

Tham số thời lượng vẫn được hiển thị trong Animator, nhưng sẽ bị bỏ qua cho thời gian mùa xuân.

Nếu hai tùy chọn này không đủ, bạn cũng có thể triển khai đường cong thời gian của riêng mình bằng cách xác nhận với giao thức UITimingCurveProvider .

Chi tiết hơn, cách tạo ảnh động với các thông số thời gian khác nhau, bạn có thể tìm thấy trong tài liệu .

Ngoài ra, vui lòng xem Những tiến bộ trong bản trình bày Hoạt ảnh và Chuyển tiếp UIKit từ WWDC 2016.


Bạn có thể tìm thấy các ví dụ về việc sử dụng chức năng Định thời gian trong kho lưu trữ iOS10-animations-demo .
Olga Konoreva

Bạn có thể vui lòng giải thích thêm về "Nếu hai tùy chọn này không đủ, bạn cũng có thể triển khai đường cong thời gian của riêng mình bằng cách xác nhận với giao thức UITimingCurveProvider." ?
Christian Schnorr

Tuyệt vời! Và CoreAnimationphiên bản của UISpringTimingParameters( CASpringAnimation) được hỗ trợ trở lại iOS 9!
Rhythmic Fistman

@OlgaKonoreva, dịch vụ khối-bezier.com CÓ GIÁ RẤT NHIỀU VÀ TUYỆT VỜI . Hy vọng bạn stil người dùng SO - gửi cho bạn một tiền thưởng béo ass để nói lời cảm ơn :)
Fattie

@ChristianSchnorr Tôi nghĩ câu trả lời đang bị bỏ qua là khả năng của a UITimingCurveProviderkhông chỉ đại diện cho một hình khối hoặc một hoạt ảnh mùa xuân mà còn thể hiện một hoạt ảnh kết hợp cả hình khối mùa xuân UITimingCurveType.composed.
David James

14

Một cách để tạo một hàm định thời gian tùy chỉnh là sử dụng phương thức factory functionWithControlPoints :::: trong CAMediaTimingFunction (cũng có một phương thức initWithControlPoints :::: tương ứng). Điều này làm là tạo ra một đường cong Bézier cho hàm thời gian của bạn. Nó không phải là một đường cong tùy ý, nhưng đường cong Bézier rất mạnh mẽ và linh hoạt. Cần thực hành một chút để nắm được các điểm kiểm soát. Một mẹo: hầu hết các chương trình vẽ đều có thể tạo ra các đường cong Bézier. Chơi với những thứ đó sẽ cung cấp cho bạn phản hồi trực quan về đường cong mà bạn đang biểu diễn với các điểm kiểm soát.

Các liên kết này điểm đến tài liệu táo. Có một phần ngắn nhưng hữu ích về cách các hàm tạo trước được xây dựng từ các đường cong.

Chỉnh sửa: Đoạn mã sau hiển thị một hoạt ảnh trả lại đơn giản. Để làm như vậy, tôi đã tạo một hàm định thời tổng hợp ( giá trị và thuộc tính định thời NSArray) và cung cấp cho mỗi phân đoạn của hoạt ảnh một độ dài thời gian khác nhau ( thuộc tính keytimes ). Bằng cách này, bạn có thể tạo các đường cong Bézier để tạo thời gian phức tạp hơn cho hoạt ảnh. Đây là một bài viết hay về loại hình ảnh động này với mã mẫu đẹp.

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

}

Đúng là như vậy. Bạn có thể kết hợp nhiều chức năng định thời bằng cách sử dụng các giá trị và thuộc tính timingFunctions của CAKeyframeAnimation để đạt được những gì bạn muốn. Tôi sẽ làm một ví dụ và đưa mã vào một chút.
mờ nhạt

Cảm ơn bạn đã nỗ lực và điểm cho việc thử :) Cách tiếp cận đó có thể hoạt động trên lý thuyết, nhưng nó cần được tùy chỉnh cho mỗi lần sử dụng. Tôi đang tìm một giải pháp chung hoạt động cho tất cả các hình ảnh động dưới dạng chức năng định thời gian. Thành thật mà nói, nó không có vẻ tốt là các đường nối và đường cong với nhau. Ví dụ "jack in a box" cũng mắc phải điều này.
Martin Wickman

1
Bạn có thể chỉ định CGPathRef thay vì một mảng giá trị cho hoạt ảnh khung hình chính, nhưng cuối cùng thì Felz chính xác - CAKeyframeAnimation và timingFunctions sẽ cung cấp cho bạn mọi thứ bạn cần. Tôi không rõ tại sao bạn nghĩ đó không phải là "giải pháp chung" mà phải được "tùy chỉnh cho mỗi lần sử dụng". Bạn tạo hoạt ảnh khung hình chính một lần trong một phương pháp gốc hoặc một cái gì đó và sau đó bạn có thể thêm hoạt ảnh đó vào bất kỳ lớp nào nhiều lần tùy thích. Tại sao nó cần được tùy chỉnh cho mỗi lần sử dụng? Có vẻ như đối với tôi, nó không khác gì quan niệm của bạn về cách các hàm thời gian phải hoạt động.
Matt Long ngày

1
Các bạn ơi, bây giờ đã muộn 4 năm, nhưng nếu functionWithControlPoints :::: hoàn toàn có thể tạo ra các hình ảnh động nảy lửa / mùa xuân. Sử dụng nó trong conjuction với cubic-bezier.com/#.6,1.87,.63,.74
Matt Foley

10

Không chắc bạn vẫn đang xem hay không, nhưng PRTween trông khá ấn tượng về khả năng vượt xa những gì Core Animation mang lại cho bạn, đáng chú ý nhất là các chức năng thời gian tùy chỉnh. Nó cũng được đóng gói với nhiều — nếu không phải là tất cả — các đường cong nới lỏng phổ biến mà các khuôn khổ web khác nhau cung cấp.


2

Một phiên bản triển khai nhanh chóng là TFAnimation . Bản demo là hoạt ảnh đường cong sin. Sử dụngTFBasicAnimation giống như CABasicAnimationngoại trừ gán timeFunctionvới một khối khác timingFunction.

Điểm mấu chốt là lớp con CAKeyframeAnimationvà tính toán khung vị trí bằng timeFunctiontrong1 / 60fps s khoảng .Sau tất cả thêm tất cả các giá trị tính toán để valuescủa CAKeyframeAnimationvà thời gian bằng khoảng thời gian để keyTimesquá.


2

Tôi đã tạo một cách tiếp cận dựa trên khối, tạo ra một nhóm hoạt ảnh, với nhiều hoạt ảnh.

Mỗi hoạt ảnh, mỗi thuộc tính, có thể sử dụng 1 trong 33 đường cong tham số khác nhau, chức năng Định thời gian giảm dần với vận tốc ban đầu hoặc một lò xo tùy chỉnh được định cấu hình theo nhu cầu của bạn.

Khi nhóm được tạo, nhóm được lưu vào bộ nhớ cache trên Chế độ xem và có thể được kích hoạt bằng AnimationKey, có hoặc không có hoạt ảnh. Sau khi được kích hoạt, hoạt ảnh sẽ được đồng bộ hóa theo các giá trị của lớp trình bày và được áp dụng tương ứng.

Khung có thể được tìm thấy tại đây FlightAnimator

Đây là một ví dụ dưới đây:

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

Để kích hoạt hoạt ảnh

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
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.