封装了一个仿照抖音评论轮播效果的iOS轮播视图

效果图

原理

就是我们在一个视图里面有两个子视图,一个是currentView,

一个是willShowView,在一次动画过程中,我们改变current View的frame,同时改变willShowView的frame,同时,需要改变currentVIew 的transform.y不然的话,currentView里面的内容就没有缩放效果了,看起来就是单纯的展示不下的感觉,动画结束之后,将currentView指向willView, willView指向currentView, 同时,将刚刚消失的视图,放到底部,等待下次动画展示

#代码

//
//  RollingCell.m
//  TEXT
//
//  Created by 刘博 on 2021/3/18.
//  Copyright © 2021 刘博. All rights reserved.
//

#import "XBNoticeViewCell.h"
#import "XBRollingNoticeView.h"

@interface XBRollingNoticeView ()

@property (nonatomic, strong) NSMutableDictionary *cellClsDict; //注册 cell 的字典,key为cell的类名,value 为identifier
@property (nonatomic, strong) NSMutableArray *reuseCells; //重用cell的实例对象数组
@property (nonatomic, strong) NSTimer *timer; //计时器
@property (nonatomic, strong) XBNoticeViewCell *currentCell; //当前展示的cell
@property (nonatomic, strong) XBNoticeViewCell *willShowCell; //即将展示的cell
@property (nonatomic, assign) BOOL isAnimating; //动画
@property (nonatomic, assign) BOOL isRefresing ; ///在刷新, 多次刷新的时候,防止上次未执行完的动画对新的一轮刷新造成干扰
///
@property (nonatomic, strong) NSMutableArray *array ;

@end

@implementation XBRollingNoticeView


- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupNoticeViews];
    }
    return self;
}

- (void)setupNoticeViews
{
    self.clipsToBounds = YES;
    _stayInterval = 2;
    _animationDuration = 0.66;
    _fadeTranslationY = 6;
    [self addGestureRecognizer:[self createTapGesture]];
}

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

- (__kindof XBNoticeViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
    for (XBNoticeViewCell *cell in self.reuseCells)
    {
        if ([cell.reuseIdentifier isEqualToString:identifier]) {
            cell.userInteractionEnabled = NO;
            return cell;
        }
    }
    
    Class cellCls = NSClassFromString(self.cellClsDict[identifier]);
    XBNoticeViewCell *cell = [[cellCls alloc] initWithReuseIdentifier:identifier];
    cell.userInteractionEnabled = NO;
    return cell;
}

#pragma mark- rolling
- (void)layoutCurrentCellAndWillShowCell
{
    int count = (int)[self.dataSource numberOfRowsForRollingNoticeView:self];
    if (_currentIndex > count - 1) {
        _currentIndex = 0;
    }
    int willShowIndex = _currentIndex + 1;
    if (willShowIndex > count - 1) {
        willShowIndex = 0;
    }
    
    float w = self.frame.size.width;
    float h = self.frame.size.height;
    if (!_currentCell) {
        // 第一次没有currentcell
        // currentcell is null at first time
        _currentCell = [self.dataSource rollingNoticeView:self cellAtIndex:_currentIndex];
        _currentCell.frame = CGRectMake(0, 0, w, h);
        if (![self.subviews containsObject:self.currentCell]) {
            [self addSubview:_currentCell];
        }
        if (self.style == RollingStyleDefault) {
            ///默认轮播滚动样式,首次展示不需要加载下一个
            return;
        }
    }
    CGFloat willY = h + self.spaceOfItem;
    if (self.style == RollingStyleFade) {
        //淡入淡出的样式
        willY = 4;
    } else if (self.style == RollingStyleScaleY) {
        willY = h + self.spaceOfItem;
    }
    _willShowCell = [self.dataSource rollingNoticeView:self cellAtIndex:willShowIndex];
    
    _willShowCell.frame = CGRectMake(0, willY, w, h);
    if (self.style == RollingStyleFade) {
        ///首次展示currentCell的时候,will 需要隐藏
        _willShowCell.alpha = 0;
    }
    if (![self.subviews containsObject:_willShowCell]) {
        [self addSubview:_willShowCell];
    }

    self.isRefresing = YES;
    [self.reuseCells removeObject:_currentCell];
    [self.reuseCells removeObject:_willShowCell];
}

- (void)reloadDataAndStartRoll
{
    [self stopTimer];
    
    [self layoutCurrentCellAndWillShowCell];
    NSInteger count = [self.dataSource numberOfRowsForRollingNoticeView:self];
    if (count && count < 2) {
        return;
    }
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer timerWithTimeInterval:self.stayInterval + self.animationDuration repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerHandle];
    }];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)stopTimer
{
    if (_timer) {
        [_timer invalidate];
        _timer = nil;
    }
    _isAnimating = NO;
    _currentIndex = 0;
    [_currentCell removeFromSuperview];
    [_willShowCell removeFromSuperview];
    _currentCell = nil;
    _willShowCell = nil;
    [self.reuseCells removeAllObjects];
}

- (void)pause
{
    if (_timer) {
        [_timer setFireDate:[NSDate distantFuture]];
    }
}

- (void)proceed
{
    if (_timer) {
        [_timer setFireDate:[NSDate date]];
    }
}

- (void)timerHandle
{
    if (self.isAnimating) {
        return;
    }
    if (self.style == RollingStyleDefault) {
        [self defaultTimeHandler];
    } else if (self.style == RollingStyleFade) {
        [self fadeTimeHandler];
    } else if (self.style == RollingStyleScaleY) {
        [self scaleYTimeHandler];
    }
}
    
- (void)defaultTimeHandler
{
    [self layoutCurrentCellAndWillShowCell];
    _currentIndex++;
    int count = (int)[self.dataSource numberOfRowsForRollingNoticeView:self];
    if (_currentIndex > count - 1) {
        _currentIndex = 0;
    }
    float w = self.frame.size.width;
    float h = self.frame.size.height;
    
    self.isAnimating = YES;
    
    [UIView animateWithDuration:_animationDuration animations:^{
        self.currentCell.frame = CGRectMake(0, - h - self.spaceOfItem, w, h);
        self.willShowCell.frame = CGRectMake(0, 0, w, h);
    } completion:^(BOOL finished) {
        // fixed bug: reload data when animate running
        if (self.currentCell && self.willShowCell) {
            [self.reuseCells addObject:self.currentCell];
            [self.currentCell removeFromSuperview];
            self.currentCell = self.willShowCell;
        }
        self.isAnimating = NO;
    }];
}
    
- (void)fadeTimeHandler
{
    self.isRefresing = NO;
    self.isAnimating = YES;
    float w = self.frame.size.width;
    float h = self.frame.size.height;
    
    int count = (int)[self.dataSource numberOfRowsForRollingNoticeView:self];
    int willShowIndex = self->_currentIndex + 1;
    if (willShowIndex > count - 1) {
        willShowIndex = 0;
    }
    self->_willShowCell = [self.dataSource rollingNoticeView:self cellAtIndex:willShowIndex];
    self->_willShowCell.frame = CGRectMake(0, self.fadeTranslationY, w, h);
    self->_willShowCell.alpha = 0;
    [self addSubview:self.willShowCell];
    [self.reuseCells removeObject:self.willShowCell];
    [self.reuseCells removeObject:self.currentCell];
    ///动画隐藏当前的cell
    [UIView animateWithDuration:self.animationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
        if (self.isRefresing) {
            self.currentCell.alpha = 1;
        } else {
            self.currentCell.alpha = 0;
        }
    } completion:^(BOOL finished) {
    }];
    [UIView animateWithDuration:self.animationDuration - 0.1 delay:0.1 options:UIViewAnimationOptionCurveLinear animations:^{
        if (self.isRefresing) {
            self.currentCell.frame = CGRectMake(0, 0, w, h);
        } else {
            self.currentCell.frame = CGRectMake(0, - self.fadeTranslationY, w, h);
            self.currentCell.alpha = 0;
        }
    } completion:^(BOOL finished) {
    }];
    ///动画展示下一个cell ,
    /*
     这里减0.07是需要在上面文案的动画还没有结束的时候,
     下面文案的动画就要开始了
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((self.animationDuration - 0.07) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self showNext];
    });
}

- (void)showNext
{
    [UIView animateWithDuration:self.animationDuration animations:^{
        if (self.isRefresing) {
            self.willShowCell.alpha = 0;
        } else {
            self.willShowCell.alpha = 1;
        }
    } completion:^(BOOL finished) {
    }] ;
    float w = self.frame.size.width;
    float h = self.frame.size.height;
    
    [UIView animateWithDuration:self.animationDuration - 0.1 delay:0.1 options:UIViewAnimationOptionCurveLinear animations:^{
        if (self.isRefresing) {
            self.willShowCell.frame = CGRectMake(0, self.fadeTranslationY, w, h);
        } else {
            self.willShowCell.frame = CGRectMake(0, 0, w, h);
        }
    } completion:^(BOOL finished) {
        if (self.isRefresing) {
            return;
        }
        self->_currentIndex++;
        int count = (int)[self.dataSource numberOfRowsForRollingNoticeView:self];
        if (self->_currentIndex > count - 1) {
            self->_currentIndex = 0;
        }
        if (self.currentCell && self.willShowCell) {
            [self.reuseCells addObject:self.currentCell];
        }
        self.currentCell = self.willShowCell;
        self.isAnimating = NO;
    }];
}

- (void)scaleYTimeHandler
{
    NSInteger count = [self.dataSource numberOfRowsForRollingNoticeView:self];
    float w = self.frame.size.width;
    float h = self.frame.size.height;
    [UIView animateWithDuration:self.animationDuration animations:^{
        self.currentCell.frame = CGRectMake(0, 0, w, 0);
        self.currentCell.transform = CGAffineTransformMakeScale(1, 0.01);
        self.willShowCell.frame = CGRectMake(0, 0, w, h);
    } completion:^(BOOL finished) {
        self.currentCell.frame = CGRectMake(0, w + self.spaceOfItem, w, h);
        self.currentCell.transform = CGAffineTransformMakeScale(1, 1);
        if (self.willShowCell && self.currentCell) {
            [self.reuseCells addObject:self.currentCell];
        }
        self.currentCell = self.willShowCell;
        self->_currentIndex += 1;
        if (self.currentIndex >= count) {
            self->_currentIndex = 0;
        }
        NSInteger willIndex = self.currentIndex + 1;
        if (willIndex >= count) {
            willIndex = 0;
        }
        self.willShowCell = [self.dataSource rollingNoticeView:self cellAtIndex:willIndex];
        self.willShowCell.frame = CGRectMake(0, w + self.spaceOfItem, w, h);
        [self addSubview:self.willShowCell];
        [self.reuseCells removeObject:self.willShowCell];
    }];
}

#pragma mark - gesture

- (void)handleCellTapAction
{
    int count = (int)[self.dataSource numberOfRowsForRollingNoticeView:self];
    if (_currentIndex > count - 1) {
        _currentIndex = 0;
    }
    if ([self.delegate respondsToSelector:@selector(didClickRollingNoticeView:forIndex:)]) {
        [self.delegate didClickRollingNoticeView:self forIndex:_currentIndex];
    }
}

- (UITapGestureRecognizer *)createTapGesture
{
    return [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleCellTapAction)];
}

#pragma mark- lazy
- (NSMutableDictionary *)cellClsDict
{
    if (!_cellClsDict) {
        _cellClsDict = [[NSMutableDictionary alloc]init];
    }
    return _cellClsDict;
}

- (NSMutableArray *)reuseCells
{
    if (!_reuseCells) {
        _reuseCells = [[NSMutableArray alloc]init];
    }
    return _reuseCells;
}

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

- (NSMutableArray *)array
{
    if (!_array) {
        _array = [NSMutableArray array];
    }
    return _array;
}

@end

如果对您有帮助,欢迎给个star
demo

相关推荐
502胶水2051 小时前
腾讯地图异步调用
开发语言·ios·swift
刘小哈哈哈4 小时前
iOS UITableView自带滑动手势和父视图添加滑动手势冲突响应机制探索
macos·ios·cocoa
分享者花花4 小时前
恢复出厂设置后如何从 iPhone 恢复数据
windows·macos·ios·智能手机·excel·cocoa·iphone
1024小神4 小时前
SwiftUI中List的liststyle样式及使用详解添加、移动、删除、自定义滑动
ios·swiftui·swift
Lik10246 小时前
ReactNative如何实现沉浸式状态栏及渐变色Header【兼容Android和iOS】
android·react native·ios
Geeker556 小时前
适用于 Windows的 5 个最佳 PDF 转 Word 转换器
ios·智能手机·pdf·电脑·word·手机·iphone
勤劳兔码农7 小时前
iOS开发新手教程:Swift语言与Xcode工具链
ios·xcode·swift
<花开花落>16 小时前
iOS App 测试环境升级,遇到的问题以及解决方法
macos·ios·appium
cpluser16 小时前
在 VS Code 中自动化 Xcode 项目编译和调试
macos·ios·自动化·apple vision pro·xcode
鹿屿二向箔16 小时前
iOS开发-Xcode
macos·ios·xcode