Pregunta

Tengo una UIView personalizada para mostrar una imagen en mosaico.

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

Ahora estoy tratando de animar el cambio de tamaño de esa vista.

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

// change frame of view

[UIView commitAnimations];

Lo que esperaría es que el área de mosaico solo crezca, dejando los tamaños de mosaico constantes. En cambio, lo que sucede es que mientras el área crece, el contenido original de la vista se escala al nuevo tamaño. Al menos durante la animación. Entonces, al principio y al final de la animación, todo está bien. Durante la animación, los mosaicos se distorsionan.

¿Por qué CA intenta escalar? ¿Cómo puedo evitar que lo haga? ¿Qué extrañé?

¿Fue útil?

Solución

Si Core Animation tuviera que volver a llamar a su código para cada cuadro de animación, nunca sería tan rápido como es, y la animación de propiedades personalizadas ha sido una característica solicitada durante mucho tiempo y preguntas frecuentes para CA en Mac.

El uso de UIViewContentModeRedraw está en el camino correcto, y también es lo mejor que obtendrá de CA. El problema es que, desde el punto de vista de UIKit, el marco solo tiene dos valores: el valor al comienzo de la transición y el valor al final de la transición, y eso es lo que estás viendo. Si observa los documentos de arquitectura de Core Animation, verá cómo CA tiene una representación privada de todas las propiedades de capa y sus valores cambian con el tiempo. Ahí es donde está ocurriendo la interpolación de trama, y ??no puede ser notificado de los cambios a medida que suceden.

Entonces, la única forma es usar un NSTimer (o performSelector: withObject: afterDelay: ) para cambiar el marco de vista con el tiempo, a la antigua usanza.

Otros consejos

Estaba enfrentando un problema similar en un contexto diferente, me topé con este hilo y encontré la sugerencia de Duncan de establecer el marco en NSTimer. Implementé este código para lograr lo mismo:

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

Descubrí que lleva mucho tiempo configurar el marco y el temporizador para completar su operación. Probablemente depende de qué tan profunda sea la jerarquía de vistas en la que llame a -setFrame usando este enfoque. Y probablemente esta es la razón por la cual la vista se redimensiona primero y luego se anima al origen en el SDK. O tal vez, ¿hay algún problema en el mecanismo del temporizador en mi código debido a que el rendimiento ha obstaculizado?

Funciona, pero es muy lento, tal vez porque mi jerarquía de vistas es demasiado profunda.

Como indica Duncan, Core Animation no volverá a dibujar el contenido de la capa de UIView cada fotograma a medida que se redimensiona. Debe hacerlo usted mismo usando un temporizador.

Los chicos de Omni publicaron un buen ejemplo de cómo hacer animaciones basadas en sus propias propiedades personalizadas que podrían ser aplicables a su caso. Ese ejemplo, junto con una explicación de cómo funciona, se puede encontrar aquí .

Si no es una gran animación o el rendimiento no es un problema para la duración de la animación, puede usar un CADisplayLink. Suaviza la animación de la escala de un dibujo UIView personalizado UIBezierPaths, por ejemplo. A continuación se muestra un código de muestra que puede adaptar para su imagen.

Los ivars de mi vista personalizada contienen displayLink como CADisplayLink y dos CGRects: toFrame y fromFrame, así como la duración y 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;
}

Tenga en cuenta que no he probado su rendimiento, pero funciona sin problemas.

Me topé con esta pregunta al intentar animar un cambio de tamaño en una UIView con un borde. Espero que otros que lleguen aquí de manera similar puedan beneficiarse de esta respuesta. Originalmente estaba creando una subclase de UIView personalizada y anulando el método drawRect de la siguiente manera:

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

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

    [super drawRect:rect];
}

Esto condujo a problemas de escala cuando se combina con animaciones como otros han mencionado. La parte superior e inferior del borde se volverían demasiado gruesas o dos delgadas y parecerían escamadas. Este efecto se nota fácilmente al alternar en Animaciones lentas.

La solución fue abandonar la subclase personalizada y utilizar este enfoque:

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

Esto solucionó el problema al escalar un borde en una UIView durante las animaciones.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top