Вопрос

У меня есть пользовательский 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 пытается масштабировать? Как я могу предотвратить это? Что я пропустил?

Это было полезно?

Решение

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

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

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

Другие советы

Я столкнулся с подобной проблемой в другом контексте, я наткнулся на эту тему и нашел предложение Дункана об установке фрейма в 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. Или, может быть, есть какая-то проблема в механизме таймера в моем коде, из-за которой снижается производительность?

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

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

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

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

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

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

Я наткнулся на этот вопрос при попытке анимировать изменение размера в 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 во время анимации.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top