demo地址
先看效果:
現在很多這樣的需求,拿到需求的時候是不是不知所措呢?是不是在想着,那麼難的控制器效果,iOS官方爲何不專門出一個控件呢? 然後就去網上找一堆三方,看的一陣矇蔽,再然後就是頭大!!!!!
本篇文章教你快速如何實現,並可以封裝後一句代碼實現本效果,從此再也不用擔心產品提這些需求了。(不知道我這是不是救了你們產品經理一命)
原理剖析
當看不明白時可以直接跳到代碼實現部分
底層容器視圖,可以左右滑動,那麼可以採用UIScrollView和UICollectionView。
UIScrollView實現
- 底部採用UIScrollView,然後每頁採用tableView(或者collectionView,scrollView,webView等),加到scrollView上
- 每頁的tableView設置空的headerView
- 視覺上的headerView是添加到self.view上的,然後根據scollView.contentOffset.y的偏移更改headerView的frame
- segment放在橙色部分,添加到headerView上
優點: 每頁的tableView可以分離到不同的UIViewController中,然後通過
[self.scrollView addSubview:childVC.view];
[self addChildViewController:childVC];
添加到scrollView,便於每個tableView的代碼管理。
**缺點:**scrollView的subViews不復用,subViews較多的時候佔用內存較大
- UICollectionView
- 底部採用UICollectionView,然後Cell中實現tableView(或者collectionView,scrollView,webView等)
- 每個cell中的tableView設置空的headerView
- 視覺上的headerView是添加到self.view上的,然後根據
collectionView.contentOffset.y
的偏移更改headerView的frame - segment放在橙色部分,添加到headerView上
優點:cell複用,省內存
缺點:封裝的話使用着沒有UIScrollView的封裝方便,代碼也比UIScrollView多
實現
這裏以UIScrollView爲容器實現
//代碼中用到的的宏定義
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define HEAD_HEIGHT 240 //headerView的高度
//需要的視圖
@property (nonatomic , strong) UIScrollView *hScrollView;
@property (nonatomic , strong) UITableView *tableView1;
@property (nonatomic , strong) UITableView *tableView2;
@property (nonatomic , strong) UIImageView *headView;
這裏忽略各個view的實現部分,因爲都是常規的視圖創建,需要的就是實現滾動的代理,更改headerView的frame,讓headerView看起來像是跟着scrollview滾動的
*
scrollView滑動時調用
*/
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.hScrollView) {
//如果是底層scrollView的滑動則不用更改headerView跟隨滑動
return;
}
//如果是其他scrollView的滑動則需要更改headerView跟隨滑動
CGFloat contentY = scrollView.contentOffset.y;
// 偏移量contentY有三種情況:
// 1. 頭視圖完全顯示,視圖下拉,即:contentY < 0,此時可做處理:headerView跟隨下移或者headerView放放大
// 2. 頭視圖部分顯示,即contentY >= 0 && contentY < HEAD_HEIGHT,此時headerView跟隨contentY移動
// 3. 頭視圖隱藏(或者只顯示segment),即contentY >= HEAD_HEIGHT,此時headerView固定frame
if (contentY < 0) {
self.headView.frame = CGRectMake(SCREEN_WIDTH * contentY / HEAD_HEIGHT /2, 0, SCREEN_WIDTH * (HEAD_HEIGHT - contentY)/HEAD_HEIGHT, HEAD_HEIGHT - contentY);//頭視圖放大
// self.headView.frame = CGRectMake(0, -contentY, SCREEN_WIDTH, HEAD_HEIGHT);//頭視圖跟隨下移
}else if (contentY >= 0 && contentY < HEAD_HEIGHT) {
self.headView.frame = CGRectMake(0, - contentY, SCREEN_WIDTH, HEAD_HEIGHT);
}else if (contentY >= HEAD_HEIGHT) {
if (CGRectGetMinY(self.headView.frame) != -HEAD_HEIGHT) {
self.headView.frame = CGRectMake(0, - HEAD_HEIGHT, SCREEN_WIDTH, HEAD_HEIGHT);
}}
}
但是此時左右滑動,切換page時,發現各個page的狀態不同步,爲了減少代碼的調用次數多了,所以在另外兩個代理中實現各個page的contentOffet的同步
//放開手指時調用
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (scrollView == self.hScrollView) {
return;
}
CGFloat contentY = scrollView.contentOffset.y;
[self updateTableViewFrame:contentY];
}
//放開手指後,若tableView仍然自己滾動,自己滾動結束時會調用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (scrollView == self.hScrollView) {
return;
}
CGFloat contentY = scrollView.contentOffset.y;
[self updateTableViewFrame:contentY];
}
- (void)updateTableViewFrame:(CGFloat)offsetY {
if (offsetY >= HEAD_HEIGHT) {
//頭視圖已隱藏時,若其他page的tableView的contentOffset的狀態是headview沒隱藏的狀態,則更改爲頭視圖已隱藏時的偏移量
if ( self.tableView1.contentOffset.y <= HEAD_HEIGHT) {
self.tableView1.contentOffset = CGPointMake(0, HEAD_HEIGHT);
}
if ( self.tableView2.contentOffset.y <= HEAD_HEIGHT) {
self.tableView2.contentOffset = CGPointMake(0, HEAD_HEIGHT);
}
}else if (offsetY >= 0 && offsetY < HEAD_HEIGHT) {
// 有視圖部分顯示若其他page的tableView的contentOffset的狀態不是headview部分隱藏的狀態,則更改爲頭視圖部分隱藏的偏移量
self.tableView1.contentOffset = CGPointMake(0, offsetY);
self.tableView2.contentOffset = CGPointMake(0, offsetY);
}else if (offsetY < 0) {
//頭視圖完全顯示時再下拉
if ( self.tableView1.contentOffset.y > 0) {
self.tableView1.contentOffset = CGPointMake(0, 0);
}
if ( self.tableView2.contentOffset.y > 0) {
self.tableView2.contentOffset = CGPointMake(0, 0);
}
}
}
完成
封裝
明白了怎麼實現,也通過上面的簡單demo完成了任務,然後呢,我們需要一勞永逸
如果每次有這樣的需求,我們都實現一遍,明顯是很費腦子的,我們程序員的腦細胞死的本來就多,就不要再做這些無謂的犧牲了,那麼封裝一下,一步到位纔是我們想要的結果!!!
封裝目標:
1. 每頁的數據由單獨的UIViewController控制
2. 繼承封裝好的ViewController後,只需要childVC,headerView,segment.height信息
3. 能夠監測到childVC切換到了第幾個
注意: 因爲每頁(UIViewController)的tableView由UIViewController單獨完成,所以tableView的代理肯定在它的VC中實現。所以封裝的VC採用KVO監測contentOffset的變化。
#import <UIKit/UIKit.h>
@interface SHViewController : UIViewController
/**
添加要左右滑動的viewController
使用viewController能夠更好的
@param childVCArray vc數組
@param headerView 頭視圖
@param segmentHeight segment高度
*/
- (void)addChildVCWithArray:(NSArray <UIViewController *> *)childVCArray
headerView:(UIView *)headerView
segmentHeight:(CGFloat)segmentHeight;
/**
切換vc時調用,index爲要顯示的vc下表
*/
@property (nonatomic , copy) void(^viewControllerScrollToIndex)(NSInteger index);
@end
#import "SHViewController.h"
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define WEAKSELF __weak typeof(self) weakSelf = self;
@interface SHViewController ()
<
UIScrollViewDelegate
>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) NSArray <UIViewController *> *vcArray;
@property (nonatomic, strong) UIView *headerView;//頭視圖
@property (nonatomic, assign) CGFloat headerHeight;//頭視圖的高度
@property (nonatomic, assign) CGFloat segmentHeight;//segment的高度
@property (nonatomic, assign) CGFloat headerMaxScrollHeight;//headerView最大的上移距離
@property (nonatomic, assign) CGFloat viewHeight;//self.view的高度
@end
@implementation SHViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// Do any additional setup after loading the view.
self.automaticallyAdjustsScrollViewInsets = NO;
self.navigationController.navigationBar.translucent = NO;
[self.view addSubview:self.scrollView];
}
- (CGFloat)viewHeight {
if (_viewHeight > 0) {
return _viewHeight;
}
CGFloat height = SCREEN_HEIGHT;
if (self.navigationController && self.navigationController.isNavigationBarHidden == NO) {
height -= 64;
}
if (self.tabBarController.tabBar.isHidden == YES) {
height -= 49;
}
_viewHeight = height;
return _viewHeight;
}
- (UIScrollView *)scrollView {
if (!_scrollView) {
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, self.viewHeight)];
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.pagingEnabled = YES;
_scrollView.delegate = self;
}
return _scrollView;
}
- (void)addChildVCWithArray:(NSArray <UIViewController *> *)childVCArray
headerView:(UIView *)headerView
segmentHeight:(CGFloat)segmentHeight {
//滾動的頭視圖
if (headerView) {
[self.view addSubview:headerView];
self.headerView = headerView;
self.headerHeight = CGRectGetHeight(headerView.frame);
self.segmentHeight = segmentHeight;
self.headerMaxScrollHeight = self.headerHeight - self.segmentHeight;
}
if (!childVCArray || childVCArray.count <= 0) {
return;
}
//scrollview的contentSize
self.scrollView.contentSize = CGSizeMake(SCREEN_WIDTH * childVCArray.count, self.viewHeight);
//需要左右滾動的segmentVC
self.vcArray = childVCArray;
WEAKSELF
[childVCArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UIViewController* childVC = (UIViewController *)obj;
childVC.view.frame = CGRectMake(SCREEN_WIDTH * idx, CGRectGetMinY(childVC.view.frame), SCREEN_WIDTH, CGRectGetHeight(childVC.view.frame));
[weakSelf.scrollView addSubview:childVC.view];
[weakSelf addChildViewController:childVC];
UIScrollView *scrollView = [weakSelf getScrollViewWithVC:childVC];
[scrollView addObserver:weakSelf forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionInitial context:nil];
}];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
UIScrollView *scrollView = object;
CGFloat offsetY = scrollView.contentOffset.y;
if ([keyPath isEqualToString:@"contentOffset"]) {
//headerview的frame變化
if (offsetY >= self.headerMaxScrollHeight) {
if (CGRectGetMinY(self.headerView.frame) != -self.headerMaxScrollHeight) {
self.headerView.frame = CGRectMake(0, - self.headerMaxScrollHeight, SCREEN_WIDTH, self.headerHeight);
}}else if (offsetY >= 0 && offsetY < self.headerMaxScrollHeight) {
self.headerView.frame = CGRectMake(0, - offsetY, SCREEN_WIDTH, self.headerHeight);
}else if (offsetY < 0) {
// self.headerView.frame = CGRectMake(SCREEN_WIDTH * offsetY / self.headerHeight /2.0,0, SCREEN_WIDTH * (self.headerHeight - offsetY)/self.headerHeight, self.headerHeight - offsetY);//頭視圖隨着拉伸變大
self.headerView.frame = CGRectMake(0, -offsetY, SCREEN_WIDTH, self.headerHeight);
}
//各個vc中scrollView的frame變化
WEAKSELF
[self.vcArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UIViewController* childVC = (UIViewController *)obj;
UIScrollView *scrollView = [weakSelf getScrollViewWithVC:childVC];
if (offsetY >= weakSelf.headerMaxScrollHeight) {
if (scrollView.contentOffset.y < weakSelf.headerMaxScrollHeight)
scrollView.contentOffset = CGPointMake(0, weakSelf.headerMaxScrollHeight);
}else if (offsetY >= 0 && offsetY < weakSelf.headerMaxScrollHeight) {
if(scrollView.contentOffset.y != offsetY)
scrollView.contentOffset = CGPointMake(0, offsetY);
}else if (offsetY < 0) {
if (scrollView.contentOffset.y > 0)
scrollView.contentOffset = CGPointMake(0, 0);
}
}];
}
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (self.viewControllerScrollToIndex) {
NSInteger index = scrollView.contentOffset.x / SCREEN_WIDTH;
self.viewControllerScrollToIndex(index);
}
}
- (UIScrollView *)getScrollViewWithVC:(UIViewController *)vc {
for (UIView *tempView in vc.view.subviews) {
if ([tempView isKindOfClass:[UIScrollView class]]) {
return (UIScrollView *)tempView;
}
}
return nil;
}