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.
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/214033.html原文链接:https://javaforall.net
