SVPullToRefresh 代码解析

SVPullToRefresh 的接口设计的很简明

@interface UIScrollView (SVPullToRefresh)

- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler;
- (void)triggerPullToRefresh;

@property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView;
@property (nonatomic, assign) BOOL showsPullToRefresh;

@end

只暴漏了两个方法及两个属性。
只需一行代码就可以为scrollView添加下拉刷新功能。

[_tableView addPullToRefreshWithActionHandler:^{
      //代码块
}];

接下来我们来说下SVPullToRefresh实现的原理,及源码的一些注意细节。

默认情况下,当scrollView下拉时,显示一个View,pullToRefresh... ,这个View在调用addPull...时就被添加到了ScrollView中。

- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler{
    
    if(!self.pullToRefreshView) {
        CGFloat yOrigin = -SVPullToRefreshViewHeight;
        
        CGFloat width = [UIScreen mainScreen].bounds.size.width;
        SVPullToRefreshView *view = [[SVPullToRefreshView alloc] initWithFrame:CGRectMake(0, yOrigin, width, SVPullToRefreshViewHeight)];
        view.pullToRefreshActionHandler = actionHandler;
        view.scrollView = self;
        [self addSubview:view];
        
        view.originalTopInset = self.contentInset.top;
        view.originalBottomInset = self.contentInset.bottom;
        self.pullToRefreshView = view;
        self.showsPullToRefresh = YES;
    }
}

KVO - ( NSKeyValueObserving )(其他篇章会讲,或者自行搜索知识点)

我们知道 ScrollView 有几个重要的属性:

CGSize contentSize                 是scrollview可以滚动的区域
CGPoint contentOffset              是scrollview当前显示区域顶点相对于frame顶点的偏移量
UIEdgeInsets contentInset         是scrollview的contentview的顶点相对于scrollview的位置

当scrollView 滚动时,contentOffSet的值会改变,就是利用了这点,以及 KVO 的observer(默认情况下showsPullToRefresh = YES 代码addPull...中)


- (void)setShowsPullToRefresh:(BOOL)showsPullToRefresh {
    
    self.pullToRefreshView.hidden = !showsPullToRefresh;
    
    if(showsPullToRefresh) {
        if (!self.pullToRefreshView.isObserving) {
            [self addObserver:self.pullToRefreshView forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
            [self addObserver:self.pullToRefreshView forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
            [self addObserver:self.pullToRefreshView forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
            self.pullToRefreshView.isObserving = YES;
            
            CGFloat yOrigin = -SVPullToRefreshViewHeight;
            self.pullToRefreshView.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight);
        }
    }
    else {
        if (self.pullToRefreshView.isObserving) {
            [self removeObserver:self.pullToRefreshView forKeyPath:@"contentOffset"];
            [self removeObserver:self.pullToRefreshView forKeyPath:@"contentSize"];
            [self removeObserver:self.pullToRefreshView forKeyPath:@"frame"];
            [self.pullToRefreshView resetScrollViewContentInset];
            self.pullToRefreshView.isObserving = NO;
        }
    }
}

由于observer被指定给了pullToRefreshView,所以在view中找到相应的observe方法。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([keyPath isEqualToString:@"contentOffset"])
        [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]];
    else if([keyPath isEqualToString:@"contentSize"]) {
        [self layoutSubviews];
        
        CGFloat yOrigin = -SVPullToRefreshViewHeight;
        self.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight);
    }
    else if([keyPath isEqualToString:@"frame"])
        [self layoutSubviews];
}

当contentOffset的值改变(及下拉时),就会调用相应的 scrollViewDidScroll 方法,,来实现各种状态的显示。

- (void)scrollViewDidScroll:(CGPoint)contentOffset {
    if(self.state != SVPullToRefreshStateLoading) {
        CGFloat scrollOffsetThreshold = self.frame.origin.y-self.originalTopInset;
        
        if(!self.scrollView.isDragging && self.state == SVPullToRefreshStateTriggered)
            self.state = SVPullToRefreshStateLoading;
        else if(contentOffset.y < scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped)
            self.state = SVPullToRefreshStateTriggered;
        else if(contentOffset.y >= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped)
            self.state = SVPullToRefreshStateStopped;
    } else {
        CGFloat offset = MAX(self.scrollView.contentOffset.y * -1, 0.0f);
        offset = MIN(offset, self.originalTopInset + self.bounds.size.height);
        UIEdgeInsets contentInset = self.scrollView.contentInset;
        self.scrollView.contentInset = UIEdgeInsetsMake(offset, contentInset.left, contentInset.bottom, contentInset.right);
    }
}

基本流程如上:
下面来说一下代码中德一些细节点:
(时间到了,要先睡觉去了,未完待续....)

objc_setAssociatedObject(

- (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView {
    [self willChangeValueForKey:@"SVPullToRefreshView"];
    objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,
                             pullToRefreshView,
                             OBJC_ASSOCIATION_ASSIGN);
    [self didChangeValueForKey:@"SVPullToRefreshView"];
}
未完待续

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 160,250评论 24 690
  • 《盲侠大律师》作为TVB首次与国内影视公司联合制作的网剧,与TVB同步播出。今天看了前两集,不错。 剧中无论人物对...
    默默文文阅读 474评论 0 1