美文网首页UI布局
scrollview在上下滑动时,改变视图高度

scrollview在上下滑动时,改变视图高度

作者: 学习无底 | 来源:发表于2019-02-22 22:21 被阅读0次

一、需求

之前遇到一个需求是,要求在scrollview在上下滑动时,scrollview显示区域高度变化。向上滑动时——拉高,向下滑动时——恢复。

二、项目中的实现

由于项目中要实现的几个页面都用到了自定义的SITableView,刚好就在自定义的SITableView中实现了

1.向外传递滑动

有以下两种方案

  • 1)协议 如果是多级或者是跨层的,不好要拿到响应者,同时如果视图层级改变的话,也需要改变赋值响应者的代码。可以精准的传递事件给需要改变的视图,也可以自定义滑动距离,虽然实际用处不大。本次实现用的是协议。

还有一种思路是,定义一个BOOL值,标识是否开启滑动改变传递,然后向上查找第一个能响应协议的responder,把它记录为委托者。

  • 2)通知
    传递数据方便,但不能自定义滑动距离。并且如果多个界面都注册了的话,接受到通知要进行判断,判断要调整大小的视图是不是在屏幕上。如果页面复用过程中,导致某个视图加载完成后,视图层级中有父视图和子视图都能响应通知,会出现问题,虽然出现的可能性不大。

协议的代码如下:

@class SITableView;
@protocol SITableViewUpDownScrollProtocol <NSObject>
//告诉外部对象,是向上还是向下滑动
- (void)tableView:(SITableView *)tableView updownScroll:(BOOL)isUp;
@optional
// 是否要自定义判断移动的距离
- (CGFloat)tableViewMinMoveDistance:(SITableView *)tableView;

@end

滑动方向是向上还是向下,应该用枚举的,偷懒了

2.SITableView中的主要变动

scrollViewDidScroll :方法中,判断contentOffset.y的变化,与前一刻的差值作为上下的依据。
要考虑以下几个问题:

1.只有当用户手动滑动时,才改变视图高度。需要记录是不是手动拖拽,虽然,scrollview有dragging,但不够精确,在手松开减速时依然是YES,不符合要求
2.需要记录初始值,来做参考
3.要移动一定距离,才能判断是否执行回调,避免有时手触碰屏幕引起的误操作
4.拦截的方法,不能影响原方法的调用

  • 1.增加私有属性,协助判断
//是不是手动移动
@property (nonatomic, assign, getter=isManuallyMoving) BOOL manuallyMoving;
//开始手动移动时contentOffset.y值
@property (nonatomic, assign) CGFloat startOffsetY;
//tableview的新的delegate,用来判断是否要拦截
@property (nonatomic, strong) SITableViewWeakProxy *weakProxy;
//默认最小移动距离 5
@property (nonatomic, assign) CGFloat minMoveDistance;
  • 2.实现
#pragma mark - 上下滑动回调
//调用有参无返回值的方法
- (void)callTableViewUpDownScrollProtocol:(BOOL)isUp {
    
    if (self.upDownScrollDelegate == nil) {
        return;
    }
    // 1. 根据方法创建签名对象sig
    NSMethodSignature *sig = [self.upDownScrollDelegate methodSignatureForSelector:@selector(tableView:updownScroll:)];
    
    // 2. 根据签名对象创建调用对象invocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    
    // 3. 设置调用对象的相关信息
    invocation.target = self.upDownScrollDelegate;
    invocation.selector = @selector(tableView:updownScroll:);
 
    SITableView *tempSelf = self;
    // 参数必须从第2个索引开始,因为前两个已经被target和selector使用
    [invocation setArgument:&tempSelf atIndex:2];
    [invocation setArgument:&isUp atIndex:3];
    
    // 4. 调用方法
    [invocation invoke];
    
}
#pragma mark - 拦截的协议方法

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    self.manuallyMoving = NO;
    //不影响原有的逻辑,回调原来delegate的方法
    if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
        [self.weakProxy.originTarget scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
    }
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    self.manuallyMoving = YES;
    self.startOffsetY = scrollView.contentOffset.y;

    //不影响原有的逻辑,回调原来delegate的方法
    if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
        [self.weakProxy.originTarget scrollViewWillBeginDragging:scrollView];
    }
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (self.isManuallyMoving) {
        if (self.startOffsetY < scrollView.contentOffset.y - self.minMoveDistance) {
          
            [self callTableViewUpDownScrollProtocol:YES];
        }
        if (self.startOffsetY > scrollView.contentOffset.y + self.minMoveDistance) {
      
            [self callTableViewUpDownScrollProtocol:NO];
        }
    }
    self.startOffsetY = scrollView.contentOffset.y;
    
    //不影响原有的逻辑,回调原来delegate的方法
    if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [self.weakProxy.originTarget scrollViewDidScroll:scrollView];
    }
}
#pragma mark - setter与getter
- (void)setDelegate:(id<UITableViewDelegate>)delegate {
    self.weakProxy.originTarget = delegate;
    [super setDelegate:self.weakProxy];
}

- (void)setUpDownScrollDelegate:(id<SITableViewUpDownScrollProtocol>)upDownScrollDelegate {
    if (upDownScrollDelegate && [upDownScrollDelegate conformsToProtocol:@protocol(SITableViewUpDownScrollProtocol)] && [upDownScrollDelegate respondsToSelector:@selector(tableView:updownScroll:)]) {
        _upDownScrollDelegate = upDownScrollDelegate;
        
        if ([upDownScrollDelegate respondsToSelector:@selector(tableViewMinMoveDistance:)]) {
            self.minMoveDistance = [upDownScrollDelegate tableViewMinMoveDistance:self];
        }
    }
    if (upDownScrollDelegate == nil) {
        _upDownScrollDelegate = upDownScrollDelegate;
    }
}
- (SITableViewWeakProxy *)weakProxy {
    if (_weakProxy == nil) {
        _weakProxy = [SITableViewWeakProxy alloc];
        _weakProxy.interceptionTarget = self;
    }
    return _weakProxy;
}

注意 [SITableViewWeakProxy alloc];这样写没有错,它没有init方法。

3.SITableViewWeakProxy的实现

为什么要做的这样复杂,
不直接把delegate设为自己,用一个属性记录原始的delegate呢?如果这样做了,tableview的UITableViewDelegate协议中的其他方法呢,怎么把协议中的方法传递给原始的delegate呢。实现所有的方法,在里面判断原始的delegate是否实现了,原始未实现的但方法需要返回值的你怎么操作。如果里面后面新增了方法怎么办,一个个版本维护更新?
走消息转发,UITableViewDelegate协议中的很多方法是optional,会调用respondsToSelector来判断是否协议中某个方法,这个地方的响应者是SITableView的实例,它明显没有实现协议中的其他方法,就无法调用了。当然也可以重写respondsToSelector,但怎么判断这个sel是UITableViewDelegate协议中的方法,一个个列出来

使用SITableViewWeakProxy,是实例不会在方法列表中查找,而是直接走消息转发,效率高,也安全,不用担心其他的影响。包括respondsToSelector方法也是走的消息转发,所以在具体的实现中,要特殊处理,判断这个方法的参数,如果是要拦截的三个方法,就要拦截。

@interface SITableViewWeakProxy : NSProxy <UITableViewDelegate>

@property (nonatomic, weak) NSObject<UITableViewDelegate> *originTarget;
@property (nonatomic, weak) NSObject *interceptionTarget;

@end

@implementation SITableViewWeakProxy

//- (id)forwardingTargetForSelector:(SEL)selector {
//    NSLog(@"%@...%@", self, NSStringFromSelector(selector));
//    for (NSString *interceptionSEL in self.interceptionSELS) {
//        if (NSSelectorFromString(interceptionSEL) == selector) {
//            return _interceptionTarget;
//        }
//    }
//    return _originTarget;
//}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.originTarget methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    //这个很重要,SITableViewWeakProxy不能响应respondsToSelector方法,只是做转发,所以需要特殊判断下
    if (self.interceptionTarget && invocation.selector == @selector(respondsToSelector:)) {
        SEL parameterSel;
        [invocation getArgument:&parameterSel atIndex:2];
        
        if ([self interceptionSelector:parameterSel]) {
            [invocation invokeWithTarget:self.interceptionTarget];
            return;
        }
      
    }else if (self.interceptionTarget && [self interceptionSelector:invocation.selector]) {
        [invocation invokeWithTarget:self.interceptionTarget];
        return;
    }
    //不需要拦截,直接调用原来的delegate
    [invocation invokeWithTarget:self.originTarget];
}
//只需要拦截这三个方法,不需其他方法
- (BOOL)interceptionSelector:(SEL)sel {
    return  sel == @selector(scrollViewDidScroll:) || sel == @selector(scrollViewDidEndDragging:willDecelerate:) || sel == @selector(scrollViewWillBeginDragging:);
}

@end

三、scrollview分类的实现

为了更多通用性,应该以scrollview category的方式实现,这就要用到Runtime了。

未完待续

相关文章

网友评论

    本文标题:scrollview在上下滑动时,改变视图高度

    本文链接:https://www.haomeiwen.com/subject/fvpyyqtx.html