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?
No, it is not needed in this case. animations and completion are not retained by self so there is no risk of strong retain cycle.
The contents of your block are performed on the main thread regardless of where you call [UIView animateWithDuration:animations:] .
To be exact, whenever you want to animate the view, you actually call layoutIfNeeded on the superview of that view. Try this instead: UIView. animate(withDuration: 0.1, delay: 0.1, options: UIViewAnimationOptions.
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.
You might want to have a look at http://www.nomadplanet.fr/2010/11/animate-calayer-custom-properties-with-coreanimation/
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 you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With