I'm working on a project where I'm using a UICollectionView to create an 'image ticker' where I'm advertising a series of logos. The collectionView is one item high and twelve items long, and shows two to three items at a time (depending on size of the logos visible).
I would like to make a slow automatic scrolling animation from the first item to the last, and then repeat.
Has anyone been able to make this work? I can get the scrolling working using
[myCollection scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:(myImages.count -1) inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:YES];
But this is way too fast!
[UIView animateWithDuration:10 delay:2 options:(UIViewAnimationOptionAutoreverse + UIViewAnimationOptionRepeat) animations:^{
[myCollection scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:(myImages.count -1) inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
} completion:nil];
This yields the desired scrolling speed, but only the last few cells are visible in the series. I suspect they (and even the starting visible cells) are being dequeued immediately.
Any thoughts?
You can try this approach:
#property (nonatomic, assign) CGPoint scrollingPoint, endPoint;
#property (nonatomic, strong) NSTimer *scrollingTimer;
#synthesize scrollingPoint, endPoint;
#synthesize scrollingTimer;
- (void)scrollSlowly {
// Set the point where the scrolling stops.
self.endPoint = CGPointMake(0, 300);
// Assuming that you are starting at {0, 0} and scrolling along the x-axis.
self.scrollingPoint = CGPointMake(0, 0);
// Change the timer interval for speed regulation.
self.scrollingTimer = [NSTimer scheduledTimerWithTimeInterval:0.015 target:self selector:#selector(scrollSlowlyToPoint) userInfo:nil repeats:YES];
}
- (void)scrollSlowlyToPoint {
self.collectionView.contentOffset = self.scrollingPoint;
// Here you have to respond to user interactions or else the scrolling will not stop until it reaches the endPoint.
if (CGPointEqualToPoint(self.scrollingPoint, self.endPoint)) {
[self.scrollingTimer invalidate];
}
// Going one pixel to the right.
self.scrollingPoint = CGPointMake(self.scrollingPoint.x, self.scrollingPoint.y+1);
}
I use a "1 pixel offset" trick. When scrolling programmatically, just set the end contentOffset.x to 1 pixel more/less than it should. This should be unnoticeable. And in completion block set it to actual value. That way you can avoid the premature cell dequeuing problem and get smooth scrolling animation ;)
Here is an example of scrolling to the right page (e.g. from page 1 to 2). Notice that in the animations: block I actually scrolls one pixel less (pageWidth * nextPage - 1). I then restore the correct value in the completion: block.
CGFloat pageWidth = self.collectionView.frame.size.width;
int currentPage = self.collectionView.contentOffset.x / pageWidth;
int nextPage = currentPage + 1;
[UIView animateWithDuration:1
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
[self.collectionView setContentOffset:CGPointMake(pageWidth * nextPage - 1, 0)];
} completion:^(BOOL finished) {
[self.collectionView setContentOffset:CGPointMake(pageWidth * nextPage, 0)];
}];
You could also have a "slow" scroll to the end of you UICollectionView without having to "jump" from indexpath to indexpath. I created this quickly for a collection view on a TVOS app:
func autoScroll () {
let co = collectionView.contentOffset.x
let no = co + 1
UIView.animateWithDuration(0.001, delay: 0, options: .CurveEaseInOut, animations: { [weak self]() -> Void in
self?.collectionView.contentOffset = CGPoint(x: no, y: 0)
}) { [weak self](finished) -> Void in
self?.autoScroll()
}
}
I just call the autoScroll() in the viewDidLoad() and the rest takes care of it itself. The speed of the scrolling is decided by the animation time of the UIView. You could (I haven't tried) add an NSTimer with 0 seconds instead so you can invalidate it on userscroll.
For anyone else finding this, I've updated Masa's suggestion to work on Swift and I've introduced a little bit of easing towards the end so it acts more like the original scrollItemsToIndexPath animated call. I have hundreds of items in my view so a steady pace wasn't an option for me.
var scrollPoint: CGPoint?
var endPoint: CGPoint?
var scrollTimer: NSTimer?
var scrollingUp = false
func scrollToIndexPath(path: NSIndexPath) {
let atts = self.collectionView!.layoutAttributesForItemAtIndexPath(path)
self.endPoint = CGPointMake(0, atts!.frame.origin.y - self.collectionView!.contentInset.top)
self.scrollPoint = self.collectionView!.contentOffset
self.scrollingUp = self.collectionView!.contentOffset.y > self.endPoint!.y
self.scrollTimer?.invalidate()
self.scrollTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: "scrollTimerTriggered:", userInfo: nil, repeats: true)
}
func scrollTimerTriggered(timer: NSTimer) {
let dif = fabs(self.scrollPoint!.y - self.endPoint!.y) / 1000.0
let modifier: CGFloat = self.scrollingUp ? -30 : 30
self.scrollPoint = CGPointMake(self.scrollPoint!.x, self.scrollPoint!.y + (modifier * dif))
self.collectionView?.contentOffset = self.scrollPoint!
if self.scrollingUp && self.collectionView!.contentOffset.y <= self.endPoint!.y {
self.collectionView!.contentOffset = self.endPoint!
timer.invalidate()
} else if !self.scrollingUp && self.collectionView!.contentOffset.y >= self.endPoint!.y {
self.collectionView!.contentOffset = self.endPoint!
timer.invalidate()
}
}
Tweaking the values of dif and modifier adjusts the duration/level of ease for most situations.
In .h file,
declare this timer,
NSTimer *Timer;
Place this timer code,
[CollectionView reloadData];
Timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:#selector(AutoScroll) userInfo:nil repeats:YES];
then,
#pragma mark- Auto scroll image collectionview
-(void)AutoScroll
{
if (i<[Array count])
{
NSIndexPath *IndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.ImageCollectionView scrollToItemAtIndexPath:IndexPath
atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
i++;
if (i==[Array count])
{
i=0;
}
}
}
Hope it'll be useful.
Related
I have a UIImageView rotating forever on my screen... I use the code below, which I found HERE:
- (void) spinWithOptions: (UIViewAnimationOptions) options {
[UIView animateWithDuration: 0.0f
delay: 0.0f
options: options
animations: ^{
Hand.transform = CGAffineTransformRotate(Hand.transform, M_PI / 30);
}
completion: ^(BOOL finished) {
if (finished) {
if (animating) { //if, keep spinning
[self spinWithOptions: UIViewAnimationOptionCurveLinear];
} else if (options != UIViewAnimationOptionCurveEaseOut) { //final spin
//[self spinWithOptions: UIViewAnimationOptionCurveEaseOut];
}
}
}];
}
- (void) startSpin {
if (!animating) {
animating = YES;
[self spinWithOptions: UIViewAnimationOptionCurveEaseIn];
}
}
- (void) stopSpin {
animating = NO;
}
It worked find and the rotation seems smooth...
I've since added a countdown timer, using the follow:
-(void)timerTick:(NSTimer *)timer{
ticks -= 0.1; //NSLog(#"Total Ticks: %.2f",ticks);
if(ticks < 0.0){ //3 seconds is up
Failed.image = [UIImage imageNamed:#"Failed.png"];
ticks = 0.0;
[self GameOver]; //out of time...
}
else {
FailedBeforeTimer.image = [UIImage imageNamed:#"Failed Before.png"];
}
CountDownTimer.text = [NSString stringWithFormat:#"%.1f",ticks];
}
-(void)startTimer{
ticks = 3.0;
timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(timerTick:) userInfo:nil repeats:YES];
}
-(void)stopTimer{
[timer invalidate];
ticks = 0.0;
}
Since adding this timer to my app, the rotation animation is really blocky and no longer a smooth rotation...
Any ideas what I should implement to solve this?
To check, I removed the timer and re-run the application, and sure enough, the animation was smooth again :(
UPDATE:
After some more trial and error, it seems to be this line of code that slows the process down: CountDownTimer.text = [NSString stringWithFormat:#"%.1f",ticks]; ie. When I don't display the countDownTimer text on the screen/update label on screen), the rotation is smooth still...
So confused... What can I do?
Animating with zero-duration animations does not make sense. That's not how UIView animation is supposed to work. My guess is that is the source of your problems. A zero duration animation will be jumpy by definition.
You should be animating your changes over some time-period. (Always use a non-zero duration.)
EDIT:
Note that your timerTick method is horribly inefficient. You load the same image into an image view on every tick. Don't do that. Set it up with the initial image, and then only change the image when your tick count reaches zero.
You should give CADisplayLink a try, it should be suitable for your case. It's easy to use, there are heaps of tutorials out there, just do a search.
I have a homepage that showing the value of incomes and expenses. Right now the values just appear on the the screen, however is was wondering whether i can make those number start from 0 and then have an animation to increment that value, like a stopwatch. Is it also possible to make the final number after the increment has finished to animate with a pulse". This is how i am making the label pulsate now but the label becomes bigger from the left had side and not from the center.
[self.view addSubview:_lblTotal];
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scaleAnimation.duration = 0.4;
scaleAnimation.repeatCount = HUGE_VAL;
scaleAnimation.autoreverses = YES;
scaleAnimation.fromValue = [NSNumber numberWithFloat:0.8];
scaleAnimation.toValue = [NSNumber numberWithFloat:1.0];
[_lblTotal.layer addAnimation:scaleAnimation forKey:#"scale"];
The following open source UILabel subclass does the scrolling numbers bit you are looking for.
https://github.com/dataxpress/UICountingLabel
Example of the type of usage is:
myLabel.method = UILabelCountingMethodLinear;
[myLabel countFrom:50 to:100 withDuration:5.0f];
I would look to see how that is done and then extend it to add the pulse at the end once the animation for the counting has completed.
yes this is possible.
add a variable with the number to increment
#property (nonatomic) double currentValue;
#property (nonatomic, retain) NSTimer *timer;
initialize it in viewDidLoad
_currentValue = 0.00;
write a function updateNumber
-(void)updateNumber {
_currentValue = _currentValue + 0.01;
[_label setText:[NSString stringWithFormat:#"%.2f", _currentValue]];
if(_currentValue >= 100.0) { //example to stop incrementation on 100
_timer = nil; // stop the timer
// do your animation
}
}
in your viewDidAppear() or whenever you want to start the counting like this
if(_timer == nil) {
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 //seconds
target:self
selector:#selector(updateNumber)
userInfo:nil
repeats:true];
}
You might want to separate the incrementing number and the pulse animation into two different questions.
On the question of the pulse, the code is correct but the reason it scales off-center is probably due to your view's frame which is bigger than its content, which appears to be right-aligned rather than centered.
If you make _lblTotal's frame fit the content, by calling [_lblTotal sizeToFit]; your pulse animation should work as expected. But then this assumes that you do the right-alignment yourself.
Here is an example code , which will surely help you but you have to customize the timing as it certainly suit my need:-
-(void)viewDidLoad
{
currentValueAmount=0; //provide initial value for your incremented label
targetFloatAmount=1300;//provide the target value up-to which the value will be incremented.
self.lblAmount.text=[NSString stringWithFormat:#"%.2f",currentValueAmount];
[self performSelector:#selector(animateIncrementLabel) withObject:nil afterDelay:0.5f]; // give some delay as per your choice as to load the whole UI first and then calling the method to increment.
}
-(void)animateIncrementLabel
{
self.lblAmount.transform=CGAffineTransformMakeScale(0.000f, 0.000f);
timer = [NSTimer scheduledTimerWithTimeInterval: 0.001 //calling timer , if the label value is very small then increase the no of Zero's for example 0.00001
target: self
selector: #selector(handleTimer:)
userInfo: nil
repeats: YES];
//animating the incremented text with some transform and bounce
[UIView animateWithDuration:2.1 delay:0.2f usingSpringWithDamping:0.4 initialSpringVelocity:0.3 options:0 animations:^{
self.lblAmount.transform=CGAffineTransformMakeScale(1.3, 1.3);
}];
}
- (void) handleTimer: (NSTimer *) timer
{
if(currentValueAmount>targetFloatAmount)
{
self.lblAmount.text=[NSString stringWithFormat:#"%.2f",targetFloatAmount];
[timer invalidate];
timer=nil;
[UIView animateWithDuration:0.5 delay:0.2f usingSpringWithDamping:0.4 initialSpringVelocity:0.3 options:0 animations:^{
self.lblAmount.transform=CGAffineTransformMakeScale(1.0, 1.0);
}];
}
else
{
currentValueAmount=currentValueAmount+0.0008;
self.lblAmount.text=[NSString stringWithFormat:#"%.2f",currentValueAmount];
}
}
I have a tall UIImageView scrolling across the screen when a button is pressed. I used a timer to change the y position of the image at a specific speed. The image is 10,000px tall.
Here's my simple code:
-(void) moveImage {
myImage.center = CGPointMake(myImage.center.x, myImage.center.y +6);
}
-(IBAction)StartGame:(id)sender{
moveImageTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(moveImage)userInfo:nil repeats:YES];
[StartGame setHidden:TRUE];
}
Here's what i've tried to stop the timer when the image has scrolled completely through the screen:
-(void) moveImage {
myImage.center = CGPointMake(myImage.center.x, myImage.center.y +6);
if ([myImage == GPointMake(myImage.center.x, myImage.center.y -568) {
[moveImageTimer invalidate];
}
That doesn't work but i thought it would. Can someone tell me why the IF statement doesn't work? Any help is appreciated, thanks
Since you want to stop when the image moves to the bottom corner.
-(void) moveImage {
CGPoint newPoint= CGPointMake(myImage.center.x, myImage.center.y +6);
myImage.center = newPoint;
if ((newPoint.y - (myImage.frame.size.width/2)) >= [UIScreen mainScreen].bounds.size.height)
{
[moveImageTimer invalidate];
}
}
you can check the condition
like if (imageView.center.y > imageView.frame.size.height - self.view.frame.size.height)
here imageView.frame.size.height - self.view.frame.size.height because the image size is twice screen size. You have to set you logic
imageView.center = CGPointMake(imageView.center.x, imageView.center.y + 6);
if (imageView.center.y > imageView.frame.size.height - self.view.frame.size.height)
{
[moveImageTimer invalidate];
}
}
I want to add an UIImageView to my view at the users touch location and have the UIImageView grow while the user is holding their finger down. Think of a ballon being blown up. I want the center of the UIImageView to remain at the user's touch location while its growing.
I figured the best way would be a UILongPressGestureRecognizer and wrote the below. This does work as I planed except the visual effect is somewhat choppy and clumsy.
Is there any way that I can animate the UIImageView's size until the UILongPressGestureRecognizer calls UIGestureRecognizerStateEnded?
Or, is there a better way to do this altogether?
declared in .h: CGPoint longPressLocation;
.m:
- (IBAction) handleInflation:(UILongPressGestureRecognizer *) inflateGesture {
longPressLocation= [inflateGesture locationInView:self.view];
switch (inflateGesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(#"Long press Began .................");
inflateTimer = [NSTimer scheduledTimerWithTimeInterval:.4 target:self selector:#selector(inflate) userInfo:nil repeats:YES];
UIImage *tempImage=[UIImage imageNamed:#"bomb.png"];
UIImageView *inflatableImageView = [[UIImageView alloc] initWithFrame:CGRectMake(longPressLocation.x-tempImage.size.width/2,
longPressLocation.y-tempImage.size.height/2,
tempImage.size.width, tempImage.size.height)];
inflatableImageView.image = tempImage;
[bonusGame addSubview:inflatableImageView];
inflatable=inflatableImageView;
}
break;
case UIGestureRecognizerStateChanged:{
NSLog(#"Long press Changed .................");
}
break;
case UIGestureRecognizerStateEnded:{
NSLog(#"Long press Ended .................");
[inflateTimer invalidate];
}
break;
default:
break;
}
}
-(void)inflate{
inflatable.frame=CGRectMake(inflatable.frame.origin.x,inflatable.frame.origin.y , inflatable.bounds.size.width+15, inflatable.bounds.size.height+15);
inflatable.center=longPressLocation;
}
Final Working Code:
- (IBAction) handleInflation:(UILongPressGestureRecognizer *) inflateGesture {
inflateGesture.minimumPressDuration = .01;
longPressLocation= [inflateGesture locationInView:self.view];
switch (inflateGesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(#"Long press Began .................");
inflateStart = [NSDate date];
inflateDisplayLink = [NSClassFromString(#"CADisplayLink") displayLinkWithTarget:self selector:#selector(inflate)];
[inflateDisplayLink setFrameInterval:1];
[inflateDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
UIImage *tempImage=[UIImage imageNamed:#"bomb.png"];
UIImageView *inflatableImageView = [[UIImageView alloc] initWithFrame:CGRectMake(longPressLocation.x-tempImage.size.width/2,
longPressLocation.y-tempImage.size.height/2,
tempImage.size.width, tempImage.size.height)];
inflatableImageView.image = tempImage;
[bonusGame addSubview:inflatableImageView];
inflatable=inflatableImageView;
}
break;
case UIGestureRecognizerStateChanged:{
NSLog(#"Long press Changed .................");
}
break;
case UIGestureRecognizerStateEnded:{
NSLog(#"Long press Ended .................");
[inflateDisplayLink invalidate];
}
break;
default:
break;
}
}
-(void)inflate{
NSDate *inflateEnd = [NSDate date];
NSTimeInterval inflateInterval;
inflateInterval = ([inflateEnd timeIntervalSince1970] - [inflateStart timeIntervalSince1970])*25;
inflatable.frame=CGRectMake(inflatable.frame.origin.x,
inflatable.frame.origin.y ,
inflatable.bounds.size.width+inflateInterval,
inflatable.bounds.size.height+inflateInterval);
inflatable.center=longPressLocation;
if(inflatable.bounds.size.width>200){
[inflateDisplayLink invalidate];
}
}
A timer may not be smooth. Instead check out CADisplayLink, which will provide a delegate callback whenever the device's screen is redrawn (~60hz), so you will get a per frame chance to adjust your balloon size.
Another thing to consider is the time between refreshes isn't constant, it could be a lot slower than when the last refresh occured, so if you are incrementing the size by a constant 15 every time you get a callback, then the animation may not seem smooth.
To combat this, when you start the animation take a timestamp and hold onto it, then when you inflate the balloon take another timestamp and determine the difference between now and the last redraw, then multiply the difference by some value, which will ensure a constant smooth size growth - this is called a timestep.
https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/Reference/Reference.html
Getting creative here, but I think this might work:
You start an animation that animates the images frame from its current size towards a maximum size.
Start the animation as soon as you detect the long press gesture.
If the gesture ends, get the current frame size from the presentationLayer of the animated view/image and update the view/image's frame to that size by starting a new animation with UIViewAnimationOptionBeginFromCurrentState so that the old animation will stop.
That should give you a smoothly growing balloon.
Try this....
imageView = [[UIImageView alloc] initWithFrame:(CGRectMake(120, 100, 30, 30))];
imageView.backgroundColor = [UIColor redColor];
[self.view addSubview:imageView];
imageView.userInteractionEnabled = TRUE;
UILongPressGestureRecognizer *g = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[g setMinimumPressDuration:0.001];
[imageView addGestureRecognizer:g];
And Add these methods
-(void)longPress:(UITapGestureRecognizer *)t
{
if (t.state == UIGestureRecognizerStateBegan)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeIncrc) userInfo:nil repeats:TRUE];
}
else if (t.state == UIGestureRecognizerStateEnded || t.state == UIGestureRecognizerStateCancelled)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeDec) userInfo:nil repeats:TRUE];
}
}
-(void)makeIncrc
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:(UIViewAnimationCurveLinear)];
CGRect frame = imageView.frame;
frame.origin.x = frame.origin.x - 5;
frame.origin.y = frame.origin.y - 5;
frame.size.width = frame.size.width + 10;
frame.size.height = frame.size.height + 10;
imageView.frame = frame;
[UIView commitAnimations];
}
-(void)makeDec
{
if (imageView.frame.size.width < 20 || imageView.frame.size.height < 20)
{
[timer invalidate];
}
else
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:(UIViewAnimationCurveLinear)];
CGRect frame = imageView.frame;
frame.origin.x = frame.origin.x + 5;
frame.origin.y = frame.origin.y + 5;
frame.size.width = frame.size.width - 10;
frame.size.height = frame.size.height - 10;
imageView.frame = frame;
[UIView commitAnimations];
}
}
Here's a Swift version. I had to replace invalidate() calls with "paused" in order to avoid runaway inflation.
var inflateDisplayLink: CADisplayLink?
var inflateStart: NSDate!
var longPressLocation: CGPoint!
var inflatable: UIImageView!
func handleInflation(inflateGesture: UILongPressGestureRecognizer) {
longPressLocation = inflateGesture.locationInView(self.view)
let imageView = inflateGesture.view as! UIImageView
switch (inflateGesture.state) {
case .Began:
println("Long press Began .................")
inflateStart = NSDate()
let tempImageView = UIImageView(image: imageView.image)
tempImageView.contentMode = .ScaleAspectFit
tempImageView.frame = imageView.frame
self.view.addSubview(tempImageView)
inflatable = tempImageView
if inflateDisplayLink == nil {
inflateDisplayLink = CADisplayLink(target: self, selector: Selector("inflate"))
inflateDisplayLink!.frameInterval = 1
inflateDisplayLink!.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
} else {
inflateDisplayLink!.paused = false
}
break
case .Changed:
println("Long press Changed .................")
break
case .Ended:
println("Long press Ended .................")
inflateDisplayLink!.paused = true
break
default:
break
}
}
func inflate() {
var inflateEnd = NSDate()
// Convert from Double to CGFloat, due to bug (?) on iPhone5
var inflateInterval: CGFloat = CGFloat((Double(inflateEnd.timeIntervalSince1970) - Double(inflateStart.timeIntervalSince1970))) * 5.0
inflatable.frame = CGRectMake(inflatable.frame.origin.x, inflatable.frame.origin.y, inflatable.bounds.size.width + inflateInterval, inflatable.bounds.size.height + inflateInterval)
inflatable.center = longPressLocation.center
if inflatable.bounds.size.width > 200 {
inflateDisplayLink!.paused = true
}
}
I use NSTimer to do the auto slide and my code is like this:
NSTimer *timer;
timer = [NSTimer scheduledTimerWithTimeInterval: 5
target: self
selector: #selector(handleTimer)
userInfo: nil
repeats: YES];
- (void)handleTimer
{
int page = _bannerScrollView.contentOffset.x / 296;
if ( page + 1 < [array count] )
{
page++;
_pageControl.currentPage = page++;
}
else
{
page = 0;
_pageControl.currentPage = page;
}
[self changePage];
}
- (void)changePage
{
int page = _pageControl.currentPage;
[_bannerScrollView setContentOffset:CGPointMake(296 * page, 0)];
}
And I need some animation stuff, because it is cool.
How can I do this? Thank you very much.
You can set frames according to ur needs and it will animate to next view with page control
[scrollview scrollRectToVisible:CGRectMake(320, 80,320, 350) animated:YES];