2013-04-30 11 views
13

Ho animato un UIView in modo che si riduca quando l'utente tocca un pulsante di attivazione e si espande alla sua dimensione originale quando l'utente tocca nuovamente il pulsante. Finora tutto funziona bene. Il problema è che l'animazione richiede del tempo, ad es. 3 secondi. Durante questo periodo voglio comunque che l'utente sia in grado di interagire con l'interfaccia. Quindi, quando l'utente tocca nuovamente il pulsante mentre l'animazione è ancora in corso, l'animazione dovrebbe fermarsi dove si trova e invertire.Come fermare e invertire un'animazione UIView?

In Apple Q & Come ho trovato un modo per mettere in pausa tutte le animazioni subito:

https://developer.apple.com/library/ios/#qa/qa2009/qa1673.html

Ma io non vedo un modo per invertire l'animazione da qui (e tralascio il resto del animazione iniziale). Come posso realizzare questo?

- (IBAction)toggleMeter:(id)sender { 
    if (self.myView.hidden) {   
     self.myView.hidden = NO; 
     [UIView animateWithDuration:3 animations:^{ 
      self.myView.transform = expandMatrix; 
     } completion:nil]; 
    } else { 
     [UIView animateWithDuration:3 animations:^{ 
      self.myView.transform = shrinkMatrix; 
     } completion:^(BOOL finished) { 
      self.myView.hidden = YES; 
     }]; 
    } 
} 

risposta

41

Oltre al seguente (in cui prendiamo lo stato corrente dal livello di presentazione, interrompi l'animazione, reimposta lo stato corrente dal livello di presentazione salvato e avvia la nuova animazione), c'è una soluzione molto più semplice.

Se si eseguono animazioni basate su blocchi, se si desidera interrompere un'animazione e avviare una nuova animazione nelle versioni iOS precedenti alla 8.0, è sufficiente utilizzare l'opzione UIViewAnimationOptionBeginFromCurrentState. (Funzionante in iOS 8, il comportamento predefinito è non solo di partire dallo stato corrente, ma di farlo in un modo che rifletta sia la posizione corrente che la velocità attuale, rendendo in gran parte inutile preoccuparsi di questo problema a tutti . per ulteriori informazioni consultare WWDC 2014 il video Building Interruptible and Responsive Interactions.)

[UIView animateWithDuration:3.0 
         delay:0.0 
        options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction 
       animations:^{ 
        // specify the new `frame`, `transform`, etc. here 
       } 
       completion:NULL]; 

È possibile raggiungere questo fermando l'animazione attuale e iniziare la nuova animazione dalla quale quello attuale lasciato. È possibile farlo con Quartz 2D:

  1. Add QuartzCore.framework to your project se non l'hai già. (Nelle versioni contemporanee di Xcode, spesso non è necessario farlo esplicitamente poiché è automaticamente collegato al progetto.)

  2. Importa l'intestazione necessaria se non l'hai già (ancora una volta, non è necessario nelle versioni contemporanee di Xcode):

    #import <QuartzCore/QuartzCore.h> 
    
  3. avere il vostro codice di interrompere l'animazione esistente:

    [self.subview.layer removeAllAnimations]; 
    
  4. Ottieni un riferimento al livello di presentazione corrente (ovvero lo stato della vista così com'è in questo momento):

    CALayer *currentLayer = self.subview.layer.presentationLayer; 
    
  5. Azzerare il transform (o frame o altro) in base al valore corrente nel presentationLayer:

    self.subview.layer.transform = currentLayer.transform; 
    
  6. Ora animati da quella transform (o frame o qualsiasi altra cosa) per il nuovo valore:

    [UIView animateWithDuration:1.0 
             delay:0.0 
            options:UIViewAnimationOptionAllowUserInteraction 
           animations:^{ 
            self.subview.layer.transform = newTransform; 
           } 
           completion:NULL]; 
    

Mettendo tutto insieme, ecco una routine che commuta la mia scala di trasformazione da 2.0x a ide ntify e ritorno:

- (IBAction)didTouchUpInsideAnimateButton:(id)sender 
{ 
    CALayer *currentLayer = self.subview.layer.presentationLayer; 

    [self.subview.layer removeAllAnimations]; 

    self.subview.layer.transform = currentLayer.transform; 

    CATransform3D newTransform; 

    self.large = !self.large; 

    if (self.large) 
     newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0); 
    else 
     newTransform = CATransform3DIdentity; 

    [UIView animateWithDuration:1.0 
          delay:0.0 
         options:UIViewAnimationOptionAllowUserInteraction 
        animations:^{ 
         self.subview.layer.transform = newTransform; 
        } 
        completion:NULL]; 
} 

O se si voleva passare frame dimensioni da 100x100 a 200x200 e ritorno:

- (IBAction)didTouchUpInsideAnimateButton:(id)sender 
{ 
    CALayer *currentLayer = self.subview.layer.presentationLayer; 

    [self.subview.layer removeAllAnimations]; 

    CGRect newFrame = currentLayer.frame; 

    self.subview.frame = currentLayer.frame; 

    self.large = !self.large; 

    if (self.large) 
     newFrame.size = CGSizeMake(200.0, 200.0); 
    else 
     newFrame.size = CGSizeMake(100.0, 100.0); 

    [UIView animateWithDuration:1.0 
          delay:0.0 
         options:UIViewAnimationOptionAllowUserInteraction 
        animations:^{ 
         self.subview.frame = newFrame; 
        } 
        completion:NULL]; 
} 

Tra l'altro, mentre in genere non ha molta importanza per davvero animazioni rapide, per animazioni lente come la tua, potresti voler impostare la durata dell'animazione di inversione per essere uguale a quanto hai progredito nell'animazione corrente (ad esempio, se sei 0.5 secondi in un'animazione di 3.0 secondi, quando si capovolge, probabilmente non si vuole prendere 3 .0 secondi per invertire quella piccola parte dell'animazione che hai fatto finora, ma piuttosto solo 0,5 secondi). Così, che potrebbe essere simile:

- (IBAction)didTouchUpInsideAnimateButton:(id)sender 
{ 
    CFTimeInterval duration = kAnimationDuration;    // default the duration to some constant 
    CFTimeInterval currentMediaTime = CACurrentMediaTime(); // get the current media time 
    static CFTimeInterval lastAnimationStart = 0.0;   // media time of last animation (zero the first time) 

    // if we previously animated, then calculate how far along in the previous animation we were 
    // and we'll use that for the duration of the reversing animation; if larger than 
    // kAnimationDuration that means the prior animation was done, so we'll just use 
    // kAnimationDuration for the length of this animation 

    if (lastAnimationStart) 
     duration = MIN(kAnimationDuration, (currentMediaTime - lastAnimationStart)); 

    // save our media time for future reference (i.e. future invocations of this routine) 

    lastAnimationStart = currentMediaTime; 

    // if you want the animations to stay relative the same speed if reversing an ongoing 
    // reversal, you can backdate the lastAnimationStart to what the lastAnimationStart 
    // would have been if it was a full animation; if you don't do this, if you repeatedly 
    // reverse a reversal that is still in progress, they'll incrementally speed up. 

    if (duration < kAnimationDuration) 
     lastAnimationStart -= (kAnimationDuration - duration); 

    // grab the state of the layer as it is right now 

    CALayer *currentLayer = self.subview.layer.presentationLayer; 

    // cancel any animations in progress 

    [self.subview.layer removeAllAnimations]; 

    // set the transform to be as it is now, possibly in the middle of an animation 

    self.subview.layer.transform = currentLayer.transform; 

    // toggle our flag as to whether we're looking at large view or not 

    self.large = !self.large; 

    // set the transform based upon the state of the `large` boolean 

    CATransform3D newTransform; 

    if (self.large) 
     newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0); 
    else 
     newTransform = CATransform3DIdentity; 

    // now animate to our new setting 

    [UIView animateWithDuration:duration 
          delay:0.0 
         options:UIViewAnimationOptionAllowUserInteraction 
        animations:^{ 
         self.subview.layer.transform = newTransform; 
        } 
        completion:NULL]; 
} 
+0

Grazie Rob. Questa è una soluzione molto buona e completa. Ma funzionerà solo al 100% precisamente con 'UIViewAnimationOptionCurveLinear'. Se utilizzo 'UIViewAnimationOptionCurveEaseInOut', l'animazione inversa non sarà esattamente simmetrica all'animazione originale perché inizierà lentamente e accelera mentre l'animazione originale si interromperà bruscamente. C'è un modo per ricordare in qualche modo lo stato dell'animazione in termini di "velocità" e "accelerazione", vale a dire ottenere il punto corrente nella curva di animazione? – Mischa

+0

@Mischa Ci sarebbero modi per farlo, ma poi sei fuori dal mondo di un'animazione basata su blocchi piacevole e semplice (perché non c'è modo di fare una porzione, ad esempio 87,529%, di una curva di animazione). Sarebbe un lavoro infernale (ad esempio codice dell'anima core personalizzato), per qualcosa che potrebbe non essere osservabile (vedi il mio suggerimento alternativo sotto il quale potrebbe approssimare la tua richiesta), ma certamente si potrebbe fare. Potrei suggerire, invece, che contemplate che se l'animazione è inferiore a, diciamo al 90%, fatta, eseguite 'EaseOut' sul retro (vale a dire, supponendo che vi stiate muovendo rapidamente all'ora), altrimenti 'EaseInOut'. – Rob

+0

@Mischa Detto questo, mi chiedo persino che l'idea di una perfetta simmetria sia del tutto corretta, perché se stai facendo 'EaseInOut' (che significa che stai cercando animazioni fluide), per dire che stai andando per invertire bruscamente l'animazione che risulta essere fatta al 50% (e quindi muoversi alla massima velocità) e ruotarla all'istante e spostarla a velocità massima nell'altro senso, non tanto con un "congedo", wow, sarebbe antitetico all'intenzione originale di 'EaseInOut', no? – Rob

1

C'è un trucco comune è possibile utilizzare per fare questo, ma è necessario scrivere un metodo separato a ridursi (e un altro simile per espandere):

- (void) shrink { 
    [UIView animateWithDuration:0.3 
        animations:^{ 
         self.myView.transform = shrinkALittleBitMatrix; 
        } 
        completion:^(BOOL finished){ 
          if (continueShrinking && size>0) { 
           size=size-1; 
           [self shrink];  
          } 
        }]; 
} 

Così ora, il il trucco è quello di interrompere l'animazione di 3 secondi della riduzione in 10 animazioni (o più di 10, ovviamente) di 0,3 secondi ciascuna in cui si riduce 1/10 della intera animazione: shrinkALittleBitMatrix. Dopo che ogni animazione è terminata, si chiama lo stesso metodo solo quando l'ivar bool continueShrinking è true e quando l'int ivar size è positivo (la vista a dimensione intera sarebbe size = 10 e la vista con dimensione minima sarebbe size = 0). Quando si preme il pulsante, si cambia l'ivar continueShrinking in FALSE, quindi si chiama expand. Questo fermerà l'animazione in meno di 0,3 secondi.

Bene, devi riempire i dettagli ma spero che aiuti.

+0

Questa è una buona soluzione per una funzionalità che dovrebbe far parte del framework. Ma il problema qui è sostanzialmente lo stesso della soluzione di Rob: supporta solo animazioni lineari perché ogni animazione parziale ha la stessa durata. Naturalmente potrei imitare il comportamento di 'UIViewAnimationOptionCurveEaseInOut' cambiando la durata in base al valore della' dimensione' di ivar e simulando così l'accelerazione e la decelerazione. Ma sarebbe un'altra soluzione per rendere le cose più complicate e meno pulite. – Mischa

0

Prima: come rimuovere o annullare un animazione con vista?

[view.layer removeAllAnimations] 

se la vista hanno molti animazioni, come, un'animazione è spostarsi da cima a fondo, altro è mossa da sinistra verso destra;

è possibile annullare o rimuovere un'animazione speciale come questo:

[view.layer removeAnimationForKey:@"someKey"]; 
// the key is you assign when you create a animation 
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"someKey"]; 

quando fai così, l'animazione si fermerà, sarà invocarlo delegato:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag 

se la bandiera == 1, indica che l'animazione è completata. se flag == 0, indica che l'animazione non è completata, forse cancellata, rimossa.

Secondo: quindi, è possibile eseguire ciò che si desidera eseguire in questo metodo delegato.

se si vuole ottenere la cornice della vista quando il codice di rimozione excute, si può fare questo:

currentFrame = view.layer.presentationlayer.frame; 

Nota:

quando si ottiene il frame corrente e rimuovere l'animazione, il la vista animerà anche un periodo di tempo, quindi currentFrame non è l'ultimo frame nella schermata del dispositivo.

Al momento non riesco a risolvere questa domanda. se un giorno potrò, aggiornerò questa domanda.