FJSegmentedPager(仿簡書/微博首頁效果、網易新聞、頭條滑塊視圖聯動效果) 介紹

之前項目中需要實現這樣一個功能,效果如圖所示:

*FJSegmentedPager*

雖然這種效果很常見,原理也挺簡單,但也有挺多坑,我個人覺得第三方中比較好的就是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;
}

如圖所示:

FJSegmentedPageViewNoHeader.gif

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;

效果如圖所示:

FJSegmentedPageView-OneScreen.gif

具體操作詳見: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是最外層父容器,tableViewOffsetYtableView最大偏移距離、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

FJSegmentdPageViewControllertableView,主要通過函數- (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;
}

四. 寫在最后

16_21128_7.jpg

gitHub 鏈接:FJSegmentedPager

靜態:手動將FJSegmentedPager文件夾拖入到工程中。
動態:CocoaPods:pod 'FJSegmentedPager', '~> 1.0.0

大家有興趣可以看一下,如果覺得不錯,麻煩給個喜歡或star,若發現問題或是其他好的想法請及時告知,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,245評論 4 61
  • 來阜平已經快兩個月了,跟他熟絡的日子頂多一個月。從第一次全校教職工大會,他笑著打哈哈走進來坐在我的前排,眼神對視了...
    三分之一小姐閱讀 330評論 0 0