I'm Terrence

仿支付宝tableView

支付宝首页tableView的大概有如下几个特点:

  1. 下拉tableVIew,”headerView”不随之下滑
  2. 上拉tableView,”headerView”随之下滑
  3. 不够力,自动滑回

而单纯的tableHeaderView,sectionHeader啊,是无法实现这两个功能的。
以下是我的实现思路:
1 创建一个View,作为“headerView”

- (void)initHeaderView
{
    _headerView = [[MyHeaderView alloc] initWithFrame:CGRectMake(0, 64, DRScreenWidth, HeaderViewHeight)];
    _headerView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:_headerView];
}

2 创建tableView,将其contentInset设在headerView下沿

- (void)initTableView
{
    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, DRScreenWidth, DRScreenHeight) style:UITableViewStylePlain];
    _tableView.dataSource = self;
    _tableView.delegate = self;
    _tableView.backgroundColor = [UIColor purpleColor];
    _tableView.contentInset = UIEdgeInsetsMake(HeaderViewHeight, 0, 0, 0);
    [_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
    [self.view addSubview:_tableView];
}

看到这里,可能有的读者会有疑问,
_tableView.contentInset = UIEdgeInsetsMake(HeaderViewHeight, 0, 0, 0);
这句有何意义呢?干嘛不直接把tableView的frame放在headerView下面就好了?
其实这句大有玄机。我们继续看HeaderView里面究竟实现了什么。

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initBtn];
        [self addObserver];
    }
    return self;
}
- (void)addObserver
{
    [self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
}

#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    CGRect rect = [change[NSKeyValueChangeNewKey] CGRectValue];
    NSLog(@"head view height = %f",rect.size.height);
    CGFloat height = rect.size.height;
    if (height < visibleOffset) {
        _btn.hidden = YES;
        _aView.hidden = YES;
    } else {
        _btn.hidden = NO;
        _aView.hidden = NO;
    }
}

这里的kvo主要是为了通过监听frame的变化来控制subView的显影。
而下面这句才能解释上面那个问题:

#pragma mark - touch
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *view = [super hitTest:point withEvent:event];
    if ([view isKindOfClass:[self class]]) {

        return nil;
    }
    return view;
}

重写了hittest,反正让用户怎么也摸不到自身!就是为了解决当触摸点在headerView上面的时候,hitTest返回对象是跌在他下面的tableView,来让tableView响应滑动。
好了,回到正题。
我们好像还没实现初衷的3个特性。
第1,2点也是通过KVO来改变headerView的height或origin.y实现的。
首先看看改变height:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"contentOffset"]) {
        CGPoint offset = [change[NSKeyValueChangeNewKey] CGPointValue];
        NSLog(@"KVO:%f",offset.y);

        if (offset.y > -(HeaderViewHeight +navigationBarHeightPlusStatusBarHeight) && offset.y < -navigationBarHeightPlusStatusBarHeight) {
            CGRect frame = _headerView.frame;
            frame.size.height = - navigationBarHeightPlusStatusBarHeight - offset.y;
            _headerView.frame = frame;
        } else if (offset.y <= -(HeaderViewHeight +navigationBarHeightPlusStatusBarHeight)) {
//tableView下滑,headerView保持不变
            _headerView.frame = CGRectMake(0, navigationBarHeightPlusStatusBarHeight, DRScreenWidth, HeaderViewHeight);
        } else if (offset.y >= -navigationBarHeightPlusStatusBarHeight) {
//tableView上滑,直到两者上沿重叠,headerView高度设0
            _headerView.frame = CGRectMake(0, navigationBarHeightPlusStatusBarHeight, DRScreenWidth, 0);
        }
    }
}

上面的逻辑很清楚了,主要是通过改变headerView的高度,来造成headerView随tableView滑动的错觉。

当然,也可以改变HeaderView的y来实现。

看代码:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"contentOffset"]) {
        CGPoint offset = [change[NSKeyValueChangeNewKey] CGPointValue];
        NSLog(@"KVO:%f",offset.y);
        if (offset.y > -(HeaderViewHeight +navigationBarHeightPlusStatusBarHeight) && offset.y < -navigationBarHeightPlusStatusBarHeight) {
            CGRect frame = _headerView.frame;
            frame.origin.y  = fabs(offset.y) - HeaderViewHeight;
            _headerView.frame = frame;
        } else if (offset.y <= -(HeaderViewHeight +navigationBarHeightPlusStatusBarHeight)) {
            _headerView.frame = CGRectMake(0, navigationBarHeightPlusStatusBarHeight, DRScreenWidth, HeaderViewHeight);
        } else if (offset.y >= -navigationBarHeightPlusStatusBarHeight) {
            _headerView.frame = CGRectMake(0, -HeaderViewHeight+navigationBarHeightPlusStatusBarHeight, DRScreenWidth, HeaderViewHeight);
        }
    }
}

最后,就是第3个特性了:

#pragma mark - scrollview delegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
//    NSLog(@"willENdDragging:%f",scrollView.contentOffset.y);
    if (targetContentOffset->y > -132 && targetContentOffset->y < -navigationBarHeightPlusStatusBarHeight) {
        *targetContentOffset = CGPointMake(0, -navigationBarHeightPlusStatusBarHeight);
    } else if (targetContentOffset->y <= -132 && targetContentOffset->y > -(navigationBarHeightPlusStatusBarHeight + HeaderViewHeight)) {
        *targetContentOffset = CGPointMake(0, -(navigationBarHeightPlusStatusBarHeight + HeaderViewHeight));
    }
}

实现这个代理就👌。关于这个代理详细可看我上一篇文章UITableView串烧