Question

J'ai un UIView personnalisé pour afficher une image en mosaïque.

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

J'essaie maintenant d'animer le redimensionnement de cette vue.

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

// change frame of view

[UIView commitAnimations];

Ce que je pense, c’est que la surface de carrelage ne fasse que croître, laissant la taille des carreaux constante. Au lieu de cela, le contenu original de la vue est redimensionné à la nouvelle taille pendant que la zone s'agrandit. Au moins pendant l'animation. Donc au début et à la fin de l'animation, tout va bien. Pendant l'animation, les tuiles sont déformées.

Pourquoi CA tente-t-il de s’adapter à l’échelle? Comment puis-je l'empêcher de le faire? Qu'est-ce qui m'a manqué?

Était-ce utile?

La solution

Si Core Animation devait rappeler votre code pour chaque image d'animation, il ne serait jamais aussi rapide et l'animation de propriétés personnalisées est une fonctionnalité demandée depuis longtemps et une FAQ fréquemment posée à CA sur Mac.

L'utilisation de UIViewContentModeRedraw est sur la bonne voie et constitue également le meilleur atout de CA. Le problème est que du point de vue de l'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 examinez les documents d'architecture 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 se produit l’interpolation de trames et vous ne pouvez pas être averti des modifications qui s’y produisent.

Le seul moyen consiste donc à utiliser un NSTimer (ou performSelector: withObject: afterDelay: ) pour modifier le cadre d'affichage dans le temps, à l'ancienne.

Autres conseils

J’étais confronté à un problème similaire dans un contexte différent, j’ai trébuché sur ce fil et trouvé la suggestion de Duncan de définir un cadre dans NSTimer. J'ai implémenté ce code pour obtenir le même résultat:

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

J'ai découvert qu'il faut beaucoup de temps pour définir le cadre et le minuteur pour terminer son opération. Cela dépend probablement de la profondeur de la hiérarchie de vues 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 la minuterie dans mon code en raison duquel la performance a entravé?

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

Comme l'indique Duncan, Core Animation ne redessine pas le contenu du calque 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 la façon de créer des animations en fonction de vos propriétés personnalisées qui pourraient s’appliquer à votre cas. Cet exemple, ainsi qu'une explication de son fonctionnement, peut être trouvé ici .

S'il ne s'agit pas d'une animation importante ou si la performance ne pose pas de problème pour la durée de l'animation, vous pouvez utiliser CADisplayLink. Cela adoucit par exemple l'animation de la mise à l'échelle d'un dessin personnalisé UIView UIBezierPaths. Vous trouverez ci-dessous un exemple de code que vous pouvez adapter à votre image.

Les ivars de ma vue personnalisée contiennent displayLink en tant que CADisplayLink, ainsi que deux CGRects: toFrame et fromFrame, ainsi que la durée et l'heure de début.

- (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.

Je suis tombé sur cette question en essayant d’animer un changement de taille d’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. A l'origine, je créais une sous-classe UIView personnalisée et substituais 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 entraîné des problèmes de mise à l’é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 trop minces et sembleraient redimensionnés. Cet effet est facilement perceptible lorsque vous basculez sur une animation lente.

La solution consistait à abandonner la sous-classe personnalisée et à utiliser cette approche:

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

Ceci corrigeait le problème de redimensionnement d'une bordure sur UIView lors d'animations.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top