之前項目中需要實現這樣一個功能,效果如圖所示:
雖然這種效果很常見,原理也挺簡單,但也有挺多坑,我個人覺得第三方中比較好的就是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,若發現問題或是其他好的想法請及時告知,謝謝!