第一个轮子——滑动视图导航控制器

这两天趁着在公司里继续做着不爱做的需求的空隙,将很多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;  

目录结构

第一个轮子——滑动视图导航控制器_第1张图片
屏幕快照 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

你可能感兴趣的:(第一个轮子——滑动视图导航控制器)