2009-05-03 14 views
6

Tengo una UIView personalizada para mostrar una imagen de mosaico.Escalamiento de UIView durante la animación

- (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 baldosas solo crezca, dejando los tamaños de baldosas constantes. Lo que sucede en cambio es que mientras el área crece, el contenido original de la vista se escala al nuevo tamaño. Al menos durante la animación Así que al principio y al final de la animación todo es bueno. Durante la animación, las fichas están distorsionadas.

¿Por qué CA intenta escalar? ¿Cómo puedo evitar que lo haga? ¿Qué me perdí?

+0

Parece estar relacionado con el modo de contenido de la vista. Pero configurarlo en UIViewContentModeLeft tampoco lo resuelve realmente. Entonces ya no se escalará, sino que pasará al nuevo tamaño de fotograma de inmediato. Configurarlo en UIViewContentModeRedraw no parece llamar a drawRect en absoluto durante la animación. – tcurdt

Respuesta

15

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

El uso de UIViewContentModeRedraw está en el camino correcto, y también es lo mejor que obtendrá de CA. El problema es 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á 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 cuadros, y no se le pueden notificar los cambios a medida que ocurren.

Así que la única forma es usar un NSTimer (o performSelector:withObject:afterDelay:) para cambiar el marco de vista en el tiempo, a la manera antigua.

+0

Gracias, utilicé un NSTimer para hacerlo (llame a drawRect junto con la animación). Está funcionando para mí. – Strong

+0

@ jazou2012 No debe llamar a 'drawRect', debe llamar a' setNeedsDisplay'. –

+0

@ Jean-PhilippePellet Sí, tienes razón. 'setNeedsDisplay' llamará a' drawRect'! Y eso es también lo que quise decir. – Strong

2

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

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

4

Estaba enfrentando un problema similar en un contexto diferente, me encontré con este hilo y encontré la sugerencia de Duncan de establecer el marco en NSTimer. He implementado el código para lograr el 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 se necesita mucho tiempo para establecer el marco y el contador de tiempo para completar su operación. Probablemente dependa de cuán profunda sea la jerarquía de vistas a la que llame -setFrame utilizando este enfoque. Y probablemente esta es la razón por la cual la vista se vuelve a clasificar según el tamaño 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 la jerarquía de mi vista es demasiado profunda.

+2

Tu código funciona muy bien para mi vista (una vista de fondo y una superposición de texto). Sin embargo, 2 cambios fueron necesarios: 1. [auto.animationTimer invalida] en la parte superior de changeFrameWithAnimation, y 2. cambiando la duración de la animación a 0.3, así como el número de pasos. Como es ahora, estás llamando al temporizador en un período de 10 ms, pero solo tienes que hacerlo cada 16 ms para una frecuencia de actualización de 60 Hz. – Jon

+0

Gracias @Jon, no se ha pensado en la frecuencia de actualización de 60Hz, pero en mi caso todo el proceso se ralentizaría porque la jerarquía de mi vista era pesada y por lo tanto estaba hablando más tiempo de lo esperado. Entonces, ¡tuvo que cambiar el requisito en sí mismo! –

1

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

Los ivars de mi vista personalizada contienen displayLink como CADisplayLink, y dos CGRects: toFrame y fromFrame, así como 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.

1

Me encontré con esta pregunta al intentar animar un cambio de tamaño en una UIView con un borde. Espero que otros que de manera similar lleguen aquí se beneficien de esta respuesta. Al principio yo estaba creando una subclase UIView costumbre y reemplazando 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 combinan con animaciones como otros han mencionado. La parte superior e inferior del borde se volverían demasiado gruesas o dos delgadas y aparecerían a escala. Este efecto se nota fácilmente al alternar en Slow Animations.

La solución era abandonar la subclase personalizada y en su lugar utilizar este enfoque:

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

Esto ha solucionado el problema con escalar una frontera en un UIView durante las animaciones.

Cuestiones relacionadas