一句代碼實現帶有頭視圖的pageController效果

demo地址

先看效果:

頭視圖跟隨下移.gif

下拉頭視圖放大.gif

現在很多這樣的需求,拿到需求的時候是不是不知所措呢?是不是在想着,那麼難的控制器效果,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較多的時候佔用內存較大

  1. 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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章