封装了一个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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值