Encapsulates an iOS carousel view with an intermediate zoom effect

renderings

Please add image description

Calculation logic

Set the size displayed in the middle, that is, the normal size, and then set the margin in the horizontal and vertical directions. When the origin of the view is equal to the contentoffset of the scrollView, that is, when the view is in the middle, the maximum value is reached, and then by calculating
other The distance between the origin of the view and scrollView.contentoffset sets the size. When the distance from the frame at the position of contentoffset is one view width, the minimum size is reached, that is, the maximum size minus margin. Then calculate the size that needs to be reduced by calculating the absolute value of the difference between the origin and contentffset of the view, that is, the ratio of the distance to the width of the view.

core code

//
//  LBHorizontalLoopView.m
//  LBHorizontalLoopView
//
//  Created by liubo on 2021/8/29.
//

#import "LBMiddleExpandLoopView.h"

@interface LBMiddleExpandLoopView ()<UIScrollViewDelegate>

@property (nonatomic, strong, readwrite) LBMiddleExpandLoopViewBaseCell *currentCell;

@property (nonatomic, assign, readwrite) NSInteger currentSelectIndex;

// 实际的个数
@property (nonatomic, assign) NSInteger realCount;

// 显示的个数
@property (nonatomic, assign) NSInteger showCount;

// 定时器
@property (nonatomic, weak) NSTimer     *timer;
@property (nonatomic, assign) NSInteger timerIndex;

// 当前显示的cell大小
@property (nonatomic, assign) CGSize cellSize;

@property (nonatomic, strong) NSMutableDictionary *viewClsDict;
@property (nonatomic, strong) NSMutableArray *visibleCells;
@property (nonatomic, strong) NSMutableArray *reusableCells;
@property (nonatomic, assign) NSRange        visibleRange;

// 处理xib加载时导致的尺寸不准确问题
@property (nonatomic, assign) CGSize        originSize;

@end

@implementation LBMiddleExpandLoopView

#pragma mark - Life Cycle
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self initialization];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        [self initialization];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    if (CGSizeEqualToSize(self.originSize, CGSizeZero)) return;
    
    // 解决xib加载时导致的布局错误问题
    if (!CGSizeEqualToSize(self.bounds.size, self.originSize)) {
        [self updateScrollViewAndCellSize];
    }
}

// 解决当父视图释放时,当前视图因为NSTimer强引用而导致的不能释放
- (void)willMoveToSuperview:(UIView *)newSuperview {
    if (!newSuperview) {
        [self stopTimer];
    }
}

- (void)dealloc {
    [self stopTimer];
    self.scrollView.delegate = nil;
}

// 重新此方法是为了解决当cell超出UIScrollView时不能点击的问题
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) {
        // 判断点击的点是否在cell上
        for (UIView *cell in self.scrollView.subviews) {
            // 将cell的frame转换到当前视图上
            CGRect convertFrame = CGRectZero;
            convertFrame.size = cell.frame.size;
            
            convertFrame.origin.x = cell.frame.origin.x + self.scrollView.frame.origin.x - self.scrollView.contentOffset.x;
            convertFrame.origin.y = self.scrollView.frame.origin.y + cell.frame.origin.y;
            
            // 判断点击的点是否在cell上
            if (CGRectContainsPoint(convertFrame, point)) {
                // 修复cell上添加其他点击事件无效的bug
                UIView *view = [super hitTest:point withEvent:event];
                if (view == self || view == cell || view == self.scrollView) return cell;
                return view;
            }
        }
        // 判断点击的点是否在UIScrollView上
        CGPoint newPoint = CGPointZero;
        newPoint.x = point.x - self.scrollView.frame.origin.x + self.scrollView.contentOffset.x;
        newPoint.y = point.y - self.scrollView.frame.origin.y + self.scrollView.contentOffset.y;
        if ([self.scrollView pointInside:newPoint withEvent:event]) {
            return [self.scrollView hitTest:newPoint withEvent:event];
        }
        // 系统处理
        return [super hitTest:point withEvent:event];
    }
    return nil;
}

#pragma mark - Public Methods
- (void)reloadData {
    // 移除所有cell
    [self.scrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    
    // 停止定时器
    [self stopTimer];
    
    // 加载数据
    if (self.dataSource && [self.dataSource respondsToSelector:@selector(numberOfCellsInCycleScrollView:)]) {
        // 实际个数
        self.realCount = [self.dataSource numberOfCellsInCycleScrollView:self];
        
        // 展示个数
        if (self.isInfiniteLoop) {
            self.showCount = self.realCount == 1 ? 1 : self.realCount * 3;
        }else {
            self.showCount = self.realCount;
        }
    }
    
    // 清除原始数据
    [self.visibleCells removeAllObjects];
    [self.reusableCells removeAllObjects];
    self.visibleRange = NSMakeRange(0, 0);
    
    //cell数量为1或defaultSelectIndex超过了当前数量
    if(self.defaultSelectIndex >= self.realCount) {
        self.defaultSelectIndex = 0;
    }
    if(self.realCount == 1) {
        self.timerIndex = 0;
    }
    
    for (NSInteger i = 0; i < self.showCount; i++){
        [self.visibleCells addObject:[NSNull null]];
    }
    
    __weak __typeof(self) weakSelf = self;
    [self refreshSizeCompletion:^{
        [weakSelf initialScrollViewAndCellSize];
    }];
}

- (void)refreshSizeCompletion:(void(^)(void))completion {
    if (self.bounds.size.width == 0 || self.bounds.size.height == 0) {
        [self layoutIfNeeded];
        // 此处做延时处理是为了解决使用Masonry布局时导致的view的大小不能及时更新的bug
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            if (self.bounds.size.width == 0 || self.bounds.size.height == 0) {
                [self refreshSizeCompletion:completion];
            }else {
                !completion ? : completion();
            }
        });
    }else {
        !completion ? : completion();
    }
}


- (void)registerClass:(nonnull Class)cellClass forViewReuseIdentifier:(NSString *)identifier
{
    [self.viewClsDict setObject:NSStringFromClass(cellClass) forKey:identifier];
}

- (__kindof LBMiddleExpandLoopViewBaseCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
    
    LBMiddleExpandLoopViewBaseCell *cell;
    for (LBMiddleExpandLoopViewBaseCell *cellReusable in self.reusableCells)
    {
        if ([cellReusable.reuseIdentifier isEqualToString:identifier]) {
            cell = cellReusable;
        }
    }
    if (!cell) {
        Class cellCls = NSClassFromString(self.viewClsDict[identifier]);
        cell = [[cellCls alloc] initWithReuseIdentifier:identifier];
        cell.userInteractionEnabled = NO;
    } else {
        [self.reusableCells removeObject:cell];
    }
    return cell;
}


- (void)scrollToCellAtIndex:(NSInteger)index animated:(BOOL)animated {
    if (index < self.realCount) {
        [self stopTimer];
        
        if (self.isInfiniteLoop) {
            self.timerIndex = self.realCount + index;
            [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(startTimer) object:nil];
            [self performSelector:@selector(startTimer) withObject:nil afterDelay:0.5];
        } else {
            self.timerIndex = index;
        }
        
        [self.scrollView setContentOffset:CGPointMake(self.cellSize.width * self.timerIndex, 0) animated:animated];
           
        [self setupCellsWithContentOffset:self.scrollView.contentOffset];
        [self updateVisibleCellAppearance];
    }
}

- (void)adjustCurrentCell {
    if (self.isAutoScroll && self.realCount > 0) {
        self.scrollView.contentOffset = CGPointMake(self.cellSize.width * self.timerIndex, 0);
    }
}

- (void)startTimer {
    if (self.realCount > 1 && self.isAutoScroll) {
        [self stopTimer];
        
//        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:self.autoScrollTime target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
//        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
      //  self.timer = timer;
    }
}

- (void)stopTimer {
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

#pragma mark Private Methods
- (void)initialization {
    // 初始化默认数据
    self.clipsToBounds      = YES;
    self.isAutoScroll       = YES;
    self.isInfiniteLoop     = YES;
    self.minimumCellAlpha   = 1.0f;
    self.autoScrollTime     = 3.0f;
    
    // 添加scrollView
    [self addSubview:self.scrollView];
}

- (void)initialScrollViewAndCellSize {
    self.originSize = self.bounds.size;
    [self updateScrollViewAndCellSize];
    
    // 默认选中
    if (self.defaultSelectIndex >= 0 && self.defaultSelectIndex < self.realCount) {
        [self handleCellScrollWithIndex:self.defaultSelectIndex];
    }
}

- (void)updateScrollViewAndCellSize {
    if (self.bounds.size.width <= 0 || self.bounds.size.height <= 0) return;
    
    // 设置cell尺寸
    self.cellSize = CGSizeMake(self.bounds.size.width - 2 * self.leftRightMargin, self.bounds.size.height);
    if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForCellInCycleScrollView:)]) {
        self.cellSize = [self.delegate sizeForCellInCycleScrollView:self];
    }
    
    // 设置scrollView大小
    self.scrollView.frame = CGRectMake(0, 0, self.cellSize.width, self.cellSize.height);
    self.scrollView.contentSize = CGSizeMake(self.cellSize.width * self.showCount,0);
    self.scrollView.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
    
    if (self.realCount > 1) {
        CGPoint offset = CGPointZero;
        
        if (self.isInfiniteLoop) { // 开启无限轮播
            // 滚动到第二组
            offset = CGPointMake(self.cellSize.width * (self.realCount + self.defaultSelectIndex), 0);
            self.timerIndex = self.realCount + self.defaultSelectIndex;
        }else {
            offset = CGPointMake(self.cellSize.width * self.defaultSelectIndex, 0);
            self.timerIndex = self.defaultSelectIndex;
        }
        
        [self.scrollView setContentOffset:offset animated:NO];
        
        // 自动轮播
        if (self.isAutoScroll) {
            [self startTimer];
        }
    }
    
    // 根据当前scrollView的offset设置显示的cell
    [self setupCellsWithContentOffset:self.scrollView.contentOffset];
    
    // 更新可见cell的显示
    [self updateVisibleCellAppearance];
}

- (void)setupCellsWithContentOffset:(CGPoint)offset {
    if (self.showCount == 0) return;
    if (self.cellSize.width <= 0 || self.cellSize.height <= 0) return;
    //计算_visibleRange
    CGFloat originX = self.scrollView.frame.origin.x == 0 ? 0.01 : self.scrollView.frame.origin.x;
    CGFloat originY = self.scrollView.frame.origin.y == 0 ? 0.01 : self.scrollView.frame.origin.y;
    
    CGPoint startPoint = CGPointMake(offset.x - originX, offset.y - originY);
    
    CGPoint endPoint = CGPointMake(offset.x + self.scrollView.frame.size.width + originX, offset.y + self.scrollView.frame.size.height + originY);
    
    NSInteger startIndex = 0;
    for (NSInteger i = 0; i < self.visibleCells.count; i++) {
        if (self.cellSize.width * (i + 1) > startPoint.x) {
            startIndex = i;
            break;
        }
    }
    
    NSInteger endIndex = startIndex;
    for (NSInteger i = self.visibleCells.count - 1; i >= startIndex; i --) {
        if (self.cellSize.width * i < endPoint.x) {
            endIndex = i;
            break;
        }
    }
    
    // 可见页分别向前向后扩展一个,提高效率
    startIndex = MAX(startIndex, 0);
    endIndex = MIN(endIndex, self.visibleCells.count - 1);
    self.visibleRange = NSMakeRange(startIndex, endIndex - startIndex + 1);
    
    for (NSInteger i = startIndex; i <= endIndex; i++) {
        [self addCellAtIndex:i];
    }
    
    for (NSInteger i = 0; i < startIndex; i ++) {
        [self removeCellAtIndex:i];
    }
    
    for (NSInteger i = endIndex + 1; i < self.visibleCells.count; i ++) {
        [self removeCellAtIndex:i];
    }
}

- (void)updateVisibleCellAppearance {
    if (self.showCount == 0) return;
    if (self.cellSize.width <= 0 || self.cellSize.height <= 0) return;
    
    CGFloat offsetX = self.scrollView.contentOffset.x;
    
    for (NSInteger i = self.visibleRange.location; i < NSMaxRange(self.visibleRange); i++) {
        LBMiddleExpandLoopViewBaseCell *cell = self.visibleCells[i];
        CGFloat originX = cell.frame.origin.x;
        CGFloat delta = fabs(originX - offsetX);
        CGRect originCellFrame = (CGRect){
   
   {self.cellSize.width * i, 0}, self.cellSize};
        
        CGFloat leftRightInset = 0;
        CGFloat topBottomInset = 0;
        CGFloat alpha = 0;
        
        if (delta < self.cellSize.width) {
            alpha = (delta / self.cellSize.width) * self.minimumCellAlpha;
            CGFloat adjustLeftRightMargin = self.leftRightMargin == 0 ? 0 : self.leftRightMargin + 1;
            CGFloat adjustTopBottomMargin = self.topBottomMargin == 0 ? 0 : self.topBottomMargin;
            
            leftRightInset = adjustLeftRightMargin * delta / self.cellSize.width;
            topBottomInset = adjustTopBottomMargin * delta / self.cellSize.width;
            
            NSInteger index = self.realCount == 0 ? 0 : i % self.realCount;
            if (index == self.currentSelectIndex) {
                [self.scrollView bringSubviewToFront:cell];
            }
        } else {
            alpha = self.minimumCellAlpha;
            
            leftRightInset = self.leftRightMargin;
            topBottomInset = self.topBottomMargin;
            
            [self.scrollView sendSubviewToBack:cell];
        }
        
        if (self.leftRightMargin == 0 && self.topBottomMargin == 0) {
            cell.frame = originCellFrame;
        }else {
            CGFloat scaleX = (self.cellSize.width - leftRightInset * 2) / self.cellSize.width;
            CGFloat scaleY = (self.cellSize.height - topBottomInset * 2) / self.cellSize.height;
            UIEdgeInsets insets = UIEdgeInsetsMake(topBottomInset, leftRightInset - 0.1, topBottomInset, leftRightInset);
            
            cell.layer.transform = CATransform3DMakeScale(scaleX, scaleY, 1.0);
            cell.frame = UIEdgeInsetsInsetRect(originCellFrame, insets);
        }
        
        // 获取当前cell
        if (cell.tag == self.currentSelectIndex) {
            self.currentCell = cell;
        }
    }
}

- (void)addCellAtIndex:(NSInteger)index {
    NSParameterAssert(index >= 0 && index < self.visibleCells.count);
    
    LBMiddleExpandLoopViewBaseCell *cell = self.visibleCells[index];
    if ((NSObject *)cell == [NSNull null]) {
        cell = [self.dataSource cycleScrollView:self cellForViewAtIndex:index % self.realCount];
        if (cell) {
            [self.visibleCells replaceObjectAtIndex:index withObject:cell];
            
            cell.tag = index % self.realCount;
            
            __weak __typeof(self) weakSelf = self;
            cell.didCellClick = ^(NSInteger index) {
                [weakSelf handleCellSelectWithIndex:index];
            };
            cell.frame = CGRectMake(self.cellSize.width * index, 0, self.cellSize.width, self.cellSize.height);
            if (!cell.superview) {
                [self.scrollView addSubview:cell];
            }
        }
    }
}

- (void)removeCellAtIndex:(NSInteger)index{
    LBMiddleExpandLoopViewBaseCell *cell = [self.visibleCells objectAtIndex:index];
    if ((NSObject *)cell == [NSNull null]) return;
    
    [self.reusableCells addObject:cell];
    
    if (cell.superview) {
        [cell removeFromSuperview];
    }
    
    [self.visibleCells replaceObjectAtIndex:index withObject:[NSNull null]];
}

- (void)handleCellSelectWithIndex:(NSInteger)index {
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:didSelectCellAtIndex:)]) {
        [self.delegate cycleScrollView:self didSelectCellAtIndex:index];
    }
}

- (void)handleCellScrollWithIndex:(NSInteger)index {
    self.currentSelectIndex = index;
    // 获取当前cell
    for (NSInteger i = self.visibleRange.location; i < NSMaxRange(self.visibleRange); i++) {
        LBMiddleExpandLoopViewBaseCell *cell = self.visibleCells[i];
        if (cell.tag == index) {
            self.currentCell = cell;
        }
    }
    
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:didScrollCellToIndex:)]) {
        [self.delegate cycleScrollView:self didScrollCellToIndex:index];
    }
}

- (void)timerUpdate {
    self.timerIndex++;
    
    // bug fixed:解决反向滑动停止后,可能出现的自动滚动错乱问题
    if (self.timerIndex > self.realCount * 2) {
        self.timerIndex = self.realCount * 2;
    }
    
    if (!self.isInfiniteLoop) {
        if (self.timerIndex >= self.realCount) {
            self.timerIndex = 0;
        }
    }
    
    [self.scrollView setContentOffset:CGPointMake(self.cellSize.width * self.timerIndex, 0) animated:YES];
}

#pragma mark UIScrollView Delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (self.realCount == 0) return;
    if (self.cellSize.width <= 0 || self.cellSize.height <= 0) return;
    
    NSInteger index = 0;
    index = (NSInteger)round(self.scrollView.contentOffset.x / self.cellSize.width) % self.realCount;
    
    if (self.isInfiniteLoop) {
        if (self.realCount > 1) {
            CGFloat horIndex = scrollView.contentOffset.x / self.cellSize.width;
            if (horIndex >= 2 * self.realCount) {
                scrollView.contentOffset = CGPointMake(self.cellSize.width * self.realCount, 0);
                self.timerIndex = self.realCount;
            }
            
            if (horIndex <= (self.realCount - 1)) {
                scrollView.contentOffset = CGPointMake(self.cellSize.width * (2 * self.realCount - 1), 0);
                self.timerIndex = 2 * self.realCount - 1;
            }
        }
        }else {
            index = 0;
        }
    
    [self setupCellsWithContentOffset:scrollView.contentOffset];
    [self updateVisibleCellAppearance];
    
    if (index >= 0 && self.currentSelectIndex != index) {
        [self handleCellScrollWithIndex:index];
    }
    [self handleScrollViewDidScroll:scrollView];
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    [self stopTimer];
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:willBeginDragging:)]) {
        [self.delegate cycleScrollView:self willBeginDragging:scrollView];
    }
}

// 结束拖拽时调用,decelerate是否有减速
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        [self startTimer];
    }
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:didEndDragging:willDecelerate:)]) {
        [self.delegate cycleScrollView:self didEndDragging:scrollView willDecelerate:decelerate];
    }
}

// 结束减速是调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self startTimer];
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:didEndDecelerating:)]) {
        [self.delegate cycleScrollView:self didEndDecelerating:scrollView];
    }
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    if (self.realCount > 1 && self.isAutoScroll) {
        NSInteger index = round(targetContentOffset->x / self.cellSize.width);
        self.timerIndex = index;
    }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
    [self updateVisibleCellAppearance];
    
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:didEndScrollingAnimation:)]) {
        [self.delegate cycleScrollView:self didEndScrollingAnimation:scrollView];
    }
}

- (void)handleScrollViewDidScroll:(UIScrollView *)scrollView {
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:didScroll:)]) {
        [self.delegate cycleScrollView:self didScroll:scrollView];
    }
    
    if ([self.delegate respondsToSelector:@selector(cycleScrollView:scrollingFromIndex:toIndex:ratio:)]) {
        BOOL isFirstRevirse = NO; // 是否在第一个位置反向滑动
        CGFloat ratio = 0;   // 滑动百分比
        CGFloat offsetX = scrollView.contentOffset.x;
        CGFloat maxW = self.realCount * scrollView.bounds.size.width;
        
        CGFloat changeOffsetX = self.isInfiniteLoop ? (offsetX - maxW) : offsetX;
        if (changeOffsetX < 0) {
            changeOffsetX = -changeOffsetX;
            isFirstRevirse = YES;
        }
        ratio = (changeOffsetX / scrollView.bounds.size.width);
        if (ratio > self.realCount || ratio < 0) return; // 越界,不作处理
        ratio = MAX(0, MIN(self.realCount, ratio));
        NSInteger baseIndex = floor(ratio);
        if (baseIndex + 1 > self.realCount) {
            // 右边越界了
            baseIndex = 0;
        }
        CGFloat remainderRatio = ratio - baseIndex;
        if (remainderRatio <= 0 || remainderRatio >= 1) return;
        NSInteger toIndex = 0;
        if (isFirstRevirse) {
            baseIndex = self.realCount - 1;
            toIndex = 0;
            remainderRatio = 1 - remainderRatio;
        }else if (baseIndex == self.realCount - 1) {
            toIndex = 0;
        }else {
            toIndex = baseIndex + 1;
        }
        [self.delegate cycleScrollView:self scrollingFromIndex:baseIndex toIndex:toIndex ratio:remainderRatio];
    }
}

#pragma mark - 懒加载
- (UIScrollView *)scrollView {
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
        _scrollView.scrollsToTop = NO;
        _scrollView.delegate = self;
        _scrollView.pagingEnabled = YES;
        _scrollView.clipsToBounds = NO;
        _scrollView.showsHorizontalScrollIndicator = NO;
        _scrollView.showsVerticalScrollIndicator = NO;
    }
    return _scrollView;
}

- (NSMutableArray *)visibleCells {
    if (!_visibleCells) {
        _visibleCells = [NSMutableArray new];
    }
    return _visibleCells;
}

- (NSMutableDictionary *)viewClsDict
{
    if (!_viewClsDict) {
        _viewClsDict = [NSMutableDictionary dictionary];
    }
    return _viewClsDict;
}

- (NSMutableArray *)reusableCells {
    if (!_reusableCells) {
        _reusableCells = [NSMutableArray new];
    }
    return _reusableCells;
}

@end

Guess you like

Origin blog.csdn.net/LIUXIAOXIAOBO/article/details/133215774