Creating slow scrolling to indexPath in UICollectionView

19,134

Solution 1

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

Solution 2

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

Solution 3

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.

Solution 4

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.

Share:
19,134

Related videos on Youtube

Chris
Author by

Chris

Updated on June 07, 2022

Comments

  • Chris
    Chris about 2 years

    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?

    • Akash KR
      Akash KR about 8 years
      I tried your code, actually the thing is scrollToItemAtIndexPath drives you to the particular indexPath you are passing i.e [myImages.count -1] , so image at that indexPath is visible
  • Hlung
    Hlung over 9 years
    Down voting with no reason comment doesn't do any good, you know?
  • Sneakyness
    Sneakyness almost 9 years
    pageWidth and nextPage are not defined in the context of the answer
  • puru020
    puru020 over 8 years
    I am also trying do to something similar, but want to create a infinite loop (going to the first after the last one). How did you do that?
  • puru020
    puru020 over 8 years
    This make the UI thread block, if you have any other controls which requires focus change (like in tvos) this doesn't work.
  • Paul Peelen
    Paul Peelen over 8 years
    @puru020 I have that problem as well. If you have a collectionview with same size items than it might be easier. Basically what you do is you add the first few items to the end of the collectionview items, and the last of the range of items to the beginning. When the the last items are comming on screen you can reposition the contentOffset to the beginning of the UICollectionView without the user noticing; hence: infinite scroll. This might help: stackoverflow.com/questions/15549233/…
  • Superwayne
    Superwayne over 8 years
    Works great for scrolling forward but when scrolling backwards, the cell is dequeued while it's still visible.
  • Tal Zion
    Tal Zion about 8 years
    @PaulPeelenYou don't need to set capture list in Swift UIView block as this is executed in a later stage. It will not create a retain cycle.
  • fattjake
    fattjake over 6 years
    Using this code you need to make sure you do something to stop scrolling when the view disappears (when you navigate to another view but this view remains alive), because in our app this gets called continuously (like thousands of times per second) maxing out the CPU core in our app.
  • mdimarca
    mdimarca about 6 years
    How do you get it to auto reverse and repeat though in an animation block without messing it up?
  • Yuval Tal
    Yuval Tal over 5 years
    This is an infinite loop which is called 1000 times per second. Please note that the screen refreshes only 30 times per second.
  • Aznix
    Aznix about 4 years
    @Superwayne use currentPage - 1 for forward, currentPage + 1 for backward