动画期间UIView的缩放

6
我有一个自定义的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要进行缩放?我该如何防止它这样做?我错过了什么吗?

似乎与视图的contentMode有关。但将其设置为UIViewContentModeLeft也并不能真正解决问题。这样做不再缩放,而是立即弹出到新的框架大小。将其设置为UIViewContentModeRedraw在动画期间似乎根本没有调用drawRect。 - tcurdt
6个回答

14
如果Core Animation必须为每一帧动画回调到您的代码,它永远不会像现在这样快,自定义属性的动画已经成为Mac上CA的长期需求和常见问题。
使用UIViewContentModeRedraw并没有错,而且也是您从CA那里能得到的最好的效果。问题在于,从UIKit的角度来看,框架只有两个值:转换开始时的值和转换结束时的值,这就是您所看到的。如果查看Core Animation架构文档,您将了解到CA具有所有层属性及其值随时间变化的私有表示形式。这就是帧插值发生的地方,但是您无法被通知更改正在发生。
因此,唯一的方法是使用NSTimer(或performSelector:withObject:afterDelay:)以老式的方式随时间改变视图框架。

谢谢,我使用了NSTimer来实现它(在动画中调用drawRect)。对我来说很有效。 - Strong84
@jazou2012 你不应该调用 drawRect,而是应该调用 setNeedsDisplay - Jean-Philippe Pellet
@Jean-PhilippePellet 是的,你说得对。setNeedsDisplay会调用drawRect!这也是我想说的。 - Strong84
@jazou2012 从技术上讲,setNeedsDisplay并不会调用drawRect,但会导致在下一次UI重绘时调用drawRect - Jean-Philippe Pellet

6

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
你的代码对我的视图(一个背景视图和一个文本叠加层)非常有效。但是需要进行两个更改:1. 在changeFrameWithAnimation的顶部添加[self.animationTimer invalidate],2. 将动画持续时间更改为0.3,并更改步数。目前,你每10毫秒调用一次计时器,但实际上只需要在60Hz刷新率下每16毫秒调用一次。 - Jon
谢谢@Jon,我没有考虑到60Hz的刷新率,但在我的情况下,整个过程会变慢,因为我的视图层次结构很重,所以它花费的时间比预期的要长。因此,不得不改变需求本身! - Raj Pawan Gumdal

2
正如Duncan所指出的,Core Animation在调整大小时不会每帧重绘您的UIView图层的内容。 您需要使用计时器自己完成这项工作。
Omni的团队发布了一个很好的示例,说明如何基于自定义属性进行动画,这可能适用于您的情况。 该示例以及其工作原理的说明可以在此处找到。

1
如果动画不大或者动画持续时间不是问题,可以使用CADisplayLink。例如,它可以平滑地缩放自定义UIView绘制的UIBezierPaths动画。以下是一个示例代码,您可以根据自己的图片进行调整。
我的自定义视图的ivars包含CADisplayLink作为displayLink,以及两个CGRects: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;
}

请注意,我尚未测试其性能,但它确实运行顺畅。

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边框的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接