2009-05-03 4 views
6

У меня есть пользовательский UIView, чтобы показать черепичное изображение.Масштабирование UIView во время анимации

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

Теперь я пытаюсь анимировать изменение размера этого вида.

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

// change frame of view 

[UIView commitAnimations]; 

Что бы я ожидал, это плиточный участок только для роста, оставляя размеры плитки постоянными. Вместо этого происходит то, что в то время как область растет, исходное содержимое представления масштабируется до нового размера. По крайней мере, во время анимации. Так что в начале и в конце анимации все хорошо. Во время анимации плитки искажаются.

Почему CA пытается масштабировать? Как я могу предотвратить это? Что я упустил?

+0

Кажется, это связано с содержимым contentMode. Но установка UIViewContentModeLeft на самом деле не решает его. Затем он больше не масштабируется, а сразу появляется новый размер кадра. Установка его в UIViewContentModeRedraw, похоже, вообще не вызывает drawRect во время анимации. – tcurdt

ответ

15

Если Core Animation пришлось переходить к вашему коду для каждого кадра анимации, это никогда не будет так быстро, как это было, и анимация пользовательских свойств была давно запрошенной функцией и часто задаваемыми вопросами для CA на Mac.

Использование UIViewContentModeRedraw на правильном пути, а также лучшее, что вы получите от CA. Проблема заключается в том, что точка зрения UIKit имеет только два значения: значение в начале перехода и значение в конце перехода, и это то, что вы видите. Если вы посмотрите на документы архитектуры Core Animation, вы увидите, как CA имеет частное представление всех свойств слоя, а их значения меняются со временем. Здесь происходит интерполяция кадров, и вы не можете получать уведомления об изменениях в этом, поскольку они происходят.

Таким образом, единственный способ - использовать NSTimer (или performSelector:withObject:afterDelay:), чтобы изменить раму зрения с течением времени, старомодным способом.

+0

Спасибо, я использовал NSTimer для этого (Call drawRect вместе с анимацией). Это работает для меня. – Strong

+0

@ jazou2012 Вы не должны называть 'drawRect', вы должны называть' setNeedsDisplay'. –

+0

@ Jean-PhilippePellet Да, вы правы. 'setNeedsDisplay' будет называть' drawRect'! И это то, что я хотел сказать. – Strong

2

Как указывает Дункан, Core Animation не будет перерисовывать содержимое слоя UIView каждого кадра при его изменении. Вам нужно сделать это самостоятельно, используя таймер.

Ребята из Omni опубликовали хороший пример того, как делать анимации на основе ваших собственных пользовательских свойств, которые могут быть применимы к вашему делу. Этот пример, а также объяснение того, как он работает, можно найти here.

4

Я столкнулся с подобной проблемой в другом контексте, я наткнулся на эту тему и нашел предложение Duncan установить рамку в NSTimer. Я реализовал этот код, чтобы достичь того же:

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

я узнал, что это занимает много времени, чтобы установить рамки и таймер, чтобы завершить свою работу. Вероятно, это зависит от того, насколько глубока иерархия представлений, на которую вы вызываете -setFrame, используя этот подход. И, вероятно, именно по этой причине представление сначала передислоцируется, а затем анимируется в начало в sdk. Или, может быть, есть какая-то проблема в механизме таймера в моем коде, из-за чего производительность мешает?

Работает, но это очень медленно, возможно потому, что моя иерархия взглядов слишком глубока.

+2

Ваш код отлично подходит для моего просмотра (фоновый просмотр и наложение текста). 2 изменения были необходимы, хотя: 1. [self.animationTimer invalidate] в верхней части changeFrameWithAnimation и 2. изменение продолжительности анимации до 0,3, а также количество шагов. Как сейчас, вы вызываете таймер в течение 10 мс, но вам нужно делать это каждые 16 мс при частоте обновления 60 Гц. – Jon

+0

Спасибо @ Jon, havent подумал о частоте обновления 60 Гц, но в моем случае весь процесс замедлялся, потому что моя иерархия взглядов была тяжелой и, следовательно, она говорила больше времени, чем ожидалось. Итак, пришлось самому изменить требование! –

1

Если это не большая анимация или производительность не является проблемой для продолжительности анимации, вы можете использовать CADisplayLink. Он сглаживает анимацию масштабирования пользовательского UIView-рисунка UIBezierPaths, например. Ниже приведен пример кода, который вы можете адаптировать для своего изображения.

Иварцы моего пользовательского представления содержат displayLink как CADisplayLink и два CGRects: toFrame и fromFrame, а также продолжительность и 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; 
} 

Обратите внимание, что я не тестировал его работу, но он работает плавно.

1

Я наткнулся на этот вопрос, пытаясь оживить изменение размера на UIView с помощью границы. Я надеюсь, что другие, которые приходят сюда, могут воспользоваться этим ответом. Первоначально я создавал пользовательские UIView подкласс и переопределить метод DrawRect следующим образом:

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

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

    [super drawRect:rect]; 
} 

Это привело к расширению масштабов проблемы сочетаемости с анимацией, как уже упоминалось. Верх и низ границы будут слишком толстыми или двумя тонкими и окажутся масштабированными. Этот эффект легко заметен при переключении на медленную анимацию.

Решения был отказаться от пользовательского подкласса и вместо того, чтобы использовать этот подход:

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

Этот фиксированный вопрос с масштабированием границы на UIView во время анимации.

Смежные вопросы