2009-05-03 13 views
6

Ho un UIView personalizzato per mostrare un'immagine affiancata.Ridimensionamento UIView durante l'animazione

- (void)drawRect:(CGRect)rect 
    { 
     ... 
     CGContextRef context = UIGraphicsGetCurrentContext();  
     CGContextClipToRect(context, 
      CGRectMake(0.0, 0.0, rect.size.width, rect.size.height));  
     CGContextDrawTiledImage(context, imageRect, imageRef); 
     ... 
    } 

Ora sto cercando di animare il ridimensionamento di quella vista.

[UIView beginAnimations:nil context:nil]; 
[UIView setAnimationDuration:0.3]; 

// change frame of view 

[UIView commitAnimations]; 

Quello che mi aspetto è per l'area piastrellata solo a crescere, lasciando la piastrella formati costante. Quello che succede invece è che mentre l'area cresce, il contenuto originale della vista viene ridimensionato alla nuova dimensione. Almeno durante l'animazione. Quindi all'inizio e alla fine dell'animazione tutto va bene. Durante l'animazione le tessere sono distorte.

Perché la CA sta tentando di scalare? Come posso impedirgli di farlo? Cosa mi sono perso?

+0

Sembra correlato alla modalità di contenuto della vista. Ma impostarlo su UIViewContentModeLeft non lo risolve davvero. Non è più scalabile, ma si apre immediatamente alla nuova dimensione del fotogramma. Impostarlo su UIViewContentModeRedraw non sembra richiamare affatto drawRect durante l'animazione. – tcurdt

risposta

15

Se Core Animation ha dovuto richiamare il codice per ogni frame di animazione, non sarebbe mai stato così veloce come è, e l'animazione delle proprietà personalizzate è stata una funzione richiesta a lungo e le FAQ per CA su Mac.

Utilizzando UIViewContentModeRedraw è sulla strada giusta, ed è anche il migliore si otterrà da CA. Il problema è dal punto di vista di UIKit: il frame ha solo due valori: il valore all'inizio della transizione e il valore alla fine della transizione, ed è quello che stai vedendo. Se si guardano i documenti dell'architettura Core Animation, vedremo come CA ha una rappresentazione privata di tutte le proprietà dei layer e dei loro valori che cambiano nel tempo. È qui che sta avvenendo l'interpolazione dei frame e non è possibile ricevere notifiche delle modifiche a tale scopo.

Quindi l'unico modo è utilizzare un NSTimer (o performSelector:withObject:afterDelay:) per modificare la cornice della vista nel tempo, alla vecchia maniera.

+0

Grazie, ho usato un NSTimer per farlo (chiama drawRect insieme all'animazione). Funziona per me. – Strong

+0

@ jazou2012 Non dovresti chiamare 'drawRect', dovresti chiamare' setNeedsDisplay'. –

+0

@ Jean-PhilippePellet Sì, hai ragione. 'setNeedsDisplay' chiamerà' drawRect'! Ed è anche quello che intendevo dire. – Strong

2

Come Duncan indica, Core Animation non sarà ridisegnare il contenuto del livello del UIView ogni fotogramma come viene ridimensionata. Devi farlo tu stesso usando un timer.

I ragazzi di Omni registrato un bell'esempio di come fare animazioni in base alle proprie proprietà personalizzate che potrebbero essere applicabili al vostro caso. Questo esempio, insieme a una spiegazione su come funziona, può essere trovato here.

4

ero di fronte problema simile in un contesto diverso, sono incappato in questa discussione e trovarono suggerimento di Duncan di impostazione telaio in NSTimer. Ho implementato questo codice per ottenere lo stesso:

CGFloat kDurationForFullScreenAnimation = 1.0; 
CGFloat kNumberOfSteps = 100.0; // (kDurationForFullScreenAnimation/kNumberOfSteps) will be the interval with which NSTimer will be triggered. 
int gCurrentStep = 0; 
-(void)animateFrameOfView:(UIView*)inView toNewFrame:(CGRect)inNewFrameRect withAnimationDuration:(NSTimeInterval)inAnimationDuration numberOfSteps:(int)inSteps 
{ 
    CGRect originalFrame = [inView frame]; 

    CGFloat differenceInXOrigin = originalFrame.origin.x - inNewFrameRect.origin.x; 
    CGFloat differenceInYOrigin = originalFrame.origin.y - inNewFrameRect.origin.y; 
    CGFloat stepValueForXAxis = differenceInXOrigin/inSteps; 
    CGFloat stepValueForYAxis = differenceInYOrigin/inSteps; 

    CGFloat differenceInWidth = originalFrame.size.width - inNewFrameRect.size.width; 
    CGFloat differenceInHeight = originalFrame.size.height - inNewFrameRect.size.height; 
    CGFloat stepValueForWidth = differenceInWidth/inSteps; 
    CGFloat stepValueForHeight = differenceInHeight/inSteps; 

    gCurrentStep = 0; 
    NSArray *info = [NSArray arrayWithObjects: inView, [NSNumber numberWithInt:inSteps], [NSNumber numberWithFloat:stepValueForXAxis], [NSNumber numberWithFloat:stepValueForYAxis], [NSNumber numberWithFloat:stepValueForWidth], [NSNumber numberWithFloat:stepValueForHeight], nil]; 
    NSTimer *aniTimer = [NSTimer timerWithTimeInterval:(kDurationForFullScreenAnimation/kNumberOfSteps) 
               target:self 
               selector:@selector(changeFrameWithAnimation:) 
               userInfo:info 
               repeats:YES]; 
    [self setAnimationTimer:aniTimer]; 
    [[NSRunLoop currentRunLoop] addTimer:aniTimer 
           forMode:NSDefaultRunLoopMode]; 
} 

-(void)changeFrameWithAnimation:(NSTimer*)inTimer 
{ 
    NSArray *userInfo = (NSArray*)[inTimer userInfo]; 
    UIView *inView = [userInfo objectAtIndex:0]; 
    int totalNumberOfSteps = [(NSNumber*)[userInfo objectAtIndex:1] intValue]; 
    if (gCurrentStep<totalNumberOfSteps) 
    { 
     CGFloat stepValueOfXAxis = [(NSNumber*)[userInfo objectAtIndex:2] floatValue]; 
     CGFloat stepValueOfYAxis = [(NSNumber*)[userInfo objectAtIndex:3] floatValue]; 
     CGFloat stepValueForWidth = [(NSNumber*)[userInfo objectAtIndex:4] floatValue]; 
     CGFloat stepValueForHeight = [(NSNumber*)[userInfo objectAtIndex:5] floatValue]; 

     CGRect currentFrame = [inView frame]; 
     CGRect newFrame; 
     newFrame.origin.x = currentFrame.origin.x - stepValueOfXAxis; 
     newFrame.origin.y = currentFrame.origin.y - stepValueOfYAxis; 
     newFrame.size.width = currentFrame.size.width - stepValueForWidth; 
     newFrame.size.height = currentFrame.size.height - stepValueForHeight; 

     [inView setFrame:newFrame]; 

     gCurrentStep++; 
    } 
    else 
    { 
     [[self animationTimer] invalidate]; 
    } 
} 

ho scoperto che ci vuole molto tempo per impostare il telaio e il timer per completare la sua operazione. Probabilmente dipende dalla profondità della gerarchia della vista su cui chiamate -setFrame usando questo approccio. E probabilmente questo è il motivo per cui la vista viene prima ridimensionata e quindi animata all'origine nel sdk. O forse, c'è qualche problema nel meccanismo del timer nel mio codice a causa del quale le prestazioni hanno ostacolato?

Funziona, ma è molto lento, forse perché la mia gerarchia di vista è troppo profondo.

+2

Il tuo codice funziona perfettamente per la mia vista (una vista di sfondo e una sovrapposizione di testo). Sono state tuttavia necessarie 2 modifiche: 1. [self.animationTimer invalidate] nella parte superiore di changeFrameWithAnimation e 2. modifica della durata dell'animazione a 0,3, nonché del numero di passaggi. Come è ora, stai chiamando il timer in un periodo di 10ms, ma devi farlo solo ogni 16ms per una frequenza di aggiornamento di 60Hz. – Jon

+0

Grazie a @Jon, hovent ha pensato a una frequenza di aggiornamento di 60 Hz, ma nel mio caso l'intero processo rallenterebbe perché la mia gerarchia di visualizzazione era pesante e quindi si parlava più del previsto. Quindi, ha dovuto cambiare il requisito stesso! –

1

Se non si tratta di una grande animazione o le prestazioni non è un problema per tutta la durata dell'animazione, è possibile utilizzare un CADisplayLink. Perfeziona l'animazione del ridimensionamento di un UIView personalizzato, disegnando per esempio UIBezierPaths. Di seguito è riportato un codice di esempio che puoi adattare all'immagine.

L'ivars della mia vista personalizzata contiene displayLink come CADisplayLink e due CGRects: toFrame e fromFrame, nonché durata e startTime.

- (void)yourAnimationMethodCall 
{ 
    toFrame = <get the destination frame> 
    fromFrame = self.frame; // current frame. 

    [displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // just in case  
    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateFrame:)]; 
    startTime = CACurrentMediaTime(); 
    duration = 0.25; 
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
} 

- (void)animateFrame:(CADisplayLink *)link 
{ 
    CGFloat dt = ([link timestamp] - startTime)/duration; 

    if (dt >= 1.0) { 
     self.frame = toFrame; 
     [displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
     displayLink = nil; 
     return; 
    } 

    CGRect f = toFrame; 
    f.size.height = (toFrame.size.height - fromFrame.size.height) * dt + fromFrame.size.height; 
    self.frame = f; 
} 

Si noti che non ho testato le sue prestazioni, ma funziona senza intoppi.

1

Mi sono imbattuto in questa domanda quando provavo ad animare una modifica della dimensione su un UIView con un bordo. Spero che altri che arrivano allo stesso modo qui potrebbero trarre beneficio da questa risposta. Originariamente stavo creando una sottoclasse UIView personalizzato e l'override del metodo drawRect come segue:

- (void)drawRect:(CGRect)rect 
{ 
    CGContextRef ctx = UIGraphicsGetCurrentContext(); 

    CGContextSetLineWidth(ctx, 2.0f); 
    CGContextSetStrokeColorWithColor(ctx, [UIColor orangeColor].CGColor); 
    CGContextStrokeRect(ctx, rect); 

    [super drawRect:rect]; 
} 

Ciò ha portato a problemi di scala quando si combinano con le animazioni come altri hanno detto. La parte superiore e inferiore del bordo diventerebbe troppo spessa o due sottili e apparire in scala. Questo effetto è facilmente visibile quando si attiva Animazioni lente.

La soluzione era quella di abbandonare la sottoclasse personalizzata e di utilizzare invece questo approccio:

[_borderView.layer setBorderWidth:2.0f]; 
[_borderView.layer setBorderColor:[UIColor orangeColor].CGColor]; 

Questo Risolto il problema con il ridimensionamento un bordo su uno UIView durante le animazioni.

Problemi correlati