MJRefresh可能是大家用得最多的一个框架了吧.基本上就没几个App(游戏除外).没有UITableView.有UITableView的地方可能没有上拉加载,但是十有八九就有下拉刷新.
本篇文章让我们来研究一下MJRefresh的实现原理.
MJRefresh框架内文件结构
偷懒用MindNode画的,希望别介意.
首先.我们得搞清楚.UITableView的下拉刷新的那个肯定是一个UIView可以直接放在UITableView上.然后呢.平时我们并不能看到它.当我们把UITableView往下拉之后才可能看到,并且进入正在刷新的状态时会让上拉的header/上拉加载的footer"悬停"一会儿.直到刷新状态完毕.
假如你是MJRefresh的作者.想要做到这一步应该怎么做一般来说.大部分人的思维肯定是子类化一个UITableView.然后重写scrollViewDidScroll.然后算位置.然而这样的话我们集成的话就非常不方便了.所以正确的处理应该是通过某种手段获取到内部的滚动方法.不管是+load的方法交换也好.还是其他什么的也好.这样就避免了子类化集成不便的问题.
MJRefresh的解决方案:
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);
// `pinchGestureRecognizer` will return nil when zooming is disabled.
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);
// `directionalPressGestureRecognizer` is disabled by default, but can be enabled to perform scrolling in response to up / down / left / right arrow button presses directly, instead of scrolling indirectly in response to focus updates.
@property(nonatomic, readonly) UIGestureRecognizer *directionalPressGestureRecognizer API_DEPRECATED("Configuring the panGestureRecognizer for indirect scrolling automatically supports directional presses now, so this property is no longer useful.", tvos(9.0, 11.0));
我们知道,UITableView的滚动是靠着手势来做的.就是上面截取的UIScrollView内部的panGestureRecognizer.第二个捏合是用来做缩放.第三个手势是应该是TV OS用来做按压力度什么的.这两个咱们目前先忽略掉.
因为scrollView的滚动是由手势来触发的.所以MJ使用了一个基类MJRefreshComponment来监听手势.
- (void)willMoveToSuperview:(UIView *)newSuperview {[super willMoveToSuperview:newSuperview];// 如果不是UIScrollView,不做任何事情if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;// 旧的父控件移除监听[self removeObservers];if (newSuperview) { // 新的父控件// 记录UIScrollView_scrollView = (UIScrollView *)newSuperview;// 设置宽度self.mj_w = _scrollView.mj_w;// 设置位置self.mj_x = -_scrollView.mj_insetL;// 设置永远支持垂直弹簧效果_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的改变、contentSize的改变以及手势的改变传入到了这三个方法里面,我截取.m里面的是告诉大家这里只是空实现.具体处理是子类去处理.这里空实现可以防止子类忘记实现导致的崩溃23333
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
言归正传.先说说- (void)willMoveToSuperview:(UIView *)newSuperview
.
这个方法可以看做是View的生命周期中的将要添加到View上和将要从View上移除.
因为,比如目前有A和B两个View.[A addSubView:B]
.那么B就会willMoveToSuperView:
了.然而[B removeFromSuperView]
的时候也会调用willMoveToSuperView:
.那么,区别在哪呢.
当添加到一个新视图上的时候,newSuperView的值不为nil.当从一个父视图上移除的时候.newSuperView为nil.这就是区别.
悬停的实现.
主要是根据上面的contentOffset监听改变的通知和当前的MJ状态来做的.
/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, MJRefreshState) {/** 普通闲置状态 */MJRefreshStateIdle = 1,/** 松开就可以进行刷新的状态 */MJRefreshStatePulling,/** 正在刷新中的状态 */MJRefreshStateRefreshing,/** 即将刷新的状态 */MJRefreshStateWillRefresh,/** 所有数据加载完毕,没有更多的数据了 */MJRefreshStateNoMoreData
};
对于上面的状态来说.
MJRefreshStateIdle
状态其实没有什么必要讲(就是普通状态,没显示header/footer的状态).然后呢MJRefreshStatePulling
状态和MJRefreshStateWillRefresh
状态其实就是一种状态.同理,MJRefreshStateRefreshing
和MJRefreshStateNoMoreData
也是一种状态.
为什么我进行上面的状态划分呢.其实仔细想想特别容易思考到.
首先,比如我们如果在设置mj_header之后再设置tableview.bounce = NO;
.那么,这个下拉刷新是无法触发的.并且在scrollViewContentOffsetDidChange:
里头的状态赋值都不会有比如当前是刷新状态,再赋值一个刷新状态进去的情况.正在刷新和没有更多数据的时候其实是用_scrollViewOriginalInset
记录了一下原始的值.然后改变了contentInset的值来做到悬停
效果.当刷新结束,状态就被置位MJRefreshStateIdle
.然后重设contentInset.