封装了一个iOS 分页嵌套滚动框架

嵌套滚动的原理

有一个smoothView,(继承于UIView),里面包含一个横向滚动的collectionView

1 有一个头部容器视图,头部容器视图包含头部视图和 分页菜单

总体是一个横向的collectionView , 有多少个分页,就有多少个cell

这个 cell的大小就是整个页面的大小,cell 上面放了一个控制器的view,控制器的view上面放了单个的纵向滚动的视图,

则初始化的状态下,纵向滚动的列表视图的contentInset.top就是头部

容器视图的高度,然后头部容器视图添加到当前展示的单个纵向列表上,这样当在没有达到临界偏移量的时候,当纵向列表想上滚动的时候,头部容器视图就会跟随纵向列表滚动。

2 当纵向列表滚动到临界偏移量的时候,头部容器视图就会被添加到

smoothView上,这个样就大大大到了悬浮的效果,

3 还有,如果我们在纵向上没有达到临界的偏移量,则如果这个时候

我们横向滚动分页视图,则需要将头部容器视图添加到self (smoothView)上面 (在scrollViewdidscroll方法里面),也是大到一个悬浮的效果,否则的话,头部视图就会跟随当前的列表视图划走

4 还有一个就是当我们滚动当前纵向列表的时候,

(在每没有悬浮的情况下) 要将其他的列表的偏移量和设置成和当前列表的偏移量相同,因为为了达到切换页面的时候,头部的位置一样

5 位了支持侧滑返回,需要自定义横向的collectionView ,

并重写手势的代理方法

复制代码
//是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥.
//是否允许多个手势识别器共同识别,一个控件的手势识别后是否阻断手势识别继续向下传播,默认返回NO;如果为YES,响应者链上层对象触发手势识别后,如果下层对象也添加了手势并成功识别也会继续执行,否则上层对象识别后则不再继续传播
//一句话总结就是此方法返回YES时,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
   printf("哈哈哈哈同shouldRecognizeSimultaneouslyWithGestureRecognizer\n");
   if ([self isPanBackAction:gestureRecognizer]) {
       return YES;
   }
   return NO;
   
}

/// 判断是否是全屏的返回手势
- (BOOL)isPanBackAction:(UIGestureRecognizer *)gestureRecognizer {
   
   // 在最左边时候 && 是pan手势 && 手势往右拖
   if (self.contentOffset.x == 0) {
       if (gestureRecognizer == self.panGestureRecognizer) {
           // 根据速度获取拖动方向
           CGPoint velocity = [self.panGestureRecognizer velocityInView:self.panGestureRecognizer.view];
           if(velocity.x>0){
               //手势向右滑动
               return YES;
           }
       }
       
   }
   return NO;
}

// 如果是全屏的左滑返回,那么ScrollView的左滑就没用了,返回NO,让ScrollView的左滑失效
// 不写此方法的话,左滑时,那个ScrollView上的子视图也会跟着左滑的
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
   printf("哈哈哈哈gestureRecognizerShouldBegin\n");
   if ([self isPanBackAction:gestureRecognizer]) {
       return NO;
   }
   return YES;

}

6 还有一个头部不能水平滑动

因为在没有悬浮的时候,我们的头部容器视图是放在纵向的列表上的

其实也是放在水平的collecitonView的一个ce ll 上面,

但是这个时候,我们从视觉上看它是头部,当我们手势在头部的时候当然是不能支持水平滚动的,所以要重写collectionView的 手势代理方法shouldReceiveTouch,这个时候不响应水平滚动手势

  • (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint point = [touch locationInView:self.headerContainerView];
    ///这里是为了横向滑动头部的时候,不让滑动
    if (CGRectContainsPoint(self.headerContainerView.bounds, point)) {
    return NO;
    }
    return YES;
    }

总的代码

复制代码
//
//  LBPageSmoothView.h
//  TEXT
//
//  Created by mac on 2024/8/11.
//  Copyright © 2024 刘博. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, LBPageSmoothHoverType) {
    LBPageSmoothHoverTypeNone   = 0,    // 未悬停
    LBPageSmoothHoverTypeTop    = 1,    // 顶部悬停
};

@class LBPageSmoothView;

@protocol LBPageSmoothListViewDelegate <NSObject>

/// 返回listView,如果是vc就返回vc.view,如果是自定义view,就返回view本身
- (UIView *)listView;

/// 返回vc或view内部持有的UIScrollView或UITableView或UICollectionView
- (UIScrollView *)listScrollView;

@optional
- (void)listViewWillAppear;
- (void)listViewDidAppear;
- (void)listViewWillDisappear;
- (void)listViewDidDisappear;

/// 当contentSize改变且不足一屏时,是否重置scrollView的位置,默认YES
- (BOOL)listScrollViewShouldReset;

@end

@protocol LBPageSmoothViewDataSource <NSObject>

/// 返回页面的headerView视图
/// @param smoothView smoothView
- (UIView *)headerViewInSmoothView:(LBPageSmoothView *)smoothView;

/// 返回需要悬浮的分段视图
/// @param smoothView smoothView
- (UIView *)segmentedViewInSmoothView:(LBPageSmoothView *)smoothView;

/// 返回列表个数
/// @param smoothView smoothView
- (NSInteger)numberOfListsInSmoothView:(LBPageSmoothView *)smoothView;

/// 根据index初始化一个列表实例,列表需实现`GKPageSmoothListViewDelegate`代理
/// @param smoothView smoothView
/// @param index 列表索引
- (id<LBPageSmoothListViewDelegate>)smoothView:(LBPageSmoothView *)smoothView initListAtIndex:(NSInteger)index;

@end

@protocol LBPageSmoothViewDelegate <NSObject>

@optional
/// 列表容器滑动代理
/// @param smoothView smoothView
/// @param scrollView containerScrollView
- (void)smoothView:(LBPageSmoothView *)smoothView scrollViewDidScroll:(UIScrollView *)scrollView;

/// 当前列表滑动代理
/// @param smoothView smoothView
/// @param scrollView 当前的列表scrollView
/// @param contentOffset 转换后的contentOffset
- (void)smoothView:(LBPageSmoothView *)smoothView listScrollViewDidScroll:(UIScrollView *)scrollView contentOffset:(CGPoint)contentOffset;

@end

@interface LBPageSmoothView : UIView

/// 代理
@property (nonatomic, weak) id<LBPageSmoothViewDelegate> delegate;

// 当前已经加载过的可用的列表字典,key是index值,value是对应列表
@property (nonatomic, strong, readonly) NSDictionary <NSNumber *, id<LBPageSmoothListViewDelegate>> *listDict;
@property (nonatomic, strong, readonly) UICollectionView *listCollectionView;

/// 默认索引
@property (nonatomic, assign) NSInteger defaultSelectedIndex;

/// 当前索引
@property (nonatomic, assign, readonly) NSInteger currentIndex;

/// 当前列表
@property (nonatomic, weak, readonly) UIScrollView *currentListScrollView;

/// 吸顶临界高度(默认为0)
@property (nonatomic, assign) CGFloat ceilPointHeight;

/// 是否内部控制指示器的显示与隐藏(默认为NO)
@property (nonatomic, assign, getter=isControlVerticalIndicator) BOOL controlVerticalIndicator;

/// 是否撑起scrollView,默认NO
/// 如果设置为YES则当scrollView的contentSize不足时会修改scrollView的contentSize使其能够滑动到悬浮状态
@property (nonatomic, assign, getter=isHoldUpScrollView) BOOL holdUpScrollView;

/// smoothView悬停类型
@property (nonatomic, assign, readonly) LBPageSmoothHoverType hoverType;

/// header容器的高度
@property (nonatomic, assign, readonly) CGFloat headerContainerHeight;

- (instancetype)initWithDataSource:(id<LBPageSmoothViewDataSource>)dataSource NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;

/**
 刷新headerView,headerView高度改变时调用
 */
- (void)refreshHeaderView;

/**
 刷新segmentedView,segmentedView高度改变时调用
 */
- (void)refreshSegmentedView;

/**
 刷新数据,刷新后pageView才能显示出来
 注意:如果需要动态改变headerView的高度,请在refreshHeaderView后在调用reloadData方法
 */
- (void)reloadData;

/**
 滑动到原点,可用于在吸顶状态下,点击返回按钮,回到原始状态
 */
- (void)scrollToOriginalPoint;
- (void)scrollToOriginalPointAnimated:(BOOL)animated;

/**
 滑动到临界点,可用于当headerView较长情况下,直接跳到临界点状态
 */
- (void)scrollToCriticalPoint;
- (void)scrollToCriticalPointAnimated:(BOOL)animated;

- (void)horizontalScrollDidEndAtIndex:(NSInteger)index;
- (void)selectIndex:(NSInteger)index;

@end

NS_ASSUME_NONNULL_END

.m

复制代码
//
//  LBPageSmoothView.m
//  TEXT
//
//  Created by mac on 2024/8/11.
//  Copyright © 2024 刘博. All rights reserved.
//

#import "LBPageSmoothView.h"

static NSString *const LIVPageSmoothViewCellID = @"smoothViewCell";

@interface LBPageSmoothCollectionView : UICollectionView <UIGestureRecognizerDelegate>

@property (nonatomic, weak) UIView *headerContainerView;

@end

@implementation LBPageSmoothCollectionView

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint point = [touch locationInView:self.headerContainerView];
    ///这里是为了横向滑动头部的时候,不让滑动
    if (CGRectContainsPoint(self.headerContainerView.bounds, point)) {
        return NO;
    }
    return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([self isPanBackAction:gestureRecognizer]) {
        return YES;
    }
    return NO;
}

- (BOOL)isPanBackAction:(UIGestureRecognizer *)gestureRecognizer
{
    //在最左边的时候 && 是pan手势,&& 手势往右拖拽
    if (self.contentOffset.x == 0) {
        if (gestureRecognizer == self.panGestureRecognizer) {
            //根据速度判断拖动的方向
            CGPoint velocity = [self.panGestureRecognizer velocityInView:self.panGestureRecognizer.view];
            if (velocity.x > 0) {
                return YES;
            }
        }
    }
    return NO;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([self isPanBackAction:gestureRecognizer]) {
        return NO;
    }
    return YES;
}

@end

@interface LBPageSmoothView () <UICollectionViewDelegate, UICollectionViewDataSource>

@property (nonatomic, weak) id<LBPageSmoothViewDataSource> dataSource;
@property (nonatomic, strong) LBPageSmoothCollectionView  *listCollectionView;

/*
 这里面盛放的是列表容器,不是列表视图
 */
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, id<LBPageSmoothListViewDelegate>> *listDict;

/*
 这里面的视图是headerContainerView 的父视图(在没有悬浮的时候),
 其实没有这个的话,直接添加到竖直滚动的列表中也是可以的,
 只不过我们有了这个方便管理
 */
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, UIView *> *listHeaderDict;

@property (nonatomic, assign) NSInteger currentIndex;

@property (nonatomic, assign) LBPageSmoothHoverType hoverType;

@property (nonatomic, strong) UIView *headerContainerView;
@property (nonatomic, weak) UIView *headerView;
@property (nonatomic, weak) UIView *segmentedView;
@property (nonatomic, weak) UIScrollView *currentListScrollView;

/*
 syncListContentOffsetEnabled 是否要同步偏移量
 这个是当我们在头部没有悬浮的时候设置成YES,因为为了要切换
 tab的时候,各个列表的偏移量相同,需要我们将其他列表的偏移量
 设置成和当前列表的偏移量相同,当我们头部悬浮的时候,
 将其他列表的偏移量设置成恰好悬浮的偏移量即可,不需要持续设置
 所以在悬浮的时候,同步过偏移量之后就将syncListContentOffsetEnabled 设置成NO
 没有悬浮的时候需要持续的同步
 */
@property (nonatomic, assign) BOOL syncListContentOffsetEnabled;

/*当前headerContainerView 相对于self的位置.y
 该属性在横向滚动分页视图的时候会用到,因为横向滚动之前,
 我们的headerContainerView视图是添加在纵向滚动的列表视图里面的,为了达到
 头部悬浮的效果,我们需要在横向滚动的时候,将headerContainerView 添加到 self上面
 这个时候就需要我们在纵向滚动的时候,实时的记录headerContainerView 相对于self的
 位置,并且在切换页面之后,使用该位置换算成相对列表的位置,设置纵向列表的偏移量,
 并将headerContainerView 添加到 新的纵向列表上
 */
@property (nonatomic, assign) CGFloat currentHeaderContainerViewY;

///headerContainerView的height, 等于headerHeight  + segmehtheight
@property (nonatomic, assign) CGFloat headerContainerHeight;
///headerView 的height。 headerView顶部视图(不包括segment)
@property (nonatomic, assign) CGFloat headerHeight;
/// segment高度
@property (nonatomic, assign) CGFloat segmentedHeight;

/*
 当前列表视图的初始偏移量
 该属性在这样的场景下起作用:我们在列表1滚动了一定的偏移量,如果列表2还没有创建,
 这时候滑动到列表2 ,列表2 也要滚动相同的偏移量(当然最多是滚动到头部悬浮),这是为了
 切换列表之后,顶部的位置不变,因为没有悬浮的时候,headerContainerView是放在
 列表视图上面的,所以新创建的列表也要设置同样的偏移量 currentListInitializeContentOffsetY
 = 当前的currentListInitializeContentOffsetY 减去当前 headerContainerView相对于
 self的位置(即当前列表滚动实际距离)
 */
@property (nonatomic, assign) CGFloat currentListInitializeContentOffsetY;

@property (nonatomic, assign) BOOL      isLoaded;

@property (nonatomic, assign) BOOL       originShowsVerticalScrollIndicator;

/*
 横向滚动
 */
@property (nonatomic, assign) BOOL       isScroll;
@property (nonatomic, assign) NSInteger  willAppearIndex;
@property (nonatomic, assign) NSInteger  willDisappearIndex;

/*
 正在设置列表的偏移量,正在设置列表的偏移量的时候,
 横向滚动的代理方法中的回调不执行
 */
@property (nonatomic, assign) BOOL       isChangeOffset;


@end

@implementation LBPageSmoothView

- (instancetype)initWithDataSource:(id<LBPageSmoothViewDataSource>)dataSource {
    if (self = [super initWithFrame:CGRectZero]) {
        self.dataSource = dataSource;
        _listDict = [NSMutableDictionary dictionary];
        _listHeaderDict = [NSMutableDictionary dictionary];
        _ceilPointHeight = 0;
        _willAppearIndex = -1;
        _willDisappearIndex = -1;
        
        [self addSubview:self.listCollectionView];
        [self addSubview:self.headerContainerView];
        [self refreshHeaderView];
    }
    return self;
}

- (void)dealloc {
    for (id<LBPageSmoothListViewDelegate> listItem in self.listDict.allValues) {
        [listItem.listScrollView removeObserver:self forKeyPath:@"contentOffset"];
        [listItem.listScrollView removeObserver:self forKeyPath:@"contentSize"];
    }
    
    [self.headerView removeFromSuperview];
    [self.segmentedView removeFromSuperview];
    self.listCollectionView.dataSource = nil;
    self.listCollectionView.delegate = nil;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    if (self.listCollectionView.superview == self) {
        [self refreshListFrame:self.bounds];
        self.listCollectionView.frame = self.bounds;
    }
    
    [self.listHeaderDict enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, UIView * _Nonnull obj, BOOL * _Nonnull stop) {
        CGRect frame = obj.frame;
        frame.origin.y = -self.headerContainerHeight;
        frame.size.height = self.headerContainerHeight;
        obj.frame = frame;
    }];
}

- (void)refreshListFrame:(CGRect)frame {
    for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
        CGRect f = list.listView.frame;
        if ((f.size.width != 0 && f.size.width != frame.size.width) || (f.size.height != 0 && f.size.height != frame.size.height)) {
            f.size.width = frame.size.width;
            f.size.height = frame.size.height;
            list.listView.frame = f;
            [self.listCollectionView reloadData];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.08 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.isChangeOffset = YES;
                [self setScrollView:self.listCollectionView offset:CGPointMake(self.currentIndex * frame.size.width, 0)];
            });
        }
    }
}

- (void)refreshHeaderView {
    [self loadHeaderAndSegmentedView];
    [self refreshHeaderContainerView];
}

- (void)refreshSegmentedView {
    self.segmentedView = [self.dataSource segmentedViewInSmoothView:self];
    [self.headerContainerView addSubview:self.segmentedView];
    
    [self refreshHeaderContainerHeight];
    [self refreshHeaderContainerView];
}

- (void)reloadData {
    self.currentListScrollView = nil;
    self.syncListContentOffsetEnabled = NO;
    self.currentHeaderContainerViewY = 0;
    self.isLoaded = YES;
    
    [self.listHeaderDict removeAllObjects];
    
    for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
        [list.listScrollView removeObserver:self forKeyPath:@"contentOffset"];
        [list.listScrollView removeObserver:self forKeyPath:@"contentSize"];
        [list.listView removeFromSuperview];
    }
    [_listDict removeAllObjects];
    
    __weak __typeof(self) weakSelf = self;
    [self refreshWidthCompletion:^(CGSize size) {
        __strong __typeof(weakSelf) self = weakSelf;
        [self setScrollView:self.listCollectionView offset:CGPointMake(size.width * self.currentIndex, 0)];
        [self.listCollectionView reloadData];
        
        // 首次加载
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self listWillAppear:self.currentIndex];
            [self listDidAppear:self.currentIndex];
        });
    }];
}

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

- (void)scrollToOriginalPointAnimated:(BOOL)animated {
    [self.currentListScrollView setContentOffset:CGPointMake(0, -self.headerContainerHeight) animated:animated];
}

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

- (void)scrollToCriticalPointAnimated:(BOOL)animated {
    [self.currentListScrollView setContentOffset:CGPointMake(0, -(self.segmentedHeight+self.ceilPointHeight)) animated:animated];
}

- (void)setDefaultSelectedIndex:(NSInteger)defaultSelectedIndex {
    _defaultSelectedIndex = defaultSelectedIndex;
    self.currentIndex = defaultSelectedIndex;
}

#pragma mark - UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return self.isLoaded ? 1 : 0;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return [self.dataSource numberOfListsInSmoothView:self];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:LIVPageSmoothViewCellID forIndexPath:indexPath];
    id<LBPageSmoothListViewDelegate> list = self.listDict[@(indexPath.item)];
    if (list == nil) {
        list = [self.dataSource smoothView:self initListAtIndex:indexPath.item];
        if ([list isKindOfClass:UIViewController.class]) {
            UIResponder *next = self.superview;
            while (next != nil) {
                if ([next isKindOfClass:UIViewController.class]) {
                    [((UIViewController *)next) addChildViewController:(UIViewController *)list];
                    break;
                }
                next = next.nextResponder;
            }
        }
        _listDict[@(indexPath.item)] = list;
        [list.listView setNeedsLayout];
        
        UIScrollView *listScrollView = list.listScrollView;
        if ([listScrollView isKindOfClass:[UITableView class]]) {
            ((UITableView *)listScrollView).estimatedRowHeight = 0;
            ((UITableView *)listScrollView).estimatedSectionHeaderHeight = 0;
            ((UITableView *)listScrollView).estimatedSectionFooterHeight = 0;
        }
        if (@available(iOS 11.0, *)) {
            listScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        }
        
        if (listScrollView) {
            CGFloat minContentSizeHeight = self.bounds.size.height - self.segmentedHeight - self.ceilPointHeight;
            if (listScrollView.contentSize.height < minContentSizeHeight && self.isHoldUpScrollView) {
                listScrollView.contentSize = CGSizeMake(self.bounds.size.width, minContentSizeHeight);
            }
        }
        
        UIEdgeInsets insets = listScrollView.contentInset;
        insets.top = self.headerContainerHeight;
        listScrollView.contentInset = insets;
        self.currentListInitializeContentOffsetY = -listScrollView.contentInset.top + MIN(-self.currentHeaderContainerViewY, (self.headerHeight - self.ceilPointHeight));
        [self setScrollView:listScrollView offset:CGPointMake(0, self.currentListInitializeContentOffsetY)];
        UIView *listHeader = [[UIView alloc] initWithFrame:CGRectMake(0, -self.headerContainerHeight, self.bounds.size.width, self.headerContainerHeight)];
        [listScrollView addSubview:listHeader];
        if (self.headerContainerView.superview == nil) {
            [listHeader addSubview:self.headerContainerView];
        }
        self.listHeaderDict[@(indexPath.item)] = listHeader;
        
        [listScrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
        [listScrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
        // bug fix #69 修复首次进入时可能出现的headerView无法下拉的问题
        [listScrollView setContentOffset:listScrollView.contentOffset];
    }
    for (id<LBPageSmoothListViewDelegate> listItem in self.listDict.allValues) {
        listItem.listScrollView.scrollsToTop = (listItem == list);
    }
    
    UIView *listView = list.listView;
    if (listView != nil && listView.superview != cell.contentView) {
        [cell.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
        listView.frame = cell.bounds;
        [cell.contentView addSubview:listView];
    }
    return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
        list.listView.frame = (CGRect){{0, 0}, self.listCollectionView.bounds.size};
    }
    return self.listCollectionView.bounds.size;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (self.isChangeOffset) {
        self.isChangeOffset = NO;
        return;
    }
    if ([self.delegate respondsToSelector:@selector(smoothView:scrollViewDidScroll:)]) {
        [self.delegate smoothView:self scrollViewDidScroll:scrollView];
    }
    
    CGFloat indexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width;
    NSInteger index = floor(indexPercent);
    self.isScroll = YES;
    
    UIScrollView *listScrollView = self.listDict[@(index)].listScrollView;
    if (index != self.currentIndex && indexPercent - index == 0 && !(scrollView.isDragging || scrollView.isDecelerating) && listScrollView.contentOffset.y <= -(self.segmentedHeight + self.ceilPointHeight)) {
        [self horizontalScrollDidEndAtIndex:index];
    }else {
        // 左右滚动的时候,把headerContainerView添加到self,达到悬浮的效果
        if (self.headerContainerView.superview != self) {
            CGRect frame = self.headerContainerView.frame;
            frame.origin.y = self.currentHeaderContainerViewY;
            self.headerContainerView.frame = frame;
            [self addSubview:self.headerContainerView];
        }
    }
    
    if (!scrollView.isDragging && !scrollView.isTracking && !scrollView.isDecelerating) {
        return;
    }
    CGFloat ratio = scrollView.contentOffset.x/scrollView.bounds.size.width;
    NSInteger maxCount = round(scrollView.contentSize.width/scrollView.bounds.size.width);
    NSInteger leftIndex = floorf(ratio);
    leftIndex = MAX(0, MIN(maxCount - 1, leftIndex));
    NSInteger rightIndex = leftIndex + 1;
    if (ratio < 0 || rightIndex >= maxCount) {
        [self listDidAppearOrDisappear:scrollView];
        return;
    }
    if (rightIndex == self.currentIndex) {
        //当前选中的在右边,用户正在从右边往左边滑动
        if (self.listDict[@(leftIndex)] != nil) {
            if (self.willAppearIndex == -1) {
                self.willAppearIndex = leftIndex;
                [self listWillAppear:self.willAppearIndex];
            }
        }
        if (self.willDisappearIndex == -1) {
            self.willDisappearIndex = rightIndex;
            [self listWillDisappear:self.willDisappearIndex];
        }
    }else {
        //当前选中的在左边,用户正在从左边往右边滑动
        if (self.listDict[@(rightIndex)] != nil) {
            if (self.willAppearIndex == -1) {
                self.willAppearIndex = rightIndex;
                [self listWillAppear:self.willAppearIndex];
            }
        }
        if (self.willDisappearIndex == -1) {
            self.willDisappearIndex = leftIndex;
            [self listWillDisappear:self.willDisappearIndex];
        }
    }
    [self listDidAppearOrDisappear:scrollView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        // 滑动到一半又取消滑动处理
        if (self.willDisappearIndex != -1) {
            [self listWillAppear:self.willDisappearIndex];
            [self listWillDisappear:self.willAppearIndex];
            [self listDidAppear:self.willDisappearIndex];
            [self listDidDisappear:self.willAppearIndex];
            self.willDisappearIndex = -1;
            self.willAppearIndex = -1;
        }
    }
    if (!decelerate) {
        NSInteger index = scrollView.contentOffset.x / scrollView.bounds.size.width;
        [self horizontalScrollDidEndAtIndex:index];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // 滑动到一半又取消滑动处理
    if (self.willDisappearIndex != -1) {
        [self listWillAppear:self.willDisappearIndex];
        [self listWillDisappear:self.willAppearIndex];
        [self listDidAppear:self.willDisappearIndex];
        [self listDidDisappear:self.willAppearIndex];
        self.willDisappearIndex = -1;
        self.willAppearIndex = -1;
    }
    NSInteger index = scrollView.contentOffset.x / scrollView.bounds.size.width;
    [self horizontalScrollDidEndAtIndex:index];
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
    if (!self.isLoaded) return;
    // 修复快速闪烁问题
    NSInteger index = scrollView.contentOffset.x / scrollView.bounds.size.width;
    self.currentIndex = index;
    self.currentListScrollView = self.listDict[@(index)].listScrollView;
    self.isScroll = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (!self.isScroll && self.headerContainerView.superview == self) {
            [self horizontalScrollDidEndAtIndex:index];
        }
    });
}

#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"contentOffset"]) {
        UIScrollView *scrollView = (UIScrollView *)object;
        if (scrollView != nil) {
            [self listScrollViewDidScroll:scrollView];
        }
    }else if ([keyPath isEqualToString:@"contentSize"]) {
        UIScrollView *scrollView = (UIScrollView *)object;
        if (scrollView != nil) {
            CGFloat minContentSizeHeight = self.bounds.size.height - self.segmentedHeight - self.ceilPointHeight;
            CGFloat contentH = scrollView.contentSize.height;
            if (minContentSizeHeight > contentH && self.isHoldUpScrollView) {
                scrollView.contentSize = CGSizeMake(scrollView.contentSize.width, minContentSizeHeight);
                // 新的scrollView第一次加载的时候重置contentOffset
                if (self.currentListScrollView != nil && scrollView != self.currentListScrollView) {
                    if (!CGSizeEqualToSize(scrollView.contentSize, CGSizeZero)) {
                        [self setScrollView:scrollView offset:CGPointMake(0, self.currentListInitializeContentOffsetY)];
                    }
                }
            }else {
                BOOL shouldReset = YES;
                for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
                    if (list.listScrollView == scrollView && [list respondsToSelector:@selector(listScrollViewShouldReset)]) {
                        shouldReset = [list listScrollViewShouldReset];
                    }
                }
                
                if (minContentSizeHeight > contentH && shouldReset) {
                    [self setScrollView:scrollView offset:CGPointMake(scrollView.contentOffset.x, -self.headerContainerHeight)];
                    [self listScrollViewDidScroll:scrollView];
                }
            }
        }
    }else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

#pragma mark - Private Methods
- (void)listWillAppear:(NSInteger)index {
    if (![self checkIndexValid:index]) return;
    id<LBPageSmoothListViewDelegate> list = _listDict[@(index)];
    if (list && [list respondsToSelector:@selector(listViewWillAppear)]) {
        [list listViewWillAppear];
    }
}

- (void)listDidAppear:(NSInteger)index {
    if (![self checkIndexValid:index]) {
        return;
    }
    self.currentIndex = index;
    id<LBPageSmoothListViewDelegate> list = _listDict[@(index)];
    if (list && [list respondsToSelector:@selector(listViewDidAppear)]) {
        [list listViewDidAppear];
    }
}

- (void)listWillDisappear:(NSInteger)index {
    if (![self checkIndexValid:index]) {
        return;
    }
    
    id<LBPageSmoothListViewDelegate> list = _listDict[@(index)];
    if (list && [list respondsToSelector:@selector(listViewWillDisappear)]) {
        [list listViewWillDisappear];
    }
}

- (void)listDidDisappear:(NSInteger)index {
    if (![self checkIndexValid:index]) {
        return;
    }
    id<LBPageSmoothListViewDelegate> list = _listDict[@(index)];
    if (list && [list respondsToSelector:@selector(listViewDidDisappear)]) {
        [list listViewDidDisappear];
    }
}

- (BOOL)checkIndexValid:(NSInteger)index {
    NSUInteger count = [self.dataSource numberOfListsInSmoothView:self];
    if (count <= 0 || index >= count) {
        return NO;
    }
    return YES;
}

- (void)listDidAppearOrDisappear:(UIScrollView *)scrollView {
    CGFloat currentIndexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width;
    if (self.willAppearIndex != -1 || self.willDisappearIndex != -1) {
        NSInteger disappearIndex = self.willDisappearIndex;
        NSInteger appearIndex = self.willAppearIndex;
        if (self.willAppearIndex > self.willDisappearIndex) {
            //将要出现的列表在右边
            if (currentIndexPercent >= self.willAppearIndex) {
                self.willDisappearIndex = -1;
                self.willAppearIndex = -1;
                [self listDidDisappear:disappearIndex];
                [self listDidAppear:appearIndex];
            }
        }else {
            //将要出现的列表在左边
            if (currentIndexPercent <= self.willAppearIndex) {
                self.willDisappearIndex = -1;
                self.willAppearIndex = -1;
                [self listDidDisappear:disappearIndex];
                [self listDidAppear:appearIndex];
            }
        }
    }
}

- (void)listScrollViewDidScroll:(UIScrollView *)scrollView {
    
    if (self.listCollectionView.isDragging || self.listCollectionView.isDecelerating) return;
    
    NSInteger listIndex = [self listIndexForListScrollView:scrollView];
    if (listIndex != self.currentIndex) return;
    self.currentListScrollView = scrollView;
    CGFloat contentOffsetY = scrollView.contentOffset.y + self.headerContainerHeight;
    
    if (contentOffsetY < (self.headerHeight - self.ceilPointHeight)) {
        self.hoverType = LBPageSmoothHoverTypeNone;
        self.syncListContentOffsetEnabled = YES;
        self.currentHeaderContainerViewY = -contentOffsetY;
        for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
            if (list.listScrollView != scrollView) {
                [self setScrollView:list.listScrollView offset:scrollView.contentOffset];
            }
        }
        UIView *listHeader = [self listHeaderForListScrollView:scrollView];
        if (self.headerContainerView.superview != listHeader) {
            CGRect frame = self.headerContainerView.frame;
            frame.origin.y = 0;
            self.headerContainerView.frame = frame;
            [listHeader addSubview:self.headerContainerView];
        }
        
        if (self.isControlVerticalIndicator && self.ceilPointHeight != 0) {
            self.currentListScrollView.showsVerticalScrollIndicator = NO;
        }
    }else {
        self.hoverType = LBPageSmoothHoverTypeTop;
        if (self.headerContainerView.superview != self) {
            CGRect frame = self.headerContainerView.frame;
            frame.origin.y = - (self.headerHeight - self.ceilPointHeight);
            self.headerContainerView.frame = frame;
            [self addSubview:self.headerContainerView];
        }
        
        if (self.isControlVerticalIndicator) {
            self.currentListScrollView.showsVerticalScrollIndicator = YES;
        }
        
        if (self.syncListContentOffsetEnabled) {
            self.syncListContentOffsetEnabled = NO;
            self.currentHeaderContainerViewY = -(self.headerHeight - self.ceilPointHeight);
            for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
                if (list.listScrollView != scrollView) {
                    [self setScrollView:list.listScrollView offset:CGPointMake(0, -(self.segmentedHeight + self.ceilPointHeight))];
                }
            }
        }
    }
    CGPoint contentOffset = CGPointMake(scrollView.contentOffset.x, contentOffsetY);
    if ([self.delegate respondsToSelector:@selector(smoothView:listScrollViewDidScroll:contentOffset:)]) {
        [self.delegate smoothView:self listScrollViewDidScroll:scrollView contentOffset:contentOffset];
    }
}

- (void)loadHeaderAndSegmentedView {
    self.headerView = [self.dataSource headerViewInSmoothView:self];
    self.segmentedView = [self.dataSource segmentedViewInSmoothView:self];
    [self.headerContainerView addSubview:self.headerView];
    [self.headerContainerView addSubview:self.segmentedView];
    
    // 刷新高度
    [self refreshHeaderContainerHeight];
}

- (void)refreshHeaderContainerView {
    __weak __typeof(self) weakSelf = self;
    [self refreshWidthCompletion:^(CGSize size) {
        __strong __typeof(weakSelf) self = weakSelf;
        [self refreshHeaderContainerHeight];
        
        CGRect frame = self.headerContainerView.frame;
        if (CGRectEqualToRect(frame, CGRectZero)) {
            frame = CGRectMake(0, 0, size.width, self.headerContainerHeight);
        }else {
            frame.size.height = self.headerContainerHeight;
        }
        self.headerContainerView.frame = frame;
        
        self.headerView.frame = CGRectMake(0, 0, size.width, self.headerHeight);
        self.segmentedView.frame =  CGRectMake(0, self.headerHeight, size.width, self.segmentedHeight);
        
        if (self.segmentedView.superview != self.headerContainerView) { // 修复headerHeight < size.height, headerContainerHeight > size.height时segmentedView.superView为bottomContainerView
            [self.headerContainerView addSubview:self.segmentedView];
        }
        
        for (id<LBPageSmoothListViewDelegate> list in self.listDict.allValues) {
            UIEdgeInsets insets = list.listScrollView.contentInset;
            insets.top = self.headerContainerHeight;
            list.listScrollView.contentInset = insets;
            [self setScrollView:list.listScrollView offset:CGPointMake(0, -self.headerContainerHeight)];
        }
        for (UIView *listHeader in self.listHeaderDict.allValues) {
            CGRect frame = listHeader.frame;
            frame.origin.y = -self.headerContainerHeight;
            frame.size.height = self.headerContainerHeight;
            listHeader.frame = frame;
        }
    }];
}

- (void)refreshHeaderContainerHeight {
    self.headerHeight = self.headerView.bounds.size.height;
    self.segmentedHeight = self.segmentedView.bounds.size.height;
    self.headerContainerHeight = self.headerHeight + self.segmentedHeight;
}

- (void)refreshWidthCompletion:(void(^)(CGSize size))completion {
    if (self.bounds.size.width == 0) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            !completion ? : completion(self.bounds.size);
        });
    }else {
        !completion ? : completion(self.bounds.size);
    }
}

- (void)horizontalScrollDidEndAtIndex:(NSInteger)index {
    self.currentIndex = index;
    UIView *listHeader = self.listHeaderDict[@(index)];
    UIScrollView *listScrollView = self.listDict[@(index)].listScrollView;
    self.currentListScrollView = listScrollView;
    if (listHeader != nil && listScrollView.contentOffset.y <= -(self.segmentedHeight + self.ceilPointHeight)) {
        for (id<LBPageSmoothListViewDelegate> listItem in self.listDict.allValues) {
            listItem.listScrollView.scrollsToTop = (listItem.listScrollView == listScrollView);
        }
        CGRect frame = self.headerContainerView.frame;
        frame.origin.y = 0;
        self.headerContainerView.frame = frame;
        if (self.headerContainerView.superview != listHeader) {
            [listHeader addSubview:self.headerContainerView];
        }
        
        CGFloat minContentSizeHeight = self.bounds.size.height - self.segmentedHeight - self.ceilPointHeight;
        if (minContentSizeHeight > listScrollView.contentSize.height && !self.isHoldUpScrollView) {
            [self setScrollView:listScrollView offset:CGPointMake(listScrollView.contentOffset.x, -self.headerContainerHeight)];
            [self listScrollViewDidScroll:listScrollView];
        }
    }
}

- (UIView *)listHeaderForListScrollView:(UIScrollView *)scrollView {
    for (NSNumber *index in self.listDict) {
        if (self.listDict[index].listScrollView == scrollView) {
            return self.listHeaderDict[index];
        }
    }
    return nil;
}

- (NSInteger)listIndexForListScrollView:(UIScrollView *)scrollView {
    for (NSNumber *index in self.listDict) {
        if (self.listDict[index].listScrollView == scrollView) {
            return index.integerValue;
        }
    }
    return 0;
}
- (void)setScrollView:(UIScrollView *)scrollView offset:(CGPoint)offset {
    if (!CGPointEqualToPoint(scrollView.contentOffset, offset)) {
        [scrollView setContentOffset:offset animated:NO];
    }
}

#pragma mark - Getter
- (UICollectionView *)listCollectionView {
    if (!_listCollectionView) {
        UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
        layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        layout.minimumLineSpacing = 0;
        layout.minimumInteritemSpacing = 0;
        _listCollectionView = [[LBPageSmoothCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
        _listCollectionView.dataSource = self;
        _listCollectionView.delegate = self;
        _listCollectionView.pagingEnabled = YES;
        _listCollectionView.bounces = NO;
        _listCollectionView.showsHorizontalScrollIndicator = NO;
        _listCollectionView.showsVerticalScrollIndicator = NO;
        _listCollectionView.scrollsToTop = NO;
        [_listCollectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:LIVPageSmoothViewCellID];
        if (@available(iOS 11.0, *)) {
            _listCollectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        }
        if (@available(iOS 10.0, *)) {
            _listCollectionView.prefetchingEnabled = NO;
        }
        _listCollectionView.headerContainerView = self.headerContainerView;
    }
    return _listCollectionView;
}

- (UIView *)headerContainerView {
    if (!_headerContainerView) {
        _headerContainerView = [UIView new];
    }
    return _headerContainerView;
}

- (void)selectIndex:(NSInteger)index;
{
    [self horizontalScrollDidEndAtIndex:index];
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

@end
相关推荐
用户095 小时前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan5 小时前
iOS26适配指南之UIColor
ios·swift
权咚21 小时前
阿权的开发经验小集
git·ios·xcode
用户0921 小时前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸1 天前
macOS自带截图命令ScreenCapture
macos
法的空间1 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918411 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
TESmart碲视1 天前
Mac 真正多显示器支持:TESmart USB-C KVM(搭载 DisplayLink 技术)如何实现
macos·计算机外设·电脑
00后程序员张1 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h2 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa