這兩天趁著在公司里繼續(xù)做著不愛做的需求的空隙览妖,將很多App 常用的滑動(dòng)視圖控制器按照自己的想法造了個(gè)輪子瘾杭,在這記錄下整個(gè)流程站蝠。
Demo 地址
GitHub
演示:
介紹
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;
}
更多使用方法請(qǐng)看Demo
原理
構(gòu)成
主要分兩個(gè)部分:
- 上方的TabBar(UICollectionView 構(gòu)成)
- 下方的容器ContentView(UIScrollView 構(gòu)成)
@property (nonatomic, strong) JCPageSlideBar *slideBar;
@property (nonatomic, strong) UIScrollView *contentView;
目錄結(jié)構(gòu)
流程
- 通過數(shù)據(jù)源獲取子Controller 的數(shù)量汰具,以及相應(yīng)索引上tabBar 的寬度和title。
- 通過數(shù)據(jù)源獲取相應(yīng)索引上的Controller菱魔,先判斷如果有相同identifier 的可復(fù)用Controller留荔,若有,則返回,否則創(chuàng)建該Controller聚蝶,存入到緩存中杰妓。
- 當(dāng)手勢(shì)滑動(dòng)ContentView 或點(diǎn)擊TabBar 來切換頁(yè)面時(shí),處理ContentView 與TabBar 之間的協(xié)同問題碘勉。
主要邏輯
首先是重用Controller巷挥,這個(gè)對(duì)提高性能很重要,我是將Controller 都存到controllersMap 這個(gè)字典里验靡,用 @“index_identifier” 來當(dāng)做key倍宾,同一個(gè)identifier 的Controller 最多創(chuàng)建兩個(gè)。
@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) {
//點(diǎn)擊tabbar切換頁(yè)面
if ([identifier isEqualToString:identifierStr]) {
if (self.currentIndex != indexStr.integerValue) {
controller = self.controllersMap[key];
findKey = key;
break;
}
}
}else{
//手勢(shì)滑動(dòng)切換頁(yè)面
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)每次滑動(dòng)即將出現(xiàn)某個(gè)index 對(duì)應(yīng)的Controller 時(shí)高职,才去創(chuàng)建該Controller,將其add 到ContentView 相應(yīng)的ContentOffset 上的辞州。
手勢(shì)滑動(dòng)切換頁(yè)面時(shí)怔锌,主要邏輯在scrollViewDidScroll
這個(gè)方法里,先判斷滑動(dòng)方向变过,然后配置相應(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];
}
//配置下個(gè)將要顯示的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) {
//配置下一個(gè)即將顯示的controller
[self willDraggingToNextController:nextPage];
}
}
當(dāng)點(diǎn)擊tabBar 切換頁(yè)面時(shí),主要實(shí)現(xiàn)JCPageSlideBarDelegate 代理方法媚狰,將nextVCL 放在當(dāng)前Controller 相鄰位置上岛杀,待滾動(dòng)結(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 放在相鄰位置上哈雏,待滾動(dòng)結(jié)束后在恢復(fù)真正位置
self.contentView.userInteractionEnabled = NO;//滾動(dòng)期間 不允許用戶手勢(shì)操作
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 的切換動(dòng)畫
typedef NS_ENUM(NSUInteger, JCSlideBarLineAnimationType) {
JCSlideBarLineAnimationFixedWidth = 0, //固定寬度
JCSlideBarLineAnimationDynamicWidth = 1, //動(dòng)態(tài)寬度楞件,與標(biāo)題文字等寬
JCSlideBarLineAnimationStretchFixedWidth = 2, //拉伸效果 固定寬度
JCSlideBarLineAnimationStretchDynamicWidth = 3, //拉伸效果 動(dòng)態(tài)寬度衫生,與標(biāo)題文字等寬
};
其中拉伸效果需要計(jì)算當(dāng)前切換頁(yè)面滑動(dòng)的progress 裳瘪,以此來計(jì)算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é)
由于自身的能力以及是第一版,尚且存在很多不足之處泪酱,例如不能自由的定制化派殷,代碼注釋不夠,一些方法邏輯躲起來不夠順暢墓阀,總的來說還是可以滿足基本需求。日后有時(shí)間將會(huì)繼續(xù)完善。歡迎大家指出問題而钞,一起交流础锐。
Demo 地址
GitHub