2009-05-03 4 views
6

J'ai un UIView personnalisé pour afficher une image en mosaïque.Mise à l'échelle UIView pendant l'animation

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

Maintenant j'essaye d'animer le redimensionnement de cette vue. Ce que je m'attendrais à ce que la zone carrelée ne fasse que grossir, laissant les tailles de carreaux constantes. Ce qui se passe à la place, c'est que pendant que la zone se développe, le contenu original de la vue est redimensionné à la nouvelle taille. Au moins pendant l'animation. Donc au début et à la fin de l'animation tout est bon. Pendant l'animation, les tuiles sont déformées.

Pourquoi CA tente-t-il d'évoluer? Comment puis-je l'empêcher de le faire? Qu'est-ce que j'ai raté?

+0

Il semble que cela soit lié au contentMode de la vue. Mais le paramétrer sur UIViewContentModeLeft ne le résout pas non plus. Il ne fait alors plus d'échelle, mais apparaît immédiatement à la nouvelle taille d'image. Le paramétrer sur UIViewContentModeRedraw ne semble pas appeler drawRect du tout pendant l'animation. – tcurdt

Répondre

15

Si Core Animation devait rappeler votre code pour chaque image d'animation, il ne serait jamais aussi rapide que cela, et l'animation des propriétés personnalisées a été une longue fonctionnalité demandée et une FAQ pour CA sur Mac.

En utilisant UIViewContentModeRedraw est sur la bonne voie, et est également le meilleur que vous obtiendrez de CA. Le problème est que du point de vue UIKit, le cadre n'a que deux valeurs: la valeur au début de la transition et la valeur à la fin de la transition, et c'est ce que vous voyez. Si vous regardez les documents d'architecture de Core Animation, vous verrez comment CA a une représentation privée de toutes les propriétés de la couche et de leurs valeurs qui changent avec le temps. C'est là que l'interpolation d'image est en cours, et vous ne pouvez pas être averti des changements à ce moment-là.

La seule façon est d'utiliser un NSTimer (ou performSelector:withObject:afterDelay:) pour changer le cadre de vue au fil du temps, à l'ancienne.

+0

Merci, j'ai utilisé un NSTimer pour le faire (Appelez drawRect avec animation). Cela fonctionne pour moi. – Strong

+0

@ jazou2012 Vous ne devriez pas appeler 'drawRect', vous devriez appeler' setNeedsDisplay'. –

+0

@ Jean-PhilippePellet Oui, vous avez raison. 'setNeedsDisplay' appellera' drawRect'! Et c'est aussi ce que je voulais dire. – Strong

2

Comme l'indique Duncan, Core Animation ne redessinera pas le contenu de votre couche UIView à chaque image lors de son redimensionnement. Vous devez le faire vous-même en utilisant une minuterie.

Les gars d'Omni ont publié un bel exemple de comment faire des animations basées sur vos propres propriétés personnalisées qui pourraient être applicables à votre cas. Cet exemple, avec une explication de son fonctionnement, peut être trouvé here.

4

Je faisais face à un problème similaire dans un contexte différent, je suis tombé sur ce fil et trouvé la suggestion de Duncan de définir l'image dans NSTimer. Je mis en œuvre ce code pour obtenir le même:

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

Je trouve que cela prend beaucoup de temps pour définir le cadre et la minuterie pour terminer son fonctionnement. Cela dépend probablement de la profondeur de la hiérarchie de vue sur laquelle vous appelez -setFrame en utilisant cette approche. Et c'est probablement la raison pour laquelle la vue est d'abord redimensionnée puis animée à l'origine dans le sdk. Ou peut-être, y at-il un problème dans le mécanisme de minuterie dans mon code en raison de laquelle la performance a entravé?

Cela fonctionne, mais c'est très lent, peut-être parce que ma hiérarchie de vue est trop profonde.

+2

Votre code fonctionne très bien pour ma vue (une vue en arrière-plan et une superposition de texte). 2 modifications étaient cependant nécessaires: 1. [self.animationTimer invalidate] en haut de changeFrameWithAnimation, et 2. en changeant la durée de l'animation à 0.3, ainsi que le nombre de pas. Comme c'est le cas en ce moment, vous appelez la minuterie à une période de 10ms, mais vous ne devez le faire que toutes les 16ms pour un taux de rafraîchissement de 60Hz. – Jon

+0

Merci @Jon, havent avait pensé à un taux de rafraîchissement de 60Hz, mais dans mon cas tout le processus ralentirait car ma hiérarchie de vue était lourde et par conséquent il parlait plus de temps que prévu. Donc, a dû changer l'exigence elle-même! –

1

Si ce n'est pas une grande animation ou la performance est pas un problème pour la durée de l'animation, vous pouvez utiliser un CADisplayLink. Il lisse l'animation de la mise à l'échelle d'un dessin UIView personnalisé UIBezierPaths par exemple. Voici un exemple de code que vous pouvez adapter à votre image.

Les ivars de ma vue personnalisée contiennent displayLink en tant que CADisplayLink et deux CGRects: toFrame et fromFrame, ainsi que duration et 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; 
} 

Notez que je n'ai pas testé ses performances, mais cela fonctionne correctement.

1

J'ai trébuché sur cette question en essayant d'animer un changement de taille sur un UIView avec une bordure. J'espère que d'autres personnes qui arrivent de la même manière pourraient bénéficier de cette réponse. Au départ, je créais une sous-classe UIView personnalisée et redéfinissant la méthode drawRect comme suit:

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

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

    [super drawRect:rect]; 
} 

Cela a conduit à des problèmes d'échelle lors de la combinaison avec des animations comme d'autres l'ont mentionné. Le haut et le bas de la bordure deviendraient trop épais ou deux minces et sembleraient écaillés. Cet effet est facilement perceptible lorsque vous basculez sur des animations lentes.

La solution est d'abandonner la sous-classe personnalisée et d'utiliser plutôt cette approche:

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

Cette résolu le problème avec une mise à l'échelle sur une frontière UIView lors des animations.

Questions connexes