iOS开发技能集锦iOS细节知识收录

iOS MJRefresh之 Header的实现

2018-11-16  本文已影响219人  豆丶浆油条

   MJRefresh是Github上点赞次数最多的刷新控件,本文主要分析MJRefresh的实现原理,学习作者的思路,具体怎么使用可以去看MJRefresh得Demo。学习框架主要是学习作者的实现思路,本文主要是沿着框架的继承关系开始讲起。

1.为ScrollView添加MJRefreshHeader(UIScrollView+MJRefresh的分析)

   MJRefresh定义了一个CategoryUIScrollView+MJRefresh.h,为UIScrollView增加了1个属性:

/** 下拉刷新控件 */
@property (strong, nonatomic) MJRefreshHeader *mj_header;

   我们只要为这个属性赋值就可以实现下拉刷新功能。

    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshListData)];

   MJRefreshNormalHeader有两种初始化方法,两种的区别仅仅是回调方式不同,这两个方法在MJRefreshHeader中实现;

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;

  由于在类别中添加属性,并不能在编译期间自动添加成员变量、set和get方法(因为类的结构已经确定,在类别中再添加成员变量会影响已经添加的成员变量的存储。),所以我们要objc_setAssociatedObjectobjc_getAssociatedObject的两个方法自己实现。

#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
    if (mj_header != self.mj_header) {
        // 删除旧的,添加新的
        [self.mj_header removeFromSuperview];
        [self insertSubview:mj_header atIndex:0];
        
        // 存储新的
        [self willChangeValueForKey:@"mj_header"]; // KVO
        objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                 mj_header, OBJC_ASSOCIATION_ASSIGN);
        [self didChangeValueForKey:@"mj_header"]; // KVO
    }
}

- (MJRefreshHeader *)mj_header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

2.继承关系

继承结构图.png

   MJRefreshComponent是Header和Footer的基类,所有我们从这个类开始分析。

3.MJRefreshComponent

   从这个类我们可以知道,MJRefreshHeader的实现主要是利用KVO监听ScrollView的contentOffset,contentInset,contentSize的变化来确定ScrollView的刷新状态(MJRefreshState)。在- (void)setState:(MJRefreshState)state中实现ScrollView的各个状态下的UI变化和调用回调方法。

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // 旧的父控件移除监听
    [self removeObservers];
    
    // 添加到父视图
    if (newSuperview) { // 新的父控件
        // 设置宽度
        self.mj_w = newSuperview.mj_w;
        // 设置位置
        self.mj_x = -_scrollView.mj_insetL;
        
        // 记录UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 设置永远支持垂直弹簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 记录UIScrollView最开始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加监听
        [self addObservers];
    }
}

#pragma mark - KVO监听
- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到这些情况就直接返回
    if (!self.userInteractionEnabled) return;
    
    // 这个就算看不见也需要处理
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    // 看不见
    if (self.hidden) return;
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [self scrollViewPanStateDidChange:change];
    }
}
// 当ContentOffset变化的时候调用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 当ContentSize变化的时候调用
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

   - (void)willMoveToSuperview:(nullable UIView *)newSuperview;这个方法在View添加到父视图(addSubView)和从父视图中移除(removeFromSuperView)都会调用。当newSuperview存在的时候,代表的是添加到父视图,此时添加KVO,并为每个属性设置了回调方法。 MJRefreshComponent的子类MJRefreshHeader实现- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change;,根据ContentOffset的变化来确定ScrollView的刷新状态。所以,我们研究MJRefresh刷新的实现,就研究 MJRefreshComponent的各个子类在这个方法中的实现即可。
   此外还定义了两个方法- (void)prepare;- (void)placeSubviews;,都需要在子类中实现。在- (void)prepare;方法中,主要是进行数据的初始化,- (void)placeSubviews;中主要是实现控件的摆放,- (void)prepare;先于- (void)placeSubviews;执行。

4.MJRefreshHeader

  在MJRefreshHeader中实现了- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法,确定了ScrollView的刷新状态,就是确定了这个属性@property (assign, nonatomic) MJRefreshState state;的值。
  分两种情况,一种是ScrollView正在刷新(MJRefreshStateRefreshing),通过修改ScrollView的contentInset,增加了contentInset.top的值,增加了MJRefreshHeader的高度,让MJRefreshHeader完全显示出来,以达到Hearder悬停效果;另一种是其他状态,此时来判断什么时候开始刷新。

刷新时的Scrollview.png
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 此时ScrollView正在刷新
    if (self.state == MJRefreshStateRefreshing) {
        // 如果视图还没有添加到KeyWindow上,返回。
        if (self.window == nil) return;
        
        // sectionheader停留解决
        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
        insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
        self.scrollView.mj_insetT = insetT;
        
        self.insetTDelta = _scrollViewOriginalInset.top - insetT;
        return;
    }
    
    // 跳转到下一个控制器时,contentInset可能会变
     _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 当前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 头部控件刚好出现的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滚动到看不见头部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通和即将刷新的临界点
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
    
    if (self.scrollView.isDragging) { // 如果正在拖拽
        self.pullingPercent = pullingPercent;
        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
            // 转为即将刷新状态
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 转为普通状态
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开
        // 开始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

   MJRefreshComponent的每个子类都实现了- (void)setState:(MJRefreshState)state这个方法。MJRefreshHeader实现了通过改变和恢复inset和offset来实现Scrollview的刷新效果。当state == MJRefreshStateIdle的时候,恢复inset。当state == MJRefreshStateRefreshing的时候,增加inset.top一个MJRefreshHeader的高度,并且滚动到顶部。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 保存刷新时间
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢复inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // 自动调整透明度
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
            
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    } else if (state == MJRefreshStateRefreshing) {
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滚动区域top
                self.scrollView.mj_insetT = top;
                // 设置滚动位置
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = -top;
                [self.scrollView setContentOffset:offset animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

  这个类还定义了两个构造方法,在构造方法中保存了头部刷新的回调。

 + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;
 + (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;

5.MJRefreshStateHeader

   设置lastUpdatedTimeLabel和stateLabel的位置以及显示的内容。这个类非常简单,没有什么可讲的。这个类里的- (void)setState:(MJRefreshState)state;实现的是这两个Label显示的内容。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 设置状态文字
    self.stateLabel.text = self.stateTitles[@(state)];
    
    // 重新设置key(重新显示时间)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshStateHeader.jpg

6. MJRefreshNormalHeader(MJRefreshGifHeader)

   MJRefreshNormalHeader刷新的时候是一个菊花,MJRefreshGifHeader可以自定义MJRefreshHeader各个状态的动画,其实两个的实现思路大同小异,只是一个是UIActivityIndicatorView,一个是UIImageView,两者的位置都是相同的。剩下的就是动画效果的是实现了,这应该属于最基本的动画了,在这里我就不讲了,有不懂的可以自行百度。
   此外还有一个箭头(arrowView),这个箭头就是UIImageView,它和UIActivityIndicatorView(Gif)交替展示,就是一个隐藏,一个就显示。这个类里的- (void)setState:(MJRefreshState)state;实现的是arrowView和loadingView的动画效果。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) {
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果执行完动画发现不是idle状态,就直接返回,进入其他状态
                if (self.state != MJRefreshStateIdle) return;
                
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                self.arrowView.hidden = NO;
            }];
        } else {
            [self.loadingView stopAnimating];
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity;
            }];
        }
    } else if (state == MJRefreshStatePulling) {
        [self.loadingView stopAnimating];
        self.arrowView.hidden = NO;
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
        }];
    } else if (state == MJRefreshStateRefreshing) {
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

   MJRefresh的分层非常清晰,一目了然,一些个功能的实现非常巧妙,而且大部分还有注释,这是了解UIScrollView以及刷新机制的非常好的一个框架。下一篇我将分析MJRefreshFooter

上一篇下一篇

猜你喜欢

热点阅读