需求效果:

demo拉取地址:demo
最簡(jiǎn)單的實(shí)現(xiàn)方式是,放一個(gè)tableview,這個(gè)tableview有一個(gè)headView,這個(gè)headview就是上圖所示的藍(lán)色頭部的View,但是這樣做的結(jié)果是,在tableView下拉刷新數(shù)據(jù)的時(shí)候,刷新動(dòng)畫(huà)會(huì)出現(xiàn)在headView的上方,這樣看著就異常令人難受了,當(dāng)時(shí)為了節(jié)省時(shí)間,就是這么實(shí)現(xiàn)的.現(xiàn)在有充足的時(shí)間的情況下,是不允許有瑕疵的.
于是就有了UIScrollView + UITableView的組合實(shí)現(xiàn)方案;
組合的方案實(shí)現(xiàn)遇到的問(wèn)題
UIScrollView和UITableView的組合使用問(wèn)題整理:
1.手勢(shì)沖突
2.tableview和scrollview一起滑動(dòng)
3.scrollview滑動(dòng)到底部之后,tableview上拉沒(méi)反應(yīng)
4.scrollview滑出了指定的頭部區(qū)域之后下拉沒(méi)反應(yīng)
5.tableview的下拉刷新無(wú)效
a.手勢(shì)事件的穿透
為解決手勢(shì)沖突問(wèn)題,自定義一個(gè)ScrollView,ArtScrollView,并將滑動(dòng)手勢(shì)的響應(yīng)傳遞到最下層的scrollview,
返回YES,則可以多個(gè)手勢(shì)一起觸發(fā)方法,返回NO則為互斥(比如外層UIScrollView名為mainScroll內(nèi)嵌的UIScrollView名為subScroll,當(dāng)我們拖動(dòng)subScroll時(shí),mainScroll是不會(huì)響應(yīng)手勢(shì)的(多個(gè)手勢(shì)默認(rèn)是互斥的),當(dāng)下面這個(gè)代理返回YES時(shí),subScroll和mainScroll就能同時(shí)響應(yīng)手勢(shì),同時(shí)滾動(dòng),這符合我們這里的需求)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
viewController中的所有代碼,這里可以忽略,demo中有全部的實(shí)現(xiàn)
#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
#define SCREEN_HEIGHT ([[UIScreen mainScreen] bounds].size.height)
#import "ViewController.h"
//#import "RCDraggableButton.h"
//#import "YZDraggeMoveView.h"
//#import "YZClearUIView.h"
#import "Masonry.h"
//#import "SDWebImage.h"
#import "Toast.h"
#import "ArtScrollView.h"
#import "MJRefresh.h"
@interface ViewController ()<UIScrollViewDelegate,UITableViewDataSource,UITableViewDelegate>
@property(nonatomic,assign)CGFloat redHeight;
@property(nonatomic,assign)CGFloat blueHeight;
@property(nonatomic,strong)UIView * redView;
@property(nonatomic,strong)UIView * blueView;
@property(nonatomic,strong)ArtScrollView * scrollView;
@property(nonatomic,strong)UIScrollView * scrollInnerView;
@property(nonatomic,strong)NSMutableArray * array;
@property(nonatomic,strong)UITableView * tableView;
@property (nonatomic, assign) BOOL vccanScroll; // 這里的布爾值類似一個(gè)鎖,初始化的默認(rèn)值是YES,當(dāng)用戶拖拽了tableview背后的scrollview并且拖拽到了scrollview的偏移距離大于blueview的時(shí)候vccanScroll值為NO,鎖住了scrollview,不讓scrollview進(jìn)行偏移,不管往上滑動(dòng)還是往下滑動(dòng),并將scrollview的偏移量改為blueview.height.當(dāng)且僅當(dāng)tableView.offset.y < 0的時(shí)候,也就是tableView被進(jìn)行了下拉操作的時(shí)候,這種情況下說(shuō)明tableview已經(jīng)進(jìn)入到了最頂端的位置,這時(shí)候,可以對(duì)scrollview進(jìn)行滑動(dòng)解鎖,也就是把vccanScroll的值再改為YES,這種情況下可以將scrollview可以正常上下滑動(dòng)了
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_vccanScroll = YES;
self.view.backgroundColor = [UIColor whiteColor];
self.redHeight = 180;
self.blueHeight = 200;
self.array = [NSMutableArray arrayWithCapacity:0];
for (int i = 0 ; i < 30; i ++) {
[self.array addObject:[NSString stringWithFormat:@"need + %d",i]];
}
[self.view addSubview:self.redView];
[self.view addSubview:self.scrollView];
self.scrollView.backgroundColor = [UIColor purpleColor];
}
-(UIView *)redView {
if (_redView == nil) {
_redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH , _redHeight)];
_redView.backgroundColor = [UIColor redColor];
}
return _redView;
}
-(UIView *)blueView {
if (_blueView == nil) {
_blueView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, _blueHeight)];
_blueView.backgroundColor = [UIColor blueColor];
UIButton * button = [UIButton buttonWithType:(UIButtonTypeCustom)];
button.backgroundColor = [UIColor whiteColor];
[button setTitle:@"change blueHeight" forState:(UIControlStateNormal)];
[button setTitleColor:[UIColor redColor] forState:(UIControlStateNormal)];
[button addTarget:self action:@selector(changeBlueValue) forControlEvents:(UIControlEventTouchUpInside)];
// [button mas_makeConstraints:^(MASConstraintMaker *make) {
// make.centerX.equalTo(_blueView.mas_centerX);
// make.centerY.equalTo(_blueView.mas_centerY);
// make.width.equalTo(@200);
// make.height.equalTo(@50);
//}];
button.frame = CGRectMake(0, 0, 200, 50);
[_blueView addSubview:button];
}
return _blueView;
}
-(void)changeBlueValue {
if (self.blueHeight == 240) {
self.blueHeight = 200;
}else
{
self.blueHeight = 240;
}
[self changeBindingFrame];
}
-(void)changeBindingFrame {
_scrollView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
_scrollInnerView.frame = CGRectMake(0, 0, SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
_scrollInnerView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
self.blueView.frame = CGRectMake(0, 0, SCREEN_WIDTH, _blueHeight);
self.tableView.frame = CGRectMake(0, _blueHeight, SCREEN_WIDTH, SCREEN_HEIGHT - _redHeight);
}
-(ArtScrollView *)scrollView {
if(_scrollView == nil){
_scrollView = [[ArtScrollView alloc] initWithFrame:CGRectMake(0, _redHeight, SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight))];
_scrollView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
_scrollView.backgroundColor = [UIColor grayColor];
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.delegate = self;
_scrollView.bounces = NO;
[_scrollView addSubview:self.scrollInnerView];
}
return _scrollView;
}
-(UIScrollView *)scrollInnerView {
if (_scrollInnerView == nil) {
_scrollInnerView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight)];
_scrollInnerView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
_scrollInnerView.showsVerticalScrollIndicator = NO;
_scrollInnerView.delegate = self;
_scrollInnerView.backgroundColor = [UIColor yellowColor];
[_scrollInnerView addSubview:self.blueView];
[_scrollInnerView addSubview:self.tableView];
}
return _scrollInnerView;
}
#pragma mark --------- tableView
- (UITableView *)tableView {
if (_tableView == nil) {
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, _blueHeight, SCREEN_WIDTH, SCREEN_HEIGHT - _redHeight) style:(UITableViewStylePlain)];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.rowHeight = 55;
MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadData)];
header.lastUpdatedTimeLabel.hidden = YES;
_tableView.mj_header = header;
_tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
[self loadDataMore];
}];
_tableView.showsVerticalScrollIndicator = NO;
}
return _tableView;
}
-(void)loadData {
[self.tableView.mj_footer endRefreshing];
[self.tableView.mj_header endRefreshing];
[self.view makeToast:@"下拉刷新了一次" duration:1 position:CSToastPositionCenter style:[[CSToastStyle alloc] initWithDefaultStyle]];
}
-(void)loadDataMore {
[self.tableView.mj_footer endRefreshing];
[self.tableView.mj_header endRefreshing];
[self.view makeToast:@"上拉加載了一次" duration:1 position:CSToastPositionCenter style:[[CSToastStyle alloc] initWithDefaultStyle]];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [[UITableViewCell alloc] init];
cell.textLabel.text = self.array[indexPath.row];
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.array.count;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
if (scrollView == self.scrollView) {
CGFloat maxOffsetY = _blueHeight;
if (offsetY >= maxOffsetY) {
scrollView.contentOffset = CGPointMake(0, maxOffsetY);
_vccanScroll = NO;
}else {
if (_vccanScroll == NO) {
scrollView.contentOffset = CGPointMake(0, maxOffsetY);
}
}
}else if(scrollView == self.tableView){
CGPoint point = [scrollView.panGestureRecognizer translationInView:scrollView];
CGFloat taboffsetY = point.y;
if (offsetY < 0) {
_vccanScroll = YES;
}
if (taboffsetY < 0) {
if(self.scrollView.contentOffset.y < _blueHeight){
self.tableView.contentOffset = CGPointZero;
}
} else {
if (offsetY > 0) {
self.scrollView.contentOffset = CGPointMake(0, _blueHeight);
}else if (offsetY < 0){
if (self.scrollView.contentOffset.y > 0 && self.scrollView.contentOffset.y < _blueHeight) {
self.tableView.contentOffset = CGPointZero;
}
}
}
}
}
b.scrollView的滑動(dòng)監(jiān)聽(tīng)
這里需要對(duì)scrollView的滑動(dòng)位置做監(jiān)聽(tīng),不但需要監(jiān)聽(tīng)scrollview的contentOffset.y值,還需要監(jiān)聽(tīng)tableView的contentOffset.y 同時(shí)需要做一些處理,因?yàn)閁ITableView繼承自UIScrollView,所以在tableView設(shè)置delegate時(shí)候,同樣能夠監(jiān)聽(tīng)到tableView的滑動(dòng)位置和狀態(tài)
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>
具體的監(jiān)聽(tīng)方法是scrollViewDidScroll:(UIScrollView *)scrollView,如果tableView和scrollView在同一個(gè)控制器中,可以簡(jiǎn)單的用
scrollView == self.tableView 和scrollView == self.scrollView來(lái)區(qū)分
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
}
c.tableview的上下拉操作的監(jiān)聽(tīng)
蘋(píng)果提供了一個(gè)很好用的方法來(lái)監(jiān)聽(tīng)scrollVIew的手勢(shì)操作,這里可以很方便的判斷出,當(dāng)前用戶對(duì)tableView的操作是上滑還是下滑,便于處理對(duì)應(yīng)的臨界值情況的效果
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint point = [scrollView.panGestureRecognizer translationInView:scrollView];
CGFloat offsetY = point.y;
if (offsetY < 0) {
/// 上滑
} else {
/// 下滑
}
}
d.底部scrollView的實(shí)際可滑動(dòng)狀態(tài)記錄
這里的布爾值類似一個(gè)鎖,初始化的默認(rèn)值是YES,當(dāng)用戶拖拽了tableview背后的scrollview并且拖拽到了scrollview的偏移距離大于blueview.height的時(shí)候vccanScroll值為NO,鎖住了scrollview,不讓scrollview進(jìn)行偏移,不管往上滑動(dòng)還是往下滑動(dòng),并將scrollview的偏移量改為blueview.height.當(dāng)且僅當(dāng)tableView.offset.y < 0的時(shí)候,也就是tableView被進(jìn)行了下拉操作的時(shí)候,這種情況下說(shuō)明tableview已經(jīng)進(jìn)入到了最頂端的位置,這時(shí)候,可以對(duì)scrollview進(jìn)行滑動(dòng)解鎖,也就是把vccanScroll的值再改為YES,這種情況下可以將scrollview可以正常上下滑動(dòng)了
@property (nonatomic, assign) BOOL vccanScroll;
網(wǎng)上搜索的答案都會(huì)稍微有點(diǎn)瑕疵,最后總結(jié)之后完善了一下,結(jié)果見(jiàn)上面的gif
流程監(jiān)聽(tīng)tableView的上下滑中參考:
監(jiān)聽(tīng)tableview滑動(dòng)
----------------- 真是嚼一路辛苦,飲一路汗水??
因?yàn)橹癲emo中viewDidLoad中使用的blueHeight初始化的值200.00沒(méi)有問(wèn)題
但是在實(shí)際接入中因?yàn)閎lueHeight的初始化值是根據(jù)一個(gè)label的高度動(dòng)態(tài)計(jì)算出來(lái)之后在viewDidLoad中賦值,我這里的blueHeight計(jì)算的打印值是225.027這樣的數(shù)據(jù),會(huì)導(dǎo)致scrollview上的blueView上滑出去之后tableView不會(huì)向上滑動(dòng)的bug
排查原因:
雖然blueHeight是225.027.但是在實(shí)際scrollview的- (void)scrollViewDidScroll:(UIScrollView *)scrollView 滑動(dòng)監(jiān)聽(tīng)方法中最大滑動(dòng)距離scrollView.contentOffset.y只能打印到225.00000,這種情況不知道是不是scrollview自身的bug
于是我將初始化的值寫(xiě)成定值225.000,不能下滑的異常情況就消失了
于是我的解決方案,在動(dòng)態(tài)計(jì)算完成blueHeight之后,將計(jì)算的最終值進(jìn)行取整之后再賦值給blueHeight可以避免這個(gè)問(wèn)題.