Jak stworzyć niestandardową funkcję wygładzania za pomocą Core Animation?

115

Ja animowania CALayerwzdłuż CGPath(QuadCurve) całkiem ładnie w iOS. Ale chciałbym użyć ciekawszej funkcji wygładzania niż kilka dostarczonych przez Apple (EaseIn / EaseOut itp.). Na przykład funkcja odbijania lub elastyczności.

Te rzeczy można zrobić z MediaTimingFunction (bezier):

wprowadź opis obrazu tutaj

Ale chciałbym stworzyć bardziej złożone funkcje czasowe . Problem polega na tym, że synchronizacja mediów wydaje się wymagać sześciennego beziera, który nie jest wystarczająco silny, aby stworzyć takie efekty:

wprowadź opis obrazu tutaj
(źródło: sparrow-framework.org )

Kod , aby utworzyć powyżej jest dość proste w innych ram, co sprawia, że to bardzo frustrujące. Należy zauważyć, że krzywe odwzorowują czas wejściowy do czasu wyjściowego (krzywa Tt), a nie krzywe czas-pozycja. Na przykład, easyOutBounce (T) = t zwraca nowe t . Następnie to t jest używane do wykreślenia ruchu (lub jakiejkolwiek właściwości, którą powinniśmy ożywić).

Chciałbym więc stworzyć złożony zwyczaj, CAMediaTimingFunctionale nie mam pojęcia, jak to zrobić, a jeśli to w ogóle możliwe? Czy są jakieś alternatywy?

EDYTOWAĆ:

Oto konkretny przykład kroków. Bardzo pouczające :)

  1. Chcę animować obiekt wzdłuż linii od punktu a do b , ale chcę, aby „odbijał” swój ruch wzdłuż linii, używając krzywej EasyOutBounce powyżej. Oznacza to, że będzie podążał dokładnie za linią od a do b , ale będzie przyspieszać i zwalniać w bardziej złożony sposób niż jest to możliwe przy użyciu obecnej funkcji CAMediaTimingFunction opartej na bezierze.

  2. Zróbmy dla tej linii dowolny dowolny ruch krzywej określony przez CGPath. Powinien nadal poruszać się wzdłuż tej krzywej, ale powinien przyspieszać i zwalniać w taki sam sposób, jak na przykładzie linii.

Teoretycznie myślę, że powinno to działać tak:

Opiszmy krzywą ruchu jako animację ruchu klatki kluczowej (t) = p , gdzie t to czas [0..1], p to pozycja obliczona w czasie t . Zatem move (0) zwraca pozycję na początku krzywej, przesuwa (0,5) dokładnie w środku i przesuwa (1) na końcu. Użycie funkcji pomiaru czasu time (T) = t do podania wartości t dla ruchu powinno dać mi to, czego chcę. Aby uzyskać efekt odbijania, funkcja pomiaru czasu powinna zwracać te same wartości t dla czasu (0,8) i czasu (0,8)(tylko przykład). Wystarczy wymienić funkcję pomiaru czasu, aby uzyskać inny efekt.

(Tak, odbijanie linii można wykonać, tworząc i łącząc cztery segmenty linii, które poruszają się tam iz powrotem, ale nie powinno to być konieczne. W końcu jest to tylko prosta funkcja liniowa, która odwzorowuje wartości czasu na pozycje.)

Mam nadzieję, że mam tutaj sens.

Martin Wickman
źródło
pamiętaj, że (jak to często bywa w przypadku SO), to bardzo stare pytanie ma teraz bardzo nieaktualne odpowiedzi . pamiętaj, aby przewinąć w dół do wszystkich aktualnych odpowiedzi. (Godne uwagi: cubic-bezier.com !)
Fattie

Odpowiedzi:

48

Znalazłem to:

Cocoa with Love - parametryczne krzywe przyspieszenia w animacji podstawowej

Ale myślę, że można to trochę uprościć i uczynić bardziej czytelnym, używając bloków. Możemy więc zdefiniować kategorię w CAKeyframeAnimation, która wygląda mniej więcej tak:

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

Teraz użycie będzie wyglądać mniej więcej tak:

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

Wiem, że może to nie być tak proste, jak chciałeś, ale to początek.

Jesse Crossen
źródło
1
Przyjąłem odpowiedź Jessego Crossena i trochę ją rozwinąłem. Możesz go użyć na przykład do animacji CGPoints i CGSize. Począwszy od iOS 7, możesz również używać dowolnych funkcji czasu z animacjami UIView. Możesz sprawdzić wyniki na github.com/jjackson26/JMJParametricAnimation
JJ Jackson,
@JJJackson Wspaniała kolekcja animacji! Dzięki za ich zebranie
Codey
33

Od iOS 10 stało się możliwe łatwiejsze tworzenie niestandardowych funkcji pomiaru czasu przy użyciu dwóch nowych obiektów czasowych.

1) UICubicTimingParameters pozwala zdefiniować sześcienną krzywą Béziera jako funkcję krzywej wygładzania .

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)

lub po prostu używając punktów kontrolnych podczas inicjalizacji animatora

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) 

Ta niesamowita usługa pomoże wybrać punkty kontrolne dla twoich krzywych.

2) UISpringTimingParameters pozwala programistom manipulować współczynnikiem tłumienia , masą , sztywnością i prędkością początkową w celu uzyskania pożądanego zachowania sprężyny.

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)

Parametr czasu trwania jest nadal wyświetlany w Animatorze, ale zostanie zignorowany w przypadku synchronizacji wiosennej.

Jeśli te dwie opcje nie są wystarczające, możesz również zaimplementować własną krzywą taktowania, potwierdzając protokół UITimingCurveProvider .

Więcej szczegółów, jak tworzyć animacje z różnymi parametrami czasowymi, można znaleźć w dokumentacji .

Także, proszę zobaczyć Advances in UIKit Animacje i przejścia prezentacji z WWDC 2016.

Olga Konoreva
źródło
Przykłady użycia funkcji pomiaru czasu można znaleźć w repozytorium iOS10-animations-demo .
Olga Konoreva,
Czy możesz rozwinąć temat „Jeśli te dwie opcje nie są wystarczające, możesz również zaimplementować własną krzywą taktowania, potwierdzając protokół UITimingCurveProvider”. ?
Christian Schnorr
Niesamowite! A CoreAnimationwersja UISpringTimingParameters( CASpringAnimation) jest obsługiwana z powrotem do iOS 9!
Rhythmic Fistman
@OlgaKonoreva, usługa cubic-bezier.com jest NIESAMOWICIE BEZCENNA I WSPANIAŁA . Nadzieję, że jesteś stil użytkownikowi SO - przesyłam Ci nagrodę fat-ass do powiedzenia dzięki :)
fattie
@ChristianSchnorr Myślę, że to, czego nie ma odpowiedź, to zdolność a UITimingCurveProviderdo nie tylko reprezentowania animacji sześciennej lub sprężynowej, ale także do przedstawiania animacji, która łączy zarówno sześcienne, jak i sprężynowe UITimingCurveType.composed.
David James
14

Sposobem na stworzenie niestandardowej funkcji pomiaru czasu jest użycie metody functionWithControlPoints :::: factory w CAMediaTimingFunction (istnieje również odpowiednia metoda initWithControlPoints :::: init). To, co to robi, to utworzenie krzywej Béziera dla funkcji pomiaru czasu. Nie jest to dowolna krzywa, ale krzywe Béziera są bardzo mocne i elastyczne. Potrzeba trochę praktyki, aby opanować punkty kontrolne. Wskazówka: większość programów do rysowania może tworzyć krzywe Béziera. Zabawa z nimi da ci wizualną informację zwrotną na temat krzywej, którą reprezentujesz za pomocą punktów kontrolnych.

To łącze prowadzi do dokumentacji firmy Apple. Istnieje krótka, ale przydatna sekcja dotycząca konstruowania funkcji przed kompilacją z krzywych.

Edycja: poniższy kod przedstawia prostą animację odbijania. Aby to zrobić, stworzyłem złożoną funkcję pomiaru czasu ( wartości i właściwości czasowe NSArray) i nadałem każdemu segmentowi animacji inną długość czasu ( właściwość keytimes ). W ten sposób można komponować krzywe Béziera, aby uzyskać bardziej wyrafinowane taktowanie animacji. To jest dobry artykuł na temat tego typu animacji z ładnym przykładowym kodem.

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

}
słaby
źródło
Tak, to prawda. Możesz łączyć wiele funkcji czasowych, używając wartości i właściwości timingFunctions CAKeyframeAnimation, aby osiągnąć to, co chcesz. Opracuję przykład i wstawię trochę kod.
święta
Dziękuję za wysiłek i punkty za próbę :) Takie podejście może działać w teorii, ale musi być dostosowane do każdego zastosowania. Szukam ogólnego rozwiązania, które działa dla wszystkich animacji jako funkcja czasu. Szczerze mówiąc, nie wygląda to tak dobrze, łącząc linie i krzywe. Cierpi na tym również przykład „jack in a box”.
Martin Wickman
1
Możesz określić CGPathRef zamiast tablicy wartości do animacji klatki kluczowej, ale ostatecznie Felz ma rację - CAKeyframeAnimation i timingFunctions dadzą ci wszystko, czego potrzebujesz. Nie jestem pewien, dlaczego uważasz, że nie jest to „rozwiązanie ogólne”, które musi być „dostosowane do każdego zastosowania”. Animację klatki kluczowej tworzysz raz metodą fabryczną lub coś w tym rodzaju, a następnie możesz dodawać tę animację do dowolnej warstwy w kółko, tyle razy, ile chcesz. Dlaczego miałby być dostosowany do każdego zastosowania? Wydaje mi się, że nie różni się to od twojego wyobrażenia o tym, jak powinny działać funkcje czasowe.
Matt Long
1
Och, chłopaki, to jest teraz 4 lata później, ale jeśli functionWithControlPoints :::: może całkowicie wykonywać sprężyste / sprężyste animacje. Użyj go w połączeniu z cubic-bezier.com/#.6,1.87,.63,.74
Matt Foley
10

Nie jestem pewien, czy nadal szukasz, ale PRTween wygląda dość imponująco pod względem możliwości wykraczania poza to, co Core Animation daje ci po wyjęciu z pudełka, w szczególności niestandardowe funkcje czasowe. Zawiera również wiele - jeśli nie wszystkie - popularnych krzywych łagodzenia, które zapewniają różne frameworki internetowe.

CIFilter
źródło
2

Szybką implementacją jest TFAnimation . Demo to animacja krzywej grzechu . UżywajTFBasicAnimation tak samo, jak CABasicAnimationz wyjątkiem przypisania timeFunctionz blokiem innym niż timingFunction.

Kluczowym punktem jest podklasą CAKeyframeAnimationi ramek oblicz pozycja o timeFunctionw 1 / 60fpss przedział .Po wszystko dodać wszystkie obliczoną wartość valuesod CAKeyframeAnimationi do przedziału czasu przez keyTimeszbyt.

Xingxing
źródło
2

Stworzyłem podejście oparte na blokach, które generuje grupę animacji z wieloma animacjami.

Każda animacja, dla każdej właściwości, może wykorzystywać 1 z 33 różnych krzywych parametrycznych, funkcję pomiaru czasu rozpadu z prędkością początkową lub niestandardową sprężynę skonfigurowaną zgodnie z Twoimi potrzebami.

Po wygenerowaniu grupa jest zapisywana w pamięci podręcznej w widoku i może zostać wyzwolona za pomocą AnimationKey, z animacją lub bez. Po uruchomieniu animacja jest odpowiednio synchronizowana z wartościami warstwy prezentacji i odpowiednio stosowana.

Framework można znaleźć tutaj FlightAnimator

Oto przykład poniżej:

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

Aby uruchomić animację

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
AntonTheDev
źródło