之前项目中需要实现这样一个功能,效果如图所示:
虽然这种效果很常见,原理也挺简单,但也有挺多坑,我个人觉得第三方中比较好的就是MXSegmentedPager,我这边是自己实现的,经过项目的验证,自己扩展、封装了下。
gitHub 链接:FJSegmentedPager
集成方法
静态:手动将FJSegmentedPager
文件夹拖入到工程中。
动态:CocoaPods:pod 'FJSegmentedPager'
一. 使用方法
A. 去掉头部:
1. 设置segementPageView
,设置dataSouce
(备注: 如果有需要也设置delegate
)
// 滚动 栏
- (FJSegementPageView *)segementPageView {
if (!_segementPageView) {
_segementPageView = [[FJSegementPageView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, self.view.frame.size.height)];
_segementPageView.segmentViewStyle = self.segmentViewStyle;
_segementPageView.dataSource = self;
}
return _segementPageView;
}
2. 实现dataSource
代理
#pragma mark --------------- Custom Delegate
// 子页面 总数
- (NSInteger)numberOfChildViewControllers {
return self.titleArray.count;
}
// 子页面 标题
- (NSArray<NSString *> *)titlesArrayOfChildViewControllers {
return self.titleArray;
}
/** 获取到将要显示的页面的控制器
* reuseViewController : 这个是返回给你的controller, 你应该首先判断这个是否为nil, 如果为nil 创建对应的控制器并返回, 如果不为nil直接使用并返回
* index : 对应的下标
*/
- (UIViewController<FJSegmentPageChildVcDelegate> *)childViewController:(UIViewController<FJSegmentPageChildVcDelegate> *)reuseViewController withIndex:(NSInteger)index {
UIViewController<FJSegmentPageChildVcDelegate> *childVc = reuseViewController;
if (!childVc) {
childVc = [[FJSecondShopViewController alloc] init];
}
return childVc;
}
如图所示:
B.带有头部:
1. 继承自FJSegmentedBaseViewController
:
#import "FJSegmentedBaseViewController.h"
@interface FJFirstShopSegmentedViewController : FJSegmentedBaseViewController
@end
2. 设置FJSegementContentCell
,然后设置dataSouce
(备注: 如果有需要也设置delegate
)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FJSegementContentCell *segementContentCell = [FJSegementContentCell cellWithTableView:tableView];
segementContentCell.segmentViewStyle = self.segmentViewStyle;
segementContentCell.segementPageView.dataSource = self;
segementContentCell.segementPageView.delegate = self;
return segementContentCell;
}
3.实现代理方法
#pragma mark --------------- Custom Delegate
#pragma mark ---- FJSegmentPageViewDataSource
// 子页面 总数
- (NSInteger)numberOfChildViewControllers {
return self.titleArray.count;
}
// 子页面 标题
- (NSArray<NSString *> *)titlesArrayOfChildViewControllers {
return self.titleArray;
}
/** 获取到将要显示的页面的控制器
* reuseViewController : 这个是返回给你的controller, 你应该首先判断这个是否为nil, 如果为nil 创建对应的控制器并返回, 如果不为nil直接使用并返回
* index : 对应的下标
*/
- (UIViewController<FJSegmentPageChildVcDelegate> *)childViewController:(UIViewController<FJSegmentPageChildVcDelegate> *)reuseViewController withIndex:(NSInteger)index {
UIViewController<FJSegmentPageChildVcDelegate> *childVc = reuseViewController;
if (!childVc) {
childVc = [[FJFirstShopViewController alloc] init];
}
return childVc;
}
#pragma mark ---- FJSegmentPageViewDelegate
// 子页面 即将 显示
- (void)scrollPageController:(UIViewController *)scrollPageController childViewControllWillAppear:(UIViewController *)childViewController withIndex:(NSInteger)index {
}
// 子页面 已经 显示
- (void)scrollPageController:(UIViewController *)scrollPageController childViewControllDidAppear:(UIViewController *)childViewController withIndex:(NSInteger)index {
}
// 子页面 即将 消失
- (void)scrollPageController:(UIViewController *)scrollPageController childViewControllWillDisappear:(UIViewController *)childViewController withIndex:(NSInteger)index {
}
// 子页面 已经 消失
- (void)scrollPageController:(UIViewController *)scrollPageController childViewControllDidDisappear:(UIViewController *)childViewController withIndex:(NSInteger)index {
}
4. 设置偏移距离
self.tableViewOffsetY = [self.tableView rectForSection:0].origin.y + 10;
效果如图所示:
具体操作详见:Demo
二. 参数 详解
1. 通过FJSegmentViewStyle
来配置相关参数
// 指示器 宽度 显示 类型
typedef NS_ENUM(NSInteger, FJSegmentIndicatorWidthShowType) {
// 自适应
FJSegmentIndicatorWidthShowTypeAdaption = 0,
// 固定 宽度
FJSegmentIndicatorWidthShowTypeAdaptionFixedWidth,
};
// 标题 view 字体颜色 改变 类型
typedef NS_ENUM(NSInteger, FJSegmentTitleViewTitleColorChangeType) {
// 选中 之后 再 颜色 改变
FJSegmentTitleViewTitleColorChangeTypeSelectedChange = 0,
// 颜色 渐变
FJSegmentTitleViewTitleColorChangeTypeGradualChange,
};
@interface FJSegmentViewStyle : NSObject
// 选择 第几个 tag
@property (nonatomic, assign) NSInteger selectedIndex;
// 标题 栏 高度
@property (nonatomic, assign) CGFloat tagSectionViewHeight;
// 分割线 高度
@property (nonatomic, assign) CGFloat separatorLineHeight;
// 指示条 高度
@property (nonatomic, assign) CGFloat segmentedIndicatorViewHeight;
// 指示条 宽度
@property (nonatomic, assign) CGFloat segmentedIndicatorViewWidth;
// 指示条 距离 底部 间距
@property (nonatomic, assign) CGFloat segmentedIndicatorViewWidthToBottomSpacing;
// 指示条 默认 扩展宽度
@property (nonatomic, assign) CGFloat segmentedIndicatorViewExtendWidth;
// 标题 默认 宽度
@property (nonatomic, assign) CGFloat segmentedTitleViewTitleWidth;
// 标题栏 cell 间距
@property (nonatomic, assign) CGFloat segmentedTagSectionCellSpacing;
// 标题栏 左右 间距
@property (nonatomic, assign) CGFloat segmentedTagSectionHorizontalEdgeSpacing;
// 标题 字体
@property (nonatomic, strong) UIFont *itemTitleFont;
// 标题 选中 字体
@property (nonatomic, strong) UIFont *itemTitleSelectedFont;
// 标题 分隔栏 背景色
@property (nonatomic, strong) UIColor *segmentToolbackgroundColor;
// 分段 标题 字体 普通 颜色
@property (nonatomic, strong) UIColor *itemTitleColorStateNormal;
// 分段 标题 字体 选中 颜色
@property (nonatomic, strong) UIColor *itemTitleColorStateSelected;
// 分段 标题 字体 高亮 颜色
@property (nonatomic, strong) UIColor *itemTitleColorStateHighlighted;
// 分割线 背景色
@property (nonatomic, strong) UIColor *separatorBackgroundColor;
// tableView 背景色
@property (nonatomic, strong) UIColor *tableViewBackgroundColor;
// 指示器 背景色
@property (nonatomic, strong) UIColor *indicatorViewBackgroundColor;
// 指示器 宽度 显示 类型
@property (nonatomic, assign) FJSegmentIndicatorWidthShowType segmentIndicatorWidthShowType;
// 标题 字体 颜色 改变 类型
@property (nonatomic, assign) FJSegmentTitleViewTitleColorChangeType titleColorChangeType;
二. 需求和思路
1. 需求:
最外层的视图包含着一个头部、分类栏以及分类栏对应的分类内容视图。
分类栏下面的内容视图页面可左右滚动,同时分类栏也定位到当前滚动的分类,同理点击分类栏上的分类,分类栏下面的分类视图也滚动到对应位置。
向上滚动分类内容视图,最外层的视图向上移动,直到卡住分类栏,分类栏滚定,分类视图内容继续向上滚动。当向下滚动分类视图内容,滚动到视图内容底部,分类栏跟着向下移动直至原来位置。
2. 思路:
最外层视图为
FJSegmentedBaseViewController
,包含tableView、tableViewOffsetY、configModelArray
,其中tableview
是最外层父容器,tableViewOffsetY
是tableView
最大偏移距离、configModelArray
是分类栏模型的数组,根据这个生成分类栏和相关分类类别视图。头部作为
tableview
的头部,分类栏和分类内容视图作为一个UITableViewCell
叫做FJSegementContentCell
分类类别内容
FJSegmentdPageViewController
,包含一个可以响应多个手势的tableView,以及当前索引currentIndex
等。当分类栏处于底部时,向上滑动,最外层的
FJSegmentedBaseViewController
的tableView响应向上移动,而分类类别内容FJSegmentdPageViewController
的tableView不移动;当分类栏处于顶部刚好卡住(偏移距离为tableViewOffsetY)的时候,最外层的FJSegmentedBaseViewController
的tableView不响应,同时通知分类类别内容FJSegmentdPageViewController
的tableView可以进行移动。当分类栏处于顶部时,向下滑动,最外层的
FJSegmentedBaseViewController
的tableView不响应,而分类类别内容FJSegmentdPageViewController
的tableView进行移动;当最外层的FJSegmentedBaseViewController
的tableView向下移动到离开顶部时,最外层的FJSegmentedBaseViewController
的tableView进行响应向下移动同时通知分类类别内容FJSegmentdPageViewController
的tableView不移动。
三. 实现
A. FJSegmentedBaseViewController
最外层容器主要包含tableView、tableViewOffsetY、configModelArray
。其中tableViewOffsetY
是用来判断分类栏卡住的位置,如果这个属性来判断滑动事件的响应者。configModelArray
模型数组主要根据这个属性来生成分类栏以及相应的类别视图。
同时也添加了点击状态栏类别tableView返回顶部的事件通知。
1. 根据tableViewOffsetY来判断滑动事件的响应者
#pragma mark --- scrollView delegate
// 子类 必须 调用 super
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
// 滑动 到 顶端
if (offsetY >= self.tableViewOffsetY) {
// 如果 不能 移动 就 固定
if (self.enableScroll == NO) {
scrollView.contentOffset = CGPointMake(0, self.tableViewOffsetY);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kGoTopNotificationName object:[NSNumber numberWithBool:YES]];
self.enableScroll = NO;
}
// 离开 顶端
else {
// 如果 不能 移动 就 固定
if (self.enableScroll == NO) {
scrollView.contentOffset = CGPointMake(0, self.tableViewOffsetY);
}
}
}
#pragma mark --- noti method
- (void)acceptMsg:(NSNotification *)noti {
if ([noti.name isEqualToString:kLeaveTopNotificationName]) {
NSNumber *tmpNum = noti.object;
if (tmpNum.boolValue == YES) {
self.enableScroll = YES;
}
}
}
2.添加点击状态栏类别tableView返回顶部事件通知
// 点击 返回 到 顶部view
- (UIView *)scrollToTopTapView {
if (!_scrollToTopTapView) {
_scrollToTopTapView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 30.0f)];
_scrollToTopTapView.userInteractionEnabled = YES;
[_scrollToTopTapView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(postScrollToTopViewNoti)]];
_scrollToTopTapView.backgroundColor = [UIColor clearColor];
}
return _scrollToTopTapView;
}
/**
用KVC取statusBar
@return statusBar
*/
- (UIView *)statusBar {
return [[UIApplication sharedApplication] valueForKey:@"statusBar"];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[self statusBar] addSubview:self.scrollToTopTapView];
[self.navigationController setNavigationBarHidden:YES animated:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.scrollToTopTapView removeFromSuperview];
}
B. FJSegementContentCell
主要包含FJSegementContentView
,主要进行参数传递。
C. FJSegementContentView
主要包含分类栏FJSegmentedTagTitleView
和分类内容视图FJSegmentedPageContentView
,以及通过双方代理处理两者之间的同步关系。
#pragma mark --- custom delegate
/******************************* FJTitleTagSectionViewDelegate ******************************/
// 当前 点击 index
- (void)titleSectionView:(FJSegmentedTagTitleView *)titleSectionView clickIndex:(NSInteger)index {
self.detailContentView.selectedIndex = index;
}
/******************************* FJDetailContentViewDelegate ******************************/
- (void)detailContentView:(FJSegmentedPageContentView *)detailContentView selectedIndex:(NSInteger)selectedIndex {
self.tagSecionView.selectedIndex = selectedIndex;
}
D. FJSegmentedTagTitleView
主要是通过UICollectionView
来显示分类标题,同时兼容分类栏多个总宽度超过屏幕宽度和小于屏幕宽度的两种情况,以及确保indicatorView
的准确。
** 1. 兼容分类栏和屏幕宽度的情况**
// 是否 超过 屏幕 宽度 限制
- (void)beyondWidthLimitWithTitleArray:(NSArray *)titleArray {
self.isBeyondLimitWidth = NO;
CGFloat tmpWidth = kFJSegmentedTagSectionCellSpacing;
for (NSString *tmpTitle in titleArray) {
tmpWidth += [self titleWidthWithTitle:tmpTitle];
tmpWidth += kFJSegmentedTagSectionCellSpacing;
}
if (tmpWidth > self.frame.size.width) {
self.isBeyondLimitWidth = YES;
}
}
依据是否超过屏幕宽度设置边距和距离等:
// 更新 tagItemSize
- (void)updateItemSizeWithTitleArray:(NSArray *)titleArray {
if (self.isBeyondLimitWidth == NO) {
self.tagItemSize = CGSizeMake(self.frame.size.width / titleArray.count, self.frame.size.height);
}
else {
self.tagFlowLayout.minimumLineSpacing = kFJSegmentedTagSectionCellSpacing; //最小线间距
self.tagFlowLayout.minimumInteritemSpacing = kFJSegmentedTagSectionCellSpacing;
}
self.tagFlowLayout.itemSize = self.tagItemSize;
CGRect indicatorViewFrame = self.indicatorView.frame;
indicatorViewFrame.origin.x = [self indicatorX];
self.indicatorView.frame = indicatorViewFrame;
self.selectedIndex = _selectedIndex;
self.indicatorView.hidden = NO;
[self.tagCollectionView reloadData];
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize tmpSize = CGSizeZero;
if (self.isBeyondLimitWidth == NO) {
tmpSize = CGSizeMake(self.frame.size.width / self.tagTitleArray.count, self.frame.size.height);
}
else {
NSString *titleStr = self.tagTitleArray[indexPath.row];
CGFloat titleWidth = [titleStr boundingRectWithSize:CGSizeMake(MAXFLOAT, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:kFJSegmentedTitleFontSize} context:nil].size.width;
tmpSize = CGSizeMake(titleWidth, self.frame.size.height);
}
return tmpSize;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
CGFloat edgeSpacing = 0;
if (self.isBeyondLimitWidth) {
edgeSpacing = kFJSegmentedTagSectionHorizontalEdgeSpacing;
}
return UIEdgeInsetsMake(0, edgeSpacing, 0, edgeSpacing);
}
** 2. 确保indicatorView
准确
// 更新 指示view 宽度
- (void)updateIndicatorWidthWithSelectedIndex:(NSInteger)selectedIndex {
NSString *tagTitle = [self.tagTitleArray objectAtIndex:selectedIndex];
CGFloat titleWidth = [self titleWidthWithTitle:tagTitle];
self.indicatorWidth = titleWidth + kFJSegmentedIndicatorViewExtendWidth;
CGRect tmpFrame = self.indicatorView.frame;
tmpFrame.size.width = self.indicatorWidth;
self.indicatorView.frame = tmpFrame;
if (self.isBeyondLimitWidth) {
//获取cell
UICollectionViewCell *cell = [self.tagCollectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:self.selectedIndex inSection:0]];
//获取cell在当前collection的位置
CGRect cellInCollection = [self.tagCollectionView convertRect:cell.frame toView:self.tagCollectionView];
//获取cell在当前屏幕的位置
CGRect cellInSuperview = [self.tagCollectionView convertRect:cellInCollection toView:self];
CGFloat indicatorViewX = cellInSuperview.origin.x - kFJSegmentedIndicatorViewExtendWidth/2.0f;
if (indicatorViewX < 0) {
indicatorViewX = kFJSegmentedTagSectionHorizontalEdgeSpacing - kFJSegmentedIndicatorViewExtendWidth/2.0;
}
[self updateIndicatorViewWithOriginX:indicatorViewX];
}
else {
CGFloat cellWidth = self.frame.size.width / self.tagTitleArray.count;
CGFloat indicatorViewX = cellWidth * selectedIndex + cellWidth/2.0 - self.indicatorView.frame.size.width/2.0;
[self updateIndicatorViewWithOriginX:indicatorViewX];
}
}
F. FJSegmentedPageContentView
主要利用UICollectionView来显示分类类别视图,主要处理和FJSegmentdPageViewController
参数的传递,以及分类栏头部和分类类别视图的同步问题。
** 1. FJSegmentdPageViewController 参数传递
- (void)generateViewControllerArrayWithViewArray:(NSArray *)viewArray {
if (self.viewControllerArray.count == 0) {
[viewArray enumerateObjectsUsingBlock:^(FJConfigModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {
Class clazz = NSClassFromString(obj.viewControllerStr);
FJSegmentdPageViewController *baseViewController = [[clazz alloc] init];
baseViewController.currentIndex = idx;
baseViewController.pageViewControllerParam = obj.pageViewControllerParam;
[self.viewControllerArray addObject:baseViewController];
}];
}
}
** 2. 分类栏头部和分类类别视图的同步问题
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSInteger index = (NSInteger)roundf(scrollView.contentOffset.x / self.pageCollectionView.frame.size.width);
if (self.delegate && [self.delegate respondsToSelector:@selector(detailContentView:selectedIndex:)]) {
[self.delegate detailContentView:self selectedIndex:index];
}
}
G. FJSegmentdPageViewController
分类类别视图主要通过tableView来展示类别内容,同时通过监听和通知判断当前滑动事件响应者以及返回顶部事件。
1. 通过监听和通知判断当前滑动事件响应者
/************************ UIScrollViewDelegate **********************/
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (!self.isEnableScroll) {
[scrollView setContentOffset:CGPointZero];
}
CGFloat offsetY = scrollView.contentOffset.y;
if (offsetY < 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:kLeaveTopNotificationName object:[NSNumber numberWithBool:YES] userInfo:nil];
}
}
#pragma mark --- noti method
- (void)acceptMsg:(NSNotification *)notification {
NSString *notificationName = notification.name;
if ([notificationName isEqualToString:kGoTopNotificationName]) {
NSNumber *tmpNum = (NSNumber *)notification.object;
if (tmpNum.boolValue == YES) {
self.enableScroll = tmpNum.boolValue;
self.tableView.showsVerticalScrollIndicator = YES;
}
}else if([notificationName isEqualToString:kLeaveTopNotificationName]){
self.tableView.contentOffset = CGPointZero;
self.enableScroll = NO;
self.tableView.showsVerticalScrollIndicator = NO;
}
}
2. 返回顶部通知响应
// 滚动 到 顶部
- (void)tableViewScrollToTop:(NSNotification *)noti {
if ([noti.name isEqualToString:kFJSubScrollViewScrollToTopNoti]) {
NSString *selectedIndex = (NSString *)noti.object;
if ([selectedIndex isKindOfClass:[NSString class]]) {
if ([selectedIndex integerValue] == self.currentIndex) {
[self scrollToTopAnimated:YES];
}
}
}
}
H. FJBaseTableView
FJSegmentdPageViewController
的tableView
,主要通过函数- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
来让tableView
可以响应多个手势。
// 当有 多个手势 都可以 响应
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
// 防止 tableView 左右滑动 还可以 上下滑动
if ([otherGestureRecognizer.view isKindOfClass:[UICollectionView class]]) {
return NO;
}
return YES;
}
四. 写在最后
gitHub 链接:FJSegmentedPager
静态:手动将FJSegmentedPager
文件夹拖入到工程中。
动态:CocoaPods:pod 'FJSegmentedPager', '~> 1.0.0
大家有兴趣可以看一下,如果觉得不错,麻烦给个喜欢或star,若发现问题或是其他好的想法请及时告知,谢谢!