第一個輪子——滑動視圖導航控制器

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

Demo 地址
GitHub

演示:

demo.gif

介紹

1.CocoaPods

pod 'JCPageController'

2.Demo 使用

//創建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

原理

構成

主要分兩個部分:

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

目錄結構

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

流程

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

主要邏輯

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

@property (nonatomic, strong) NSMutableDictionary *controllersMap; //用于保存controllers 用 @“index_identifier” 來當做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;
}

為了性能考慮,只有當每次滑動即將出現某個index 對應的Controller 時,才去創建該Controller,將其add 到ContentView 相應的ContentOffset 上的。

手勢滑動切換頁面時,主要邏輯在scrollViewDidScroll這個方法里,先判斷滑動方向,然后配置相應的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) {
        //配置當前顯示的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];
    }
}

當點擊tabBar 切換頁面時,主要實現JCPageSlideBarDelegate 代理方法,將nextVCL 放在當前Controller 相鄰位置上,待滾動結束后在恢復真正位置。

- (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 放在相鄰位置上,待滾動結束后在恢復真正位置
        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];
}

默認提供了四種line 的切換動畫

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

其中拉伸效果需要計算當前切換頁面滑動的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];
}

總結

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

Demo 地址
GitHub

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

推薦閱讀更多精彩內容

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