有一个smoothView,(继承于UIView),里面包含一个横向滚动的collectionView
总体是一个横向的collectionView , 有多少个分页,就有多少个cell
这个 cell的大小就是整个页面的大小,cell 上面放了一个控制器的view,控制器的view上面放了单个的纵向滚动的视图,
则初始化的状态下,纵向滚动的列表视图的contentInset.top就是头部
容器视图的高度,然后头部容器视图添加到当前展示的单个纵向列表上,这样当在没有达到临界偏移量的时候,当纵向列表想上滚动的时候,头部容器视图就会跟随纵向列表滚动。
smoothView上,这个样就大大大到了悬浮的效果,
我们横向滚动分页视图,则需要将头部容器视图添加到self (smoothView)上面 (在scrollViewdidscroll方法里面),也是大到一个悬浮的效果,否则的话,头部视图就会跟随当前的列表视图划走
(在每没有悬浮的情况下) 要将其他的列表的偏移量和设置成和当前列表的偏移量相同,因为为了达到切换页面的时候,头部的位置一样
并重写手势的代理方法
//是否支持多手势触发,返回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;
}
因为在没有悬浮的时候,我们的头部容器视图是放在纵向的列表上的
其实也是放在水平的collecitonView的一个ce ll 上面,
但是这个时候,我们从视觉上看它是头部,当我们手势在头部的时候当然是不能支持水平滚动的,所以要重写collectionView的 手势代理方法shouldReceiveTouch,这个时候不响应水平滚动手势
总的代码
//
// LBPageSmoothView.h
// TEXT
//
// Created by mac on 2024/8/11.
// Copyright © 2024 刘博. All rights reserved.
//
#import
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, LBPageSmoothHoverType) {
LBPageSmoothHoverTypeNone = 0, // 未悬停
LBPageSmoothHoverTypeTop = 1, // 顶部悬停
};
@class LBPageSmoothView;
@protocol LBPageSmoothListViewDelegate
/// 返回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
/// 返回页面的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)smoothView:(LBPageSmoothView *)smoothView initListAtIndex:(NSInteger)index;
@end
@protocol LBPageSmoothViewDelegate
@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 delegate;
// 当前已经加载过的可用的列表字典,key是index值,value是对应列表
@property (nonatomic, strong, readonly) NSDictionary > *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)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
@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 ()
@property (nonatomic, weak) id dataSource;
@property (nonatomic, strong) LBPageSmoothCollectionView *listCollectionView;
/*
这里面盛放的是列表容器,不是列表视图
*/
@property (nonatomic, strong) NSMutableDictionary > *listDict;
/*
这里面的视图是headerContainerView 的父视图(在没有悬浮的时候),
其实没有这个的话,直接添加到竖直滚动的列表中也是可以的,
只不过我们有了这个方便管理
*/
@property (nonatomic, strong) NSMutableDictionary *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)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 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 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 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 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 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 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 *)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 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 list = _listDict[@(index)];
if (list && [list respondsToSelector:@selector(listViewWillAppear)]) {
[list listViewWillAppear];
}
}
- (void)listDidAppear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
self.currentIndex = index;
id list = _listDict[@(index)];
if (list && [list respondsToSelector:@selector(listViewDidAppear)]) {
[list listViewDidAppear];
}
}
- (void)listWillDisappear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id list = _listDict[@(index)];
if (list && [list respondsToSelector:@selector(listViewWillDisappear)]) {
[list listViewWillDisappear];
}
}
- (void)listDidDisappear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id 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 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 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 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 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