nicklockwood / iCarousel

A simple, highly customisable, data-driven 3D carousel for iOS and Mac OS
http://www.charcoaldesign.co.uk/source/cocoa#icarousel
Other
12k stars 2.58k forks source link

Change scroll offset without visual effect and timer involved #325

Open aleqs opened 11 years ago

aleqs commented 11 years ago

Hi,

I implemented this feature locally so thought you might find it useful and include in iCarousel. The version I'm using is master@1.7.4

Sometimes when using carousel, I want to completely bypass the timer and have the scroll offset change to take effect immediately. So I created a version of setScrollOffset and didScroll methods that accept animated flag and placed setScrollOffset:animated to the h file:


- (void)setScrollOffset:(CGFloat)scrollOffset animated:(BOOL)animated;

m file:

- (void)setScrollOffset:(CGFloat)scrollOffset {
    [self setScrollOffset:scrollOffset animated:YES];
}

- (void)setScrollOffset:(CGFloat)scrollOffset animated:(BOOL)animated {
    if (_scrollOffset != scrollOffset) {
        _scrolling = NO;
        _decelerating = NO;
        [self disableAnimation];
        _scrollOffset = [self clampedOffset:scrollOffset];
        [self didScroll:animated];
        _previousItemIndex = self.currentItemIndex;
        [self depthSortViews];
        [self enableAnimation];
    }
}

...


- (void)didScroll {
    [self didScroll:YES];
}

- (void)didScroll:(BOOL)animated {
    if (_wrapEnabled || !_bounces) {
        _scrollOffset = [self clampedOffset:_scrollOffset];
    }
    else {
        CGFloat min = -_bounceDistance;
        CGFloat max = fmaxf(_numberOfItems - 1, 0.0f) + _bounceDistance;
        if (_scrollOffset < min) {
            _scrollOffset = min;
            _startVelocity = 0.0f;
        }
        else if (_scrollOffset > max) {
            _scrollOffset = max;
            _startVelocity = 0.0f;
        }
    }

    //check if index has changed
    NSInteger currentIndex = roundf(_scrollOffset);
    NSInteger difference = [self minScrollDistanceFromIndex:_previousItemIndex toIndex:currentIndex];
    if (difference) {
        _toggleTime = CACurrentMediaTime();
        _toggle = fmaxf(-1.0f, fminf(1.0f, -(CGFloat) difference));

#ifdef ICAROUSEL_MACOS

        if (_vertical)
        {
            //invert toggle
            _toggle = -_toggle;
        }

#endif
        if (animated) {
            [self startAnimation];
        }
    }

    [self loadUnloadViews];
    [self transformItemViews];

    if ([_delegate respondsToSelector:@selector(carouselDidScroll:)]) {
        [self enableAnimation];
        [_delegate carouselDidScroll:self];
        [self disableAnimation];
    }

    //notify delegate of change index
    if ([self clampedIndex:_previousItemIndex] != self.currentItemIndex &&
            [_delegate respondsToSelector:@selector(carouselCurrentItemIndexDidChange:)]) {
        [self enableAnimation];
        [_delegate carouselCurrentItemIndexDidChange:self];
        [self disableAnimation];
    }

    //DEPRECATED
    if ([self clampedIndex:_previousItemIndex] != self.currentItemIndex &&
            [_delegate respondsToSelector:@selector(carouselCurrentItemIndexUpdated:)]) {

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

        [(id <iCarouselDeprecated>) _delegate carouselCurrentItemIndexUpdated:self];

#pragma clang diagnostic pop

    }

    //update previous index
    _previousItemIndex = currentIndex;

    if (!animated) {
        _scrolling = NO;
        [self depthSortViews];
        if ([_delegate respondsToSelector:@selector(carouselDidEndScrollingAnimation:)]) {
            [self enableAnimation];
            [_delegate carouselDidEndScrollingAnimation:self];
            [self disableAnimation];
        }
    }
}
nicklockwood commented 11 years ago

There are already methods to do this:

scrollOffset is a readwrite property, so you can set it and the carousel will scroll to position immediately without an animation.

There is also the following method to animation the scroll:

- (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;
aleqs commented 11 years ago

Currently this is not true. Perhaps you intended it to work differently, but your implementation implicitly uses the scrollOffset property instead of using the _ scrollOffset variable in case of duration == 0.0, which causes the timer to be created and the change to take effect only after the timer triggers step method first time

- (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration {
    if (duration > 0.0) {
        _decelerating = NO;
        _scrolling = YES;
        _startTime = CACurrentMediaTime();
        _startOffset = _scrollOffset;
        _scrollDuration = duration;
        _previousItemIndex = roundf(_scrollOffset);
        _endOffset = _startOffset + offset;
        if (!_wrapEnabled) {
            _endOffset = [self clampedOffset:_endOffset];
        }
        if ([_delegate respondsToSelector:@selector(carouselWillBeginScrollingAnimation:)]) {
            [_delegate carouselWillBeginScrollingAnimation:self];
        }
        [self startAnimation];
    }
    else {
        self.scrollOffset += offset;
    }
}
nicklockwood commented 11 years ago

The scrollOffset property doesn't invoke the timer, it sets the _scrollOffset immediately using the same logic as your setScrollOffset:animated: method:

- (void)setScrollOffset:(CGFloat)scrollOffset
{
    if (_scrollOffset != scrollOffset)
    {
        _scrolling = NO;
        _decelerating = NO;
        [self disableAnimation];
        _scrollOffset = [self clampedOffset:scrollOffset];
        [self didScroll];
        _previousItemIndex = self.currentItemIndex;
        [self depthSortViews];
        [self enableAnimation];
    }
}
aleqs commented 11 years ago

yes it does :) setScrollOffset invokes didScroll which in turn calls startAnimation, and all the changes are then performed in step method

- (void)didScroll {
...
    if (difference) {
        _toggleTime = CACurrentMediaTime();
        _toggle = fmaxf(-1.0f, fminf(1.0f, -(CGFloat) difference));

#ifdef ICAROUSEL_MACOS

        if (_vertical)
        {
            //invert toggle
            _toggle = -_toggle;
        }

#endif
        [self startAnimation];
    }
...
}
aleqs commented 11 years ago

On a separate note, I want to say thank you for this awesome library. I totally love it and like the fact that you are so quick to respond. It gives me pleasure to participate in improving it

nicklockwood commented 11 years ago

I think you misunderstand the purpose of starting the timer in didScroll. It's is not to move the carousel to the specified offset, it is to so that the carousel can auto-correct if it has been misaligned.

I just checked and this is working as intended. In the tests folder of iCarousel there is a project called "Scrolling". Open that project, and run it. When you press the button, the carousel jumps to a particular point and then smoothly aligns to the nearest integer offset.

If you modify this function:

- (void)reloadAndScroll
{
    [carousel reloadData];
    [carousel scrollByOffset:4.5 duration:0.0];
}

To this:

- (void)reloadAndScroll
{
    carousel.scrollOffset += 4;
}

Now run it and press the button a few times. Notice that the carousel snaps instantly into position and doesn't animate?

It will still run the timer for a single step afterwards, but you can completely disable the timer by commenting out the body of the startAnimation method and it will still work exactly the same way.

nicklockwood commented 11 years ago

If you are trying to disable the auto-align behaviour when scrollOffset is set to a non-integer value, you can do that by setting carousel.scrollToItemBoundary = NO;

And you're welcome - I'm glad you like iCarousel :-)

aleqs commented 11 years ago

Yes good point thanks

The reason I asked this is I have a table view with nested iCarousel items in each table cell. When a user scrolls through table view, I want to set iCarousel offset so that a specific item be presented by default. However, despite the scrollOffset value being propagated correctly in cellForRowAtIndexPath method, I have a visual artefact of iCarousel first appearing with the state where the cell was left off, and only after a couple of seconds adjusting to the offset I set. After having another look, I can conclude that iCarousel is rendered stuck in the previous state while table view scrolling animation is running. When I scroll through the table view and see this artefact, it's all gone once animation stops or when I release the mouse

I thought I found a solution when posting this issue, but as you pointed out I didn't quite understand the purpose of didScroll method, so just targeted it to avoid animation. So I ran my modified code again, only to find that the issue I was fighting with is still there