滑动视图导航控制器

这两天趁着在公司里继续做着不爱做的需求的空隙,将很多App 常用的滑动视图控制器按照自己的想法造了个轮子,在这记录下整个流程。

Demo 地址
GitHub

演示:

demo.gif

介绍

1.CocoaPods

1
pod 'JCPageController'

2.Demo 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//创建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 构成)
1
2
@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 最多创建两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (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 相邻位置上,待滚动结束后在恢复真正位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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 的切换动画

1
2
3
4
5
6
typedef NS_ENUM(NSUInteger, JCSlideBarLineAnimationType) {
JCSlideBarLineAnimationFixedWidth = 0, //固定宽度
JCSlideBarLineAnimationDynamicWidth = 1, //动态宽度,与标题文字等宽
JCSlideBarLineAnimationStretchFixedWidth = 2, //拉伸效果 固定宽度
JCSlideBarLineAnimationStretchDynamicWidth = 3, //拉伸效果 动态宽度,与标题文字等宽
};

其中拉伸效果需要计算当前切换页面滑动的progress ,以此来计算line 的origin.x 以及width。

这里也提供了tabBar 选中放大效果以及title 颜色渐变,,主要使用的是CGAffineTransformMakeScale

1
@property (nonatomic) BOOL scaleSelectedBar;//是否有选中放大效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (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