基本原理:
通过header view和 foot view 添加对scrollview contentOffset属性 kvo 实现动态刷新。在runtime对scrollview添加header的时候,在view的willMoveToSuperView生命周期方法中进行注册。
需要记忆理解的地方:
1.scrollview往上滑动的时候,contentOffsize的y是正数。scrollview往下滑动的时候,contentOffsize的y是负数。
2.header和footer的位置和大小是在view的layoutSubviews生命周期方法里设置的。
3.通过设置header或者footer的状态的时候控制其行为
小技巧:
1.在子类中调用了父类的方法,如果要求子类在其实现中必须调用父类的方法,可以在方法声明的时候添加 NS_REQUIRES_SUPER 宏,编译器在编译过程可进行检查。
代码:
1.MJRefreshNormalHeader
在初始化的时候,会调用init方法,在Init方法里,系统默认调用了initWithFrame方法,frame传的CGRectZero.在MJRefreshNormalHeader的父类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 准备工作 [self prepare]; // 默认是普通状态 self.state = MJRefreshStateIdle; } return self; }
|
prepare方法设置了header的宽高和x的位置:
1 2 3 4 5 6 7 8 9 10 11 12
| - (void)prepare { [super prepare]; // 设置key self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey; // 设置高度 self.mj_h = MJRefreshHeaderHeight; }
|
设置state属性方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| - (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) { //这个属性主要是用来控制随着下拉的距离来反应到header的透明度 self.pullingPercent = 0.0; }]; } else if (state == MJRefreshStateRefreshing) { [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^ { // 增加滚动区域 CGFloat top = self.scrollViewOriginalInset.top + self.mj_h; self.scrollView.mj_insetT = top; // 设置滚动位置 self.scrollView.mj_offsetY = - top; } completion:^(BOOL finished) { //业务逻辑层的刷新数据 [self executeRefreshingCallback]; }]; } }
|
根据拖拽进度设置透明度:
1 2 3 4 5 6 7 8 9 10 11 12
| - (void)setPullingPercent:(CGFloat)pullingPercent { _pullingPercent = pullingPercent; if (self.isRefreshing) return; if (self.isAutomaticallyChangeAlpha) { self.alpha = pullingPercent; } }
|
在UIScrollVIEW+MJRefresh中的setMj_header方法,给UIScrollView动态的添加了一个header,并将它加到了ScrollView上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| - (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 } }
|
将header加到ScrollView上的时候,系统会调用willMoveToSuperview方法,将header从ScrollView删除的时候也会调用这个方法,区别是删除的时候参数传的nil,添加的时候参数为当前scrollView.在这个方法中设置了header的x位置以及header的宽度,并且添加了对scrollview的监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| - (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 = 0; // 记录UIScrollView _scrollView = (UIScrollView *)newSuperview; // 设置永远支持垂直弹簧效果 _scrollView.alwaysBounceVertical = YES; // 记录UIScrollView最开始的contentInset _scrollViewOriginalInset = _scrollView.contentInset; // 添加监听 [self addObservers]; } }
|
添加监听的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - (void)addObservers { NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; //监测滑动距离,判断是否到达需要刷新的程度 [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil]; //检测contentSize变化,比如上拉加载更多,动态改变footer的位置 [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]; }
|
接下来,系统会开始调用layoutSubViews方法:在MJRefreshComponent中重写了这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| - (void)layoutSubviews { [super layoutSubviews]; [self placeSubviews]; } //placeSubviews方法是在各个MJRefreshComponent的子类中实现的, //继承链:MJRefreshNormalHeader -> MJRefreshStateHeader -> MJRefreshHeader -> MJRefreshComponent //以下是MJRefreshHeader中的实现,设置了header的y的位置 - (void)placeSubviews { [super placeSubviews]; // 设置y值(当自己的高度发生改变了,肯定要重新调整Y值, //所以放到placeSubviews方法中设置y值) self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop; } //以下是MJRefreshStateHeader中的实现,添加刷新状体和更新时间 - (void)placeSubviews { [super placeSubviews]; if (self.stateLabel.hidden) return; BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0; if (self.lastUpdatedTimeLabel.hidden) { // 状态 if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds; } else { CGFloat stateLabelH = self.mj_h * 0.5; // 状态 if (noConstrainsOnStatusLabel) { self.stateLabel.mj_x = 0; self.stateLabel.mj_y = 0; self.stateLabel.mj_w = self.mj_w; self.stateLabel.mj_h = stateLabelH; } // 更新时间 if (self.lastUpdatedTimeLabel.constraints.count == 0) { self.lastUpdatedTimeLabel.mj_x = 0; self.lastUpdatedTimeLabel.mj_y = stateLabelH; self.lastUpdatedTimeLabel.mj_w = self.mj_w; self.lastUpdatedTimeLabel.mj_h = self.mj_h - self.lastUpdatedTimeLabel.mj_y; } } } //以下是MJRefreshNormalHeader中的实现,添加剪头和loadingview - (void)placeSubviews { [super placeSubviews]; // 箭头的中心点 CGFloat arrowCenterX = self.mj_w * 0.5; if (!self.stateLabel.hidden) { arrowCenterX -= 100; } CGFloat arrowCenterY = self.mj_h * 0.5; CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY); // 箭头 if (self.arrowView.constraints.count == 0) { self.arrowView.mj_size = self.arrowView.image.size; self.arrowView.center = arrowCenter; } // 圈圈 if (self.loadingView.constraints.count == 0) { self.loadingView.center = arrowCenter; } }
|
header中监控下拉距离的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change { [super scrollViewContentOffsetDidChange:change]; // 在刷新的refreshing状态 if (self.state == MJRefreshStateRefreshing) { 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.contentInset; // 当前的contentOffset CGFloat offsetY = self.scrollView.mj_offsetY; // 头部控件刚好出现的offsetY CGFloat happenOffsetY = - self.scrollViewOriginalInset.top; // 如果是向上滚动到看不见头部控件,直接返回 if (offsetY > happenOffsetY) return; // 普通 和 即将刷新 的临界点, self.mj_h是当前header的高度 CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h; CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h; if (self.scrollView.isDragging) { // 如果正在拖拽 self.pullingPercent = pullingPercent; //header正好完全露出来,开始转为刷新状态 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; } }
|
footer同header一样都继承自MJFfreshComponent,都自动检测scrollview的行为,各自实现具体的被通知的逻辑
MJRefreshAutoFooter的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change { [super scrollViewContentOffsetDidChange:change]; if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self. mj_y == 0) return; if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 内容超过一个屏幕 // 这里的_scrollView.mj_contentH替换掉self.mj_y更为合理 if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView. mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) { // 防止手松开时连续调用 CGPoint old = [change[@"old"] CGPointValue]; CGPoint new = [change[@"new"] CGPointValue]; if (new.y <= old.y) return; // 当底部刷新控件完全出现时,才刷新 [self beginRefreshing]; } } } - (void)scrollViewPanStateDidChange:(NSDictionary *)change { [super scrollViewPanStateDidChange:change]; if (self.state != MJRefreshStateIdle) return; if (_scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded ) {// 手松开 if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h ) { // 不够一个屏幕 if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽 [self beginRefreshing]; } } else { // 超出一个屏幕 if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView .mj_insetB - _scrollView.mj_h) { [self beginRefreshing]; } } } }
|