Question

I've got a custom UIView to show a tiled image.

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

Now I am trying to animate the resize of that view.

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

// change frame of view

[UIView commitAnimations];

What I would expect is for the tiled area just to grow, leaving the tile sizes constant. What happens instead is that while the area grows the original content of the view is scaled to the new size. At least during the animation. So the at the beginning and the end of the animation all is good. During the animation the tiles are distorted.

Why is CA trying to scale? How can I prevent it from doing so? What did I miss?

Was it helpful?

Solution

If Core Animation had to call back to your code for every animation frame it would never be as fast as it is, and animation of custom properties has been a long requested feature and FAQ for CA on the Mac.

Using UIViewContentModeRedraw is on the right track, and is also the best you'll get from CA. The problem is from the UIKit point of view the frame only has two values: the value at the beginning of the transition and the value at the end of the transition, and that's what you're seeing. If you look at the Core Animation architecture documents you'll see how CA has a private representation of all layer properties and their values changing over time. That's where the frame interpolation is happening, and you can't be notified of changes to that as they happen.

So the only way is to use an NSTimer (or performSelector:withObject:afterDelay:) to change the view frame over time, the old fashioned way.

OTHER TIPS

I was facing similar problem in a different context, I stumbled upon this thread and found Duncan's suggestion of setting frame in NSTimer. I implemented this code to achieve the same:

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

I found out that it takes a long time to set the frame and the timer to complete its operation. It probably depends on how deep the view hierarchy is on which you call -setFrame using this approach. And probably this is the reason why the view is first re-sized and then animated to the origin in the sdk. Or perhaps, is there some problem in the timer mechanism in my code due to which the performance has hindered?

It works, but it is very slow, maybe because my view hierarchy is too deep.

As Duncan indicates, Core Animation won't redraw your UIView's layer's content every frame as it is resized. You need to do that yourself using a timer.

The guys at Omni posted a nice example of how to do animations based on your own custom properties that might be applicable to your case. That example, along with an explanation for how it works, can be found here.

If it is not a big animation or performance isn't an issue for the animation duration, you can use a CADisplayLink. It does smooth out the animation of the scaling of a custom UIView drawing UIBezierPaths for instance. Below is a sample code you can adapt for your image.

The ivars of my custom view contains displayLink as a CADisplayLink, and two CGRects: toFrame and fromFrame, as well as duration and 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;
}

Note that I haven't tested its performance, but it does work smoothly.

I stumbled upon this question when trying to animate a size change on a UIView with a border. I hope that others who similarly arrive here might benefit from this answer. Originally I was creating a custom UIView subclass and overriding the drawRect method as follows:

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

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

    [super drawRect:rect];
}

This led to scaling problems when combining with animations as others have mentioned. The top and bottom of the border would grow too thick or two thin and appear scaled. This effect is easily noticeable when toggling on Slow Animations.

The solution was to abandon the custom subclass and instead use this approach:

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

This fixed the issue with scaling a border on a UIView during animations.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top