iOS 使用MJRefresh实现刷新

   MJRefresh是Github上点赞次数最多的刷新控件,本文主要演示如何使用MJRefresh实现刷新和MJRefresh的内部实现原理。

1.使用MJRefresh实现刷新——UIScrollView+MJRefresh.h

  使用MJRefresh实现刷新非常简单,主要分3步:1.导入框架。2.定义一个Scrollview;2.为Scrollview添加刷新控件。

// 1.导入框架
#import "MJRefresh.h"

@interface ViewController()
@property (nonatomic, strong) UITableView * tableView;
@end;

@implementation ViewController;

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 2.定义一个UITableView
    self.tableView = [[UITableView alloc] initWithFrame: self.view.bounds];
    [self.view addSubview:self.tableView];
    
    // 3.添加刷新控件
    // 下拉刷新
    __weak typeof(self)  weakSelf = self;
    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        // 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 结束刷新
            [weakSelf.tableView.mj_header endRefreshing];
        });
    }];
    
    // 上拉刷新
    self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
        // 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 结束刷新
            [weakSelf.tableView.mj_footer endRefreshing];
        });
    }];
  }

  MJRefresh定义了一个类别UIScrollView+MJRefresh.h,利用类别为Tableview增加了两个属性mj_header和mj_footer。

#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"

@class MJRefreshHeader, MJRefreshFooter;

@interface UIScrollView (MJRefresh)
/** 下拉刷新控件 */
@property (strong, nonatomic) MJRefreshHeader *mj_header;
/** 上拉刷新控件 */
@property (strong, nonatomic) MJRefreshFooter *mj_footer;

@end

  由于在类别中添加属性,并不能在编译期间自动添加成员变量、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);
}

   MJRefreshHeaderKey这是定义了一个静态字符,用它的地址作为存储mj_header的key,用最小的存储空间实现了key的定义。
   通过上文中的代码我们可以看出,所谓的添加下拉刷新控件就是在Scrollview上加了一个View。下文我们将介绍MJRefresh是怎么定义这个View(mj_header)的。

2.继承关系

  MJRefresh总共分四层,我将从最底层MJRefreshComponent开始介绍MJRefresh的实现原理。


继承结构图.png

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];
    
    // 添加到父视图
    //  如果newSuperview为nil,表示从父视图中移除
    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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容