需求效果:
2j.gifdemo拉取地址:demo
最简单的实现方式是,放一个tableview,这个tableview有一个headView,这个headview就是上图所示的蓝色头部的View,但是这样做的结果是,在tableView下拉刷新数据的时候,刷新动画会出现在headView的上方,这样看着就异常令人难受了,当时为了节省时间,就是这么实现的.现在有充足的时间的情况下,是不允许有瑕疵的.
于是就有了UIScrollView + UITableView的组合实现方案;
组合的方案实现遇到的问题
UIScrollView和UITableView的组合使用问题整理:
1.手势冲突
2.tableview和scrollview一起滑动
3.scrollview滑动到底部之后,tableview上拉没反应
4.scrollview滑出了指定的头部区域之后下拉没反应
5.tableview的下拉刷新无效
a.手势事件的穿透
为解决手势冲突问题,自定义一个ScrollView,ArtScrollView,并将滑动手势的响应传递到最下层的scrollview,
返回YES,则可以多个手势一起触发方法,返回NO则为互斥(比如外层UIScrollView名为mainScroll内嵌的UIScrollView名为subScroll,当我们拖动subScroll时,mainScroll是不会响应手势的(多个手势默认是互斥的),当下面这个代理返回YES时,subScroll和mainScroll就能同时响应手势,同时滚动,这符合我们这里的需求)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
viewController中的所有代码,这里可以忽略,demo中有全部的实现
#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; // 这里的布尔值类似一个锁,初始化的默认值是YES,当用户拖拽了tableview背后的scrollview并且拖拽到了scrollview的偏移距离大于blueview的时候vccanScroll值为NO,锁住了scrollview,不让scrollview进行偏移,不管往上滑动还是往下滑动,并将scrollview的偏移量改为blueview.height.当且仅当tableView.offset.y < 0的时候,也就是tableView被进行了下拉操作的时候,这种情况下说明tableview已经进入到了最顶端的位置,这时候,可以对scrollview进行滑动解锁,也就是把vccanScroll的值再改为YES,这种情况下可以将scrollview可以正常上下滑动了
@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的滑动监听
这里需要对scrollView的滑动位置做监听,不但需要监听scrollview的contentOffset.y值,还需要监听tableView的contentOffset.y 同时需要做一些处理,因为UITableView继承自UIScrollView,所以在tableView设置delegate时候,同样能够监听到tableView的滑动位置和状态
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>
具体的监听方法是scrollViewDidScroll:(UIScrollView *)scrollView,如果tableView和scrollView在同一个控制器中,可以简单的用
scrollView == self.tableView 和scrollView == self.scrollView来区分
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
}
c.tableview的上下拉操作的监听
苹果提供了一个很好用的方法来监听scrollVIew的手势操作,这里可以很方便的判断出,当前用户对tableView的操作是上滑还是下滑,便于处理对应的临界值情况的效果
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint point = [scrollView.panGestureRecognizer translationInView:scrollView];
CGFloat offsetY = point.y;
if (offsetY < 0) {
/// 上滑
} else {
/// 下滑
}
}
d.底部scrollView的实际可滑动状态记录
这里的布尔值类似一个锁,初始化的默认值是YES,当用户拖拽了tableview背后的scrollview并且拖拽到了scrollview的偏移距离大于blueview.height的时候vccanScroll值为NO,锁住了scrollview,不让scrollview进行偏移,不管往上滑动还是往下滑动,并将scrollview的偏移量改为blueview.height.当且仅当tableView.offset.y < 0的时候,也就是tableView被进行了下拉操作的时候,这种情况下说明tableview已经进入到了最顶端的位置,这时候,可以对scrollview进行滑动解锁,也就是把vccanScroll的值再改为YES,这种情况下可以将scrollview可以正常上下滑动了
@property (nonatomic, assign) BOOL vccanScroll;
网上搜索的答案都会稍微有点瑕疵,最后总结之后完善了一下,结果见上面的gif
流程监听tableView的上下滑中参考:
监听tableview滑动
----------------- 真是嚼一路辛苦,饮一路汗水💦
因为之前demo中viewDidLoad中使用的blueHeight初始化的值200.00没有问题
但是在实际接入中因为blueHeight的初始化值是根据一个label的高度动态计算出来之后在viewDidLoad中赋值,我这里的blueHeight计算的打印值是225.027这样的数据,会导致scrollview上的blueView上滑出去之后tableView不会向上滑动的bug
排查原因:
虽然blueHeight是225.027.但是在实际scrollview的- (void)scrollViewDidScroll:(UIScrollView *)scrollView 滑动监听方法中最大滑动距离scrollView.contentOffset.y只能打印到225.00000,这种情况不知道是不是scrollview自身的bug
于是我将初始化的值写成定值225.000,不能下滑的异常情况就消失了
于是我的解决方案,在动态计算完成blueHeight之后,将计算的最终值进行取整之后再赋值给blueHeight可以避免这个问题.
网友评论