Currently I have an image of clouds of 2048x435 that scrolls across a Landscaped oriented UIImageView of 1024x435 using CABasicAnimation. The cloud image scrolls as it shou
You say that your image is 2048 wide and your view is 1024 wide. I don't know if this means you have duplicated the contents of a 1024-wide image to make a 2048-wide image.
Anyway, here's what I suggest. We'll need to store the cloud layer and its animation in instance variables:
@implementation ViewController {
CALayer *cloudLayer;
CABasicAnimation *cloudLayerAnimation;
}
Instead of setting the cloud layer's content to the cloud image, we set its background color to a pattern color created from the image. That way, we can set the layer's bounds to whatever we want and the image will be tiled to fill the bounds:
-(void)cloudScroll {
UIImage *cloudsImage = [UIImage imageNamed:@"TitleClouds.png"];
UIColor *cloudPattern = [UIColor colorWithPatternImage:cloudsImage];
cloudLayer = [CALayer layer];
cloudLayer.backgroundColor = cloudPattern.CGColor;
However, a CALayer's coordinate system puts the origin at the lower left instead of the upper left, with the Y axis increasing up. This means that the pattern will be drawn upside-down. We can fix that by flipping the Y axis:
cloudLayer.transform = CATransform3DMakeScale(1, -1, 1);
By default, a layer's anchor point is at its center. This means that setting the layer's position sets the position of its center. It will be easier to position the layer by setting the position of its upper-left corner. We can do that by moving its anchor point to its upper-left corner:
cloudLayer.anchorPoint = CGPointMake(0, 1);
The width of the layer needs to be the width of the image plus the width of the containing view. That way, as we scroll the layer so that the right edge of the image comes into view, another copy of the image will be drawn to the right of the first copy.
CGSize viewSize = self.cloudsImageView.bounds.size;
cloudLayer.frame = CGRectMake(0, 0, cloudsImage.size.width + viewSize.width, viewSize.height);
Now we're ready to add the layer to the view:
[self.cloudsImageView.layer addSublayer:cloudLayer];
Now let's set up the animation. Remember that we changed the layer's anchor point, so we can control its position by setting the position of its upper-left corner. We want the layer's upper-left corner to start at the view's upper-left corner:
CGPoint startPoint = CGPointZero;
and we want the layer's upper-left corner to move left by the width of the image:
CGPoint endPoint = CGPointMake(-cloudsImage.size.width, 0);
The rest of the animation setup is the same as your code. I changed the duration to 3 seconds for testing:
cloudLayerAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
cloudLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
cloudLayerAnimation.fromValue = [NSValue valueWithCGPoint:startPoint];
cloudLayerAnimation.toValue = [NSValue valueWithCGPoint:endPoint];
cloudLayerAnimation.repeatCount = HUGE_VALF;
cloudLayerAnimation.duration = 3.0;
We'll call another method to actually attach the animation to the layer:
[self applyCloudLayerAnimation];
}
Here's the method that applies the animation:
- (void)applyCloudLayerAnimation {
[cloudLayer addAnimation:cloudLayerAnimation forKey:@"position"];
}
When the application enters the background (because the user switched to another app), the system removes the animation from the cloud layer. So we need to reattach it when we enter the foreground again. That's why we have the applyCloudLayerAnimation
method. We need to call that method when the app enters the foreground.
In viewDidAppear:
, we can start observing the notification that tells us the app has entered the foreground:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
We need to stop observing the notification when our view disappears, or when the view controller is deallocated:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}
When the view controller actually receives the notification, we need to apply the animation again:
- (void)applicationWillEnterForeground:(NSNotification *)note {
[self applyCloudLayerAnimation];
}
Here's all the code together for easy copy and paste:
- (void)viewDidLoad {
[self cloudScroll];
[super viewDidLoad];
}
-(void)cloudScroll {
UIImage *cloudsImage = [UIImage imageNamed:@"TitleClouds.png"];
UIColor *cloudPattern = [UIColor colorWithPatternImage:cloudsImage];
cloudLayer = [CALayer layer];
cloudLayer.backgroundColor = cloudPattern.CGColor;
cloudLayer.transform = CATransform3DMakeScale(1, -1, 1);
cloudLayer.anchorPoint = CGPointMake(0, 1);
CGSize viewSize = self.cloudsImageView.bounds.size;
cloudLayer.frame = CGRectMake(0, 0, cloudsImage.size.width + viewSize.width, viewSize.height);
[self.cloudsImageView.layer addSublayer:cloudLayer];
CGPoint startPoint = CGPointZero;
CGPoint endPoint = CGPointMake(-cloudsImage.size.width, 0);
cloudLayerAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
cloudLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
cloudLayerAnimation.fromValue = [NSValue valueWithCGPoint:startPoint];
cloudLayerAnimation.toValue = [NSValue valueWithCGPoint:endPoint];
cloudLayerAnimation.repeatCount = HUGE_VALF;
cloudLayerAnimation.duration = 3.0;
[self applyCloudLayerAnimation];
}
- (void)applyCloudLayerAnimation {
[cloudLayer addAnimation:cloudLayerAnimation forKey:@"position"];
}
- (void)viewDidUnload {
[self setCloudsImageView:nil];
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)applicationWillEnterForeground:(NSNotification *)note {
[self applyCloudLayerAnimation];
}
original solution with options for vertical and horizontal scroll
//
// originally found here: http://stackoverflow.com/questions/8790079/animate-infinite-scrolling-of-an-image-in-a-seamless-loop
//
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
@interface TiledCloundScrollViewController : UIViewController {
CALayer *cloudLayer;
CABasicAnimation *cloudLayerAnimation;
UIImage *cloudsImage;
BOOL verticalScroll;
CFTimeInterval animationDuration;
}
- (id) initWithImage:(UIImage*)cloudsImage verticalScroll:(BOOL)verticalScroll animationDuration:(CFTimeInterval)animationDuration;
@end
#import "TiledCloundScrollViewController.h"
@interface TiledCloundScrollViewController ()
@end
@implementation TiledCloundScrollViewController
- (id) init {
[self doesNotRecognizeSelector:_cmd];
return nil;
}
- (id) initWithImage:(UIImage*)image verticalScroll:(BOOL)vScroll animationDuration:(CFTimeInterval)duration {
self = [super init];
if (self ) {
cloudsImage = image;
verticalScroll = vScroll;
animationDuration = duration;
}
return self;
}
- (void) viewDidLoad {
[super viewDidLoad];
self.view.clipsToBounds = YES;
const CGSize viewSize = self.view.bounds.size;
const CGSize imageSize = cloudsImage.size;
UIColor *cloudPattern = [UIColor colorWithPatternImage:cloudsImage];
cloudLayer = [CALayer layer];
cloudLayer.backgroundColor = cloudPattern.CGColor;
cloudLayer.transform = CATransform3DMakeScale(1, -1, 1);
cloudLayer.anchorPoint = CGPointMake(0, 1);
[self.view.layer addSublayer:cloudLayer];
CGPoint startPoint = CGPointZero;
CGPoint endPoint;
if (verticalScroll) {
endPoint = CGPointMake(0, -imageSize.height);
cloudLayer.frame = CGRectMake(0, 0, viewSize.width, viewSize.height + imageSize.height);
} else {
endPoint = CGPointMake(-imageSize.width, 0);
cloudLayer.frame = CGRectMake(0, 0, viewSize.width + imageSize.width, viewSize.height);
}
cloudLayerAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
cloudLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
cloudLayerAnimation.fromValue = [NSValue valueWithCGPoint:startPoint];
cloudLayerAnimation.toValue = [NSValue valueWithCGPoint:endPoint];
cloudLayerAnimation.repeatCount = HUGE_VALF;
cloudLayerAnimation.duration = animationDuration;
[self applyCloudLayerAnimation];
}
- (void) viewDidUnload {
cloudLayer = nil;
cloudLayerAnimation = nil;
[super viewDidUnload];
}
- (void) applyCloudLayerAnimation {
[cloudLayer addAnimation:cloudLayerAnimation forKey:@"position"];
}
- (void)applicationWillEnterForeground:(NSNotification *)note {
[self applyCloudLayerAnimation];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}
@end
Rob, you rock brother. I was looking for a way to do the same thing but vertically. After reading your answer above I had no problem adjusting it to suit my needs. For anyone that may end up here on their hunt for a vertical solution this is what I ended up with, modeled after the above:
- (IBAction)animateBackground6
{
UIImage *backgroundImage = [UIImage imageNamed:@"space.png"];
UIColor *backgroundPattern = [UIColor colorWithPatternImage:backgroundImage];
CALayer *background = [CALayer layer];
background.backgroundColor = backgroundPattern.CGColor;
background.transform = CATransform3DMakeScale(1, -1, 1);
background.anchorPoint = CGPointMake(0, 1);
CGSize viewSize = self.backgroundImageView.bounds.size;
background.frame = CGRectMake(0, 0, viewSize.width, backgroundImage.size.height + viewSize.height);
[self.backgroundImageView.layer addSublayer:background];
CGPoint startPoint = CGPointZero;
CGPoint endPoint = CGPointMake(0, -backgroundImage.size.height);
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.fromValue = [NSValue valueWithCGPoint:endPoint];
animation.toValue = [NSValue valueWithCGPoint:startPoint];
animation.repeatCount = HUGE_VALF;
animation.duration = 5.0;
[background addAnimation:animation forKey:@"position"];
}