Domanda

Ho un UIView personalizzato per mostrare un'immagine piastrellata.

    - (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 aspetterei è che l'area piastrellata cresca solo, lasciando costanti le dimensioni delle piastrelle. Quello che succede invece è che mentre l'area cresce il contenuto originale della vista viene ridimensionato alle nuove dimensioni. Almeno durante l'animazione. Quindi all'inizio e alla fine dell'animazione tutto va bene. Durante l'animazione le tessere sono distorte.

Perché CA sta cercando di ridimensionare? Come posso impedirlo? Cosa mi sono perso?

È stato utile?

Soluzione

Se Core Animation dovesse richiamare il tuo codice per ogni fotogramma di animazione, non sarebbe mai così veloce come è, e l'animazione delle proprietà personalizzate è stata una funzionalità richiesta da tempo e FAQ per CA sul Mac.

L'utilizzo di UIViewContentModeRedraw è sulla strada giusta ed è anche il migliore che otterrai dalla 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 guardi i documenti dell'architettura di animazione principale, vedrai come CA ha una rappresentazione privata di tutte le proprietà dei livelli e i loro valori cambiano nel tempo. È qui che sta avvenendo l'interpolazione del frame e non puoi essere informato delle modifiche apportate mentre si verificano.

Quindi l'unico modo è usare un NSTimer (o performSelector: withObject: afterDelay: ) per cambiare il frame della vista nel tempo, alla vecchia maniera.

Altri suggerimenti

Stavo affrontando un problema simile in un contesto diverso, mi sono imbattuto in questo thread e ho trovato il suggerimento di Duncan di impostare il frame 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 frame e il timer per completare il suo funzionamento. Probabilmente dipende dalla profondità della gerarchia della vista su cui chiami -setFrame usando questo approccio. E probabilmente questo è il motivo per cui la vista viene prima ridimensionata e quindi animata all'origine in 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 visualizzazioni è troppo profonda.

Come indica Duncan, Core Animation non ridisegna il contenuto del livello UIView in ogni fotogramma quando viene ridimensionato. Devi farlo tu stesso usando un timer.

I ragazzi di Omni hanno pubblicato un bell'esempio di come realizzare animazioni basate sulle tue proprietà personalizzate che potrebbero essere applicabili al tuo caso. Quell'esempio, insieme a una spiegazione di come funziona, può essere trovato qui .

Se non si tratta di una grande animazione o una performance non è un problema per la durata dell'animazione, è possibile utilizzare un CADisplayLink. Rende più fluida l'animazione del ridimensionamento di un UIView personalizzato che disegna UIBezierPaths per esempio. Di seguito è riportato un codice di esempio che puoi adattare per la tua immagine.

Gli ivar della mia vista personalizzata contengono displayLink come CADisplayLink e due CGRect: 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;
}

Nota che non ho testato le sue prestazioni, ma funziona senza problemi.

Mi sono imbattuto in questa domanda quando ho cercato di animare una modifica delle dimensioni in una vista UIV con un bordo. Spero che altri che arrivano allo stesso modo qui possano beneficiare di questa risposta. Inizialmente stavo creando una sottoclasse UIView personalizzata e sovrascrivendo il 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 ridimensionamento quando combinato con animazioni come altri hanno già detto. La parte superiore e inferiore del bordo diventerebbero troppo spesse o due sottili e sembrerebbero ridimensionate. Questo effetto è facilmente evidente quando si attiva / disattivano le animazioni lente.

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

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

Questo risolto il problema con il ridimensionamento di un bordo su una vista UIV durante le animazioni.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top