美文网首页
iCarousel源码解读

iCarousel源码解读

作者: lqbk | 来源:发表于2018-06-19 13:55 被阅读0次

iCarousel简介:

iCarousel是一个优秀开源的视图控件,专门用于实现各种类型的旋转木马(分页滚动视图)iPhone、iPad和Mac OS,iCarousel实现一些常见的如圆柱、平面式的旋转木马。

阅读源码的原因:

在iOS中可以使用 UIScrollView 或者 UICollectionView 去取巧地实现一个无限循环的视图,取巧地方式非常便捷,但是有些情况会遇到小问题。因为 UIScrollView 的坐标系是基于Bounds的,要实现无限循环的效果就需要在滑动中跳动ContentOffset。

使用其他方式呢?实现一个无限循环的视图 首先要抽象出一个循环坐标系:

... 4 5 0 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 ...

再根据这个坐标系做布局就可以去实现一个无限循环的视图。

根据这种思路去实现就需要做到 :
1.管理抽象的坐标系
2.处理手势动作
3.管理滑动的动画

恰巧我发现iCarousel 是以这种思路实现的,所以我开始阅读iCarousel的源码


带着问题去读源码:
在开启了滑动动画的时候,假如有手势输入了,界面如何快速响应手势呢?如何有效地管理动画呢?


iCarousel主要流程图:

主要流程图

didScroll :

didScroll 主要做的是更新界面,首先会对offset坐标做一些更正,确保offset的正确。 第二步是管理视图的加载,滑动过程中,加载滑入界面的视图,然后移除滑出界面的视图。第三步是调整界面界面的视图3D transform 。最后进行的是协议的回调。


- (void)didScroll

{
  
    if (_wrapEnabled || !_bounces)

    {
        //循环状态下
        _scrollOffset = [self clampedOffset:_scrollOffset];

    }

    else

    {
        //去除bounces效果
        CGFloat min = -_bounceDistance;

        CGFloat max = MAX(_numberOfItems - 1, 0.0) + _bounceDistance;

        if (_scrollOffset < min)

        {

            _scrollOffset = min;

            _startVelocity = 0.0;

        }

        else if (_scrollOffset > max)

        {

            _scrollOffset = max;

            _startVelocity = 0.0;

        }

    }

    //如果index变化了

    NSInteger difference = [self minScrollDistanceFromIndex:self.currentItemIndex toIndex:self.previousItemIndex];

    if (difference)

    {

        _toggleTime = CACurrentMediaTime();

        _toggle = MAX(-1, MIN(1, difference));

#ifdef ICAROUSEL_MACOS

        if (_vertical)

        {

            //invert toggle

            _toggle = -_toggle;

        }

#endif

        [self startAnimation];

    }

    //加载没有加载的视图
    [self loadUnloadViews];   
    //界面变化
    [self transformItemViews];

    //delegate 回调

    if (fabs(_scrollOffset - _previousScrollOffset) > FLOAT_ERROR_MARGIN)

    {

        [self pushAnimationState:YES];

        [_delegate carouselDidScroll:self];

        [self popAnimationState];

    }

    //notify delegate of index change

    if (_previousItemIndex != self.currentItemIndex)

    {

        [self pushAnimationState:YES];

        [_delegate carouselCurrentItemIndexDidChange:self];

        [self popAnimationState];

    }

    //update previous index

    _previousScrollOffset = _scrollOffset;

    _previousItemIndex = self.currentItemIndex;

}

loadUnloadViews

loadUnloadViews 首先计算可视区域视图的数量,然后根据offset 和numberOfVisibleItems 得到 visibleIndices。然后遍历之前的可视视图字典,将已经滑出的视图移除,并加入了复用池中。最后遍历 visibleIndices 将没有在可视视图字典视图加入。

- (void)loadUnloadViews
{
    //set item width
    [self updateItemWidth];
    
    //update number of visible items
    [self updateNumberOfVisibleItems];
    
    //calculate visible view indices
    NSMutableSet *visibleIndices = [NSMutableSet setWithCapacity:_numberOfVisibleItems];
    NSInteger min = -(NSInteger)(ceil((CGFloat)_numberOfPlaceholdersToShow/2.0));
    NSInteger max = _numberOfItems - 1 + _numberOfPlaceholdersToShow/2;
    NSInteger offset = self.currentItemIndex - _numberOfVisibleItems/2;
    if (!_wrapEnabled)
    {
        offset = MAX(min, MIN(max - _numberOfVisibleItems + 1, offset));
    }
    for (NSInteger i = 0; i < _numberOfVisibleItems; i++)
    {
        NSInteger index = i + offset;
        if (_wrapEnabled)
        {
            index = [self clampedIndex:index];
        }
        CGFloat alpha = [self alphaForItemWithOffset:[self offsetForItemAtIndex:index]];
        if (alpha)
        {
            //only add views with alpha > 0
            [visibleIndices addObject:@(index)];
        }
    }
    
    //remove offscreen views
    for (NSNumber *number in [_itemViews allKeys])
    {
        if (![visibleIndices containsObject:number])
        {
            UIView *view = _itemViews[number];
            if ([number integerValue] < 0 || [number integerValue] >= _numberOfItems)
            {
                [self queuePlaceholderView:view];
            }
            else
            {
                [self queueItemView:view];
            }
            [view.superview removeFromSuperview];
            [(NSMutableDictionary *)_itemViews removeObjectForKey:number];
        }
    }
    
    //add onscreen views
    for (NSNumber *number in visibleIndices)
    {
        UIView *view = _itemViews[number];
        if (view == nil)
        {
            [self loadViewAtIndex:[number integerValue]];
        }
    }

step

回到开头的问题,iCarousel 处理动画是通过定时器去实现的帧动画。定时器的频率 1/60 与屏幕的刷新率相同,相当于 CADisplayLink。定时器每一次调用step 就相当于处理每一帧的动画。通过 endOffset 、scrollDuration 和currentTime,来得到每一帧的offset ,为了更符合真实,通过一个缓冲方程式计算,在滑动状态下 ,会实现easeIneaseOut 慢入慢出的效果,offset计算完成后会调用didScroll 进行真正的界面更新 。因为iCarousel 视图的变化是通过CATransform3D,所以每一次step 开头都会将 隐式动画关闭。

- (void)step
{
    [self pushAnimationState:NO];
    NSTimeInterval currentTime = CACurrentMediaTime();
    double delta = currentTime - _lastTime;
    _lastTime = currentTime;
    
    if (_scrolling && !_dragging)
    {
        NSTimeInterval time = MIN(1.0, (currentTime - _startTime) / _scrollDuration);
        delta = [self easeInOut:time];
        _scrollOffset = _startOffset + (_endOffset - _startOffset) * delta;
        [self didScroll];
        if (time >= 1.0)
        {
            _scrolling = NO;
            [self depthSortViews];
            [self pushAnimationState:YES];
            [_delegate carouselDidEndScrollingAnimation:self];
            [self popAnimationState];
        }
    }
    else if (_decelerating)
    {
        CGFloat time = MIN(_scrollDuration, currentTime - _startTime);
        CGFloat acceleration = -_startVelocity/_scrollDuration;
        CGFloat distance = _startVelocity * time + 0.5 * acceleration * pow(time, 2.0);
        _scrollOffset = _startOffset + distance;
        [self didScroll];
        if (fabs(time - _scrollDuration) < FLOAT_ERROR_MARGIN)
        {
            _decelerating = NO;
            [self pushAnimationState:YES];
            [_delegate carouselDidEndDecelerating:self];
            [self popAnimationState];
            if ((_scrollToItemBoundary || fabs(_scrollOffset - [self clampedOffset:_scrollOffset]) > FLOAT_ERROR_MARGIN) && !_autoscroll)
            {
                if (fabs(_scrollOffset - self.currentItemIndex) < FLOAT_ERROR_MARGIN)
                {
                    //call scroll to trigger events for legacy support reasons
                    //even though technically we don't need to scroll at all
                    [self scrollToItemAtIndex:self.currentItemIndex duration:0.01];
                }
                else
                {
                    [self scrollToItemAtIndex:self.currentItemIndex animated:YES];
                }
            }
            else
            {
                CGFloat difference = round(_scrollOffset) - _scrollOffset;
                if (difference > 0.5)
                {
                    difference = difference - 1.0;
                }
                else if (difference < -0.5)
                {
                    difference = 1.0 + difference;
                }
                _toggleTime = currentTime - MAX_TOGGLE_DURATION * fabs(difference);
                _toggle = MAX(-1.0, MIN(1.0, -difference));
            }
        }
    }
    else if (_autoscroll && !_dragging)
    {
        //autoscroll goes backwards from what you'd expect, for historical reasons
        self.scrollOffset = [self clampedOffset:_scrollOffset - delta * _autoscroll];
    }
    else if (fabs(_toggle) > FLOAT_ERROR_MARGIN)
    {
        NSTimeInterval toggleDuration = _startVelocity? MIN(1.0, MAX(0.0, 1.0 / fabs(_startVelocity))): 1.0;
        toggleDuration = MIN_TOGGLE_DURATION + (MAX_TOGGLE_DURATION - MIN_TOGGLE_DURATION) * toggleDuration;
        NSTimeInterval time = MIN(1.0, (currentTime - _toggleTime) / toggleDuration);
        delta = [self easeInOut:time];
        _toggle = (_toggle < 0.0)? (delta - 1.0): (1.0 - delta);
        [self didScroll];
    }
    else if (!_autoscroll)
    {
        [self stopAnimation];
    }
    
    [self popAnimationState];
}

didPan
pan手势的处理,根据不同的手势状态来 模拟真实滑动,如减速效果、弹簧bounces效果。

- (void)didPan:(UIPanGestureRecognizer *)panGesture
{
    if (_scrollEnabled && _numberOfItems)
    {
        switch (panGesture.state)
        {
            case UIGestureRecognizerStateBegan:
            {
                _dragging = YES;
                _scrolling = NO;
                _decelerating = NO;
                _previousTranslation = _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x;

#if defined(USING_CHAMELEON) && USING_CHAMELEON

                _previousTranslation = -_previousTranslation;
#endif

                [_delegate carouselWillBeginDragging:self];
                break;
            }
            case UIGestureRecognizerStateEnded:
            case UIGestureRecognizerStateCancelled:
            case UIGestureRecognizerStateFailed:
            {
                _dragging = NO;
                _didDrag = YES;
                if ([self shouldDecelerate])
                {
                    _didDrag = NO;
                    [self startDecelerating];
                }
                
                [self pushAnimationState:YES];
                [_delegate carouselDidEndDragging:self willDecelerate:_decelerating];
                [self popAnimationState];
                
                if (!_decelerating)
                {
                    if ((_scrollToItemBoundary || fabs(_scrollOffset - [self clampedOffset:_scrollOffset]) > FLOAT_ERROR_MARGIN) && !_autoscroll)
                    {
                        if (fabs(_scrollOffset - self.currentItemIndex) < FLOAT_ERROR_MARGIN)
                        {
                            //call scroll to trigger events for legacy support reasons
                            //even though technically we don't need to scroll at all
                            [self scrollToItemAtIndex:self.currentItemIndex duration:0.01];
                        }
                        else if ([self shouldScroll])
                        {
                            NSInteger direction = (int)(_startVelocity / fabs(_startVelocity));
                            [self scrollToItemAtIndex:self.currentItemIndex + direction animated:YES];
                        }
                        else
                        {
                            [self scrollToItemAtIndex:self.currentItemIndex animated:YES];
                        }
                    }
                    else
                    {
                        [self depthSortViews];
                    }
                }
                else
                {
                    [self pushAnimationState:YES];
                    [_delegate carouselWillBeginDecelerating:self];
                    [self popAnimationState];
                }
                break;
            }
            case UIGestureRecognizerStateChanged:
            {
                CGFloat translation = _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x;
                CGFloat velocity = _vertical? [panGesture velocityInView:self].y: [panGesture velocityInView:self].x;

#if defined(USING_CHAMELEON) && USING_CHAMELEON

                translation = -translation;
                velocity = -velocity;
#endif

                CGFloat factor = 1.0;
                if (!_wrapEnabled && _bounces)
                {
                    factor = 1.0 - MIN(fabs(_scrollOffset - [self clampedOffset:_scrollOffset]),
                                       _bounceDistance) / _bounceDistance;
                }
                
                _startVelocity = -velocity * factor * _scrollSpeed / _itemWidth;
                _scrollOffset -= (translation - _previousTranslation) * factor * _offsetMultiplier / _itemWidth;
                _previousTranslation = translation;
                [self didScroll];
                break;
            }
            case UIGestureRecognizerStatePossible:
            {
                //do nothing
                break;
            }
        }
    }
}

相关文章

网友评论

      本文标题:iCarousel源码解读

      本文链接:https://www.haomeiwen.com/subject/reissftx.html