第一個輪子——滑動視圖導(dǎo)航控制器

這兩天趁著在公司里繼續(xù)做著不愛做的需求的空隙,將很多App 常用的滑動視圖控制器按照自己的想法造了個輪子,在這記錄下整個流程。

Demo 地址
GitHub

演示:

demo.gif

介紹

1.CocoaPods

pod 'JCPageController'

2.Demo 使用

//創(chuàng)建pageController
- (JCPageContoller *)pageController{
    if (!_pageController) {
        _pageController = [[JCPageContoller alloc]init];
        _pageController.delegate = self;
        _pageController.dataSource = self;
        [self addChildViewController:_pageController];
        [self.view addSubview:_pageController.view];
        _pageController.lineAinimationType = self.lineAinimationType;
        _pageController.scaleSelectedBar = self.scaleSelectedBar;
    }
    return _pageController;
}

- (NSInteger)numberOfControllersInPageController{
    return count;
}

- (NSString *)reuseIdentifierForControllerAtIndex:(NSInteger)index;{
    return identifier;//用于controller重用
}

- (UIViewController *)pageContoller:(JCPageContoller *)pageContoller controllerAtIndex:(NSInteger)index{
    UIViewController *controller = [pageContoller dequeueReusableControllerWithReuseIdentifier:identifier atIndex:index];//獲取重用的controller
    if (!controller) {
        //controller init
    }
    return controller;
}

- (CGFloat)pageContoller:(JCPageContoller *)pageContoller widthForCellAtIndex:(NSInteger )index{
    return width;
}

- (NSString *)pageContoller:(JCPageContoller *)pageContoller titleForCellAtIndex:(NSInteger)index{
    return text;
}

更多使用方法請看Demo

原理

構(gòu)成

主要分兩個部分:

  • 上方的TabBar(UICollectionView 構(gòu)成)
  • 下方的容器ContentView(UIScrollView 構(gòu)成)
@property (nonatomic, strong) JCPageSlideBar *slideBar;
@property (nonatomic, strong) UIScrollView *contentView;  

目錄結(jié)構(gòu)

屏幕快照 2017-02-26 22.36.00.png

流程

  1. 通過數(shù)據(jù)源獲取子Controller 的數(shù)量,以及相應(yīng)索引上tabBar 的寬度和title。
  2. 通過數(shù)據(jù)源獲取相應(yīng)索引上的Controller,先判斷如果有相同identifier 的可復(fù)用Controller,若有,則返回,否則創(chuàng)建該Controller,存入到緩存中。
  3. 當(dāng)手勢滑動ContentView 或點擊TabBar 來切換頁面時,處理ContentView 與TabBar 之間的協(xié)同問題。

主要邏輯

首先是重用Controller,這個對提高性能很重要,我是將Controller 都存到controllersMap 這個字典里,用 @“index_identifier” 來當(dāng)做key,同一個identifier 的Controller 最多創(chuàng)建兩個。

@property (nonatomic, strong) NSMutableDictionary *controllersMap; //用于保存controllers 用 @“index_identifier” 來當(dāng)做key   value為controller

- (UIViewController *)dequeueReusableControllerWithReuseIdentifier:(NSString *)identifier atIndex:(NSInteger)index{
    if (!identifier) {
        return nil;
    }
    NSInteger count = [self.dataSource numberOfControllersInPageController];
    if (index >= count || index < 0) {
        return nil;
    }
    UIViewController *controller = nil;
    NSString *findKey = nil;
    for (NSString *key in self.controllersMap) {
        NSArray *components = [key componentsSeparatedByString:@"_"];
        NSString *indexStr = components.firstObject;
        NSString *identifierStr = components.lastObject;
        NSInteger gap = labs(indexStr.integerValue - index);
        if (self.didSelectBarToChangePage) {
            //點擊tabbar切換頁面
            if ([identifier isEqualToString:identifierStr]) {
                if (self.currentIndex != indexStr.integerValue) {
                    controller = self.controllersMap[key];
                    findKey = key;
                    break;
                }
            }
        }else{
            //手勢滑動切換頁面
            if ([identifier isEqualToString:identifierStr] && gap > 1) {
                controller = self.controllersMap[key];
                findKey = key;
                break;
            }
        }
    }
    if (findKey) {
        if ([controller respondsToSelector:@selector(prepareForReuse)]) {
            [controller performSelector:@selector(prepareForReuse)];
        }
        [self.controllersMap removeObjectForKey:findKey];
    }else{
        if ([self getControllerFromMap:index]) {
            controller = [self getControllerFromMap:index];
        }
    }
    return controller;
}

為了性能考慮,只有當(dāng)每次滑動即將出現(xiàn)某個index 對應(yīng)的Controller 時,才去創(chuàng)建該Controller,將其add 到ContentView 相應(yīng)的ContentOffset 上的。

手勢滑動切換頁面時,主要邏輯在scrollViewDidScroll這個方法里,先判斷滑動方向,然后配置相應(yīng)的Controller。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    
    ...
    
    BOOL scrollToRight = YES;
    if (contentOffsetX - self.lastOffsetX > 0) {
        if (contentOffsetX <= curControllerOriginX) {
            return;
        }
        nextPage = page < totalCount - 1 ? page + 1 : totalCount - 1;
    }else{
        if (contentOffsetX >= curControllerOriginX) {
            return;
        }
        scrollToRight = NO;
        page = page < totalCount - 1 ? page+1 : totalCount-1;
        nextPage = page > 0 ? page - 1 : 0;
    }
    self.lastOffsetX = contentOffsetX;
    
    if (self.currentIndex != page) {
        //配置當(dāng)前顯示的controller
        self.currentIndex = page;
        self.currentController = self.nextController;
        [self.slideBar selectTabAtIndex:self.currentIndex];
    }
    //配置下個將要顯示的controller
    [self checkNeedConfigNextPage:scrollToRight nextPage:nextPage];
}

- (void)checkNeedConfigNextPage:(BOOL)scrollToRight nextPage:(NSInteger)nextPage{
    CGFloat contentOffsetX = self.contentView.contentOffset.x;
    BOOL needConfigNextPage = NO;
    if (scrollToRight) {
        if (contentOffsetX > self.currentIndex * self.contentView.frame.size.width) {
            needConfigNextPage = YES;
        }
    }else{
        if (contentOffsetX < self.currentIndex * self.contentView.frame.size.width) {
            needConfigNextPage = YES;
        }
    }
    if (needConfigNextPage && self.nextIndex != nextPage) {
        //配置下一個即將顯示的controller
        [self willDraggingToNextController:nextPage];
    }
}

當(dāng)點擊tabBar 切換頁面時,主要實現(xiàn)JCPageSlideBarDelegate 代理方法,將nextVCL 放在當(dāng)前Controller 相鄰位置上,待滾動結(jié)束后在恢復(fù)真正位置。

- (void)pageSlideBar:(JCPageSlideBar *)pageSlideBar didSelectBarAtIndex:(NSInteger)index{
    ...
    self.selectBarIndex = index;
    NSInteger realIndex = self.currentIndex < index ?  self.currentIndex + 1 : self.currentIndex - 1;
    UIViewController *nextVCL = [self willDraggingToNextController:index];
    if (nextVCL) {
        //將nextVCL 放在相鄰位置上,待滾動結(jié)束后在恢復(fù)真正位置
        self.contentView.userInteractionEnabled = NO;//滾動期間 不允許用戶手勢操作
        self.currentController = nextVCL;
        CGRect rect = nextVCL.view.frame;
        rect.origin.x = realIndex * self.contentView.frame.size.width;
        nextVCL.view.frame = rect;
    }
    [self.contentView setContentOffset:CGPointMake(realIndex * self.contentView.frame.size.width,0) animated:YES];
}

默認(rèn)提供了四種line 的切換動畫

typedef NS_ENUM(NSUInteger, JCSlideBarLineAnimationType) {
    JCSlideBarLineAnimationFixedWidth = 0,       //固定寬度
    JCSlideBarLineAnimationDynamicWidth = 1,     //動態(tài)寬度,與標(biāo)題文字等寬
    JCSlideBarLineAnimationStretchFixedWidth = 2,          //拉伸效果 固定寬度
    JCSlideBarLineAnimationStretchDynamicWidth = 3,          //拉伸效果 動態(tài)寬度,與標(biāo)題文字等寬
};

其中拉伸效果需要計算當(dāng)前切換頁面滑動的progress ,以此來計算line 的origin.x 以及width。

這里也提供了tabBar 選中放大效果以及title 顏色漸變,,主要使用的是CGAffineTransformMakeScale,

@property (nonatomic) BOOL scaleSelectedBar;//是否有選中放大效果
- (void)scaleTitleFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex progress:(CGFloat)progress{
    if (!self.scaleSelectedBar) {
        return;
    }
    CGFloat scale = kSlideBarCellScaleSize;
    CGFloat currentTransform = (scale - 1) * progress;
    UICollectionViewCell *fromCell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:fromIndex inSection:0]];
    UICollectionViewCell *toCell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:toIndex inSection:0]];
    fromCell.transform = CGAffineTransformMakeScale(scale - currentTransform , scale - currentTransform);
    toCell.transform = CGAffineTransformMakeScale(1 + currentTransform, 1 + currentTransform);
    
    if (self.lineAinimationType < JCSlideBarLineAnimationStretchFixedWidth) {
        //不是拉伸效果就不用變顏色了
        return;
    }
    CGFloat narR,narG,narB,narA;
    [kTitleNormalColor getRed:&narR green:&narG blue:&narB alpha:&narA];
    CGFloat selR,selG,selB,selA;
    [kTitleSelectedColor getRed:&selR green:&selG blue:&selB alpha:&selA];
    CGFloat detalR = narR - selR ,detalG = narG - selG,detalB = narB - selB,detalA = narA - selA;
    
    UILabel *fromTitle = [fromCell viewWithTag:kSlideBarCellTitleTag];
    UILabel *toTitle = [toCell viewWithTag:kSlideBarCellTitleTag];
    fromTitle.textColor = [UIColor colorWithRed:selR + detalR * progress green:selG+detalG * progress blue:selB+detalB * progress alpha:selA+detalA * progress];
    toTitle.textColor = [UIColor colorWithRed:narR-detalR * progress green:narG-detalG * progress blue:narB-detalB * progress alpha:narA-detalA * progress];
}

總結(jié)

由于自身的能力以及是第一版,尚且存在很多不足之處,例如不能自由的定制化,代碼注釋不夠,一些方法邏輯躲起來不夠順暢,總的來說還是可以滿足基本需求。日后有時間將會繼續(xù)完善。歡迎大家指出問題,一起交流。

Demo 地址
GitHub

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,423評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,167評論 25 708
  • 她,我深深愛著的她。 她,我難以忘記的她。 她,我永得不到的她。 多少次為她哭泣的我, 想永遠(yuǎn)的忘記她。 多少次為...
    彼岸花s閱讀 356評論 0 2
  • 黃昏一簾花雨香,倦書沉夢逅謝娘。 摘花別是傷心人,時有風(fēng)微吹茶涼。
    拾肆十四14閱讀 177評論 0 0
  • 1 基本介紹 2 tcp_wrapper 3 PAM 認(rèn)證機制 PAM 認(rèn)證機制
    一橋長書閱讀 591評論 0 0

友情鏈接更多精彩內(nèi)容