美文网首页
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