如果是一名 iOS 开发者, 用 Objective-C,
- 平时用的 mj_header , mj_footer, 到底是什么?
- 是怎么给 mj_header , mj_footer, 添加方法的?
·····
平时给 tableView , collectionView 添加的 mj_footer , mj_header ,都是 MJRefreshComponent 的子类。
下拉刷新的框架设计,先搞定基础服务,就是 MJRefreshComponent 了。
MJRefreshComponent ,KVO + 状态管理,造类似系统的代理方法
MJRefreshComponent 本质上是一个 UIView, 添加了一个父视图对象 UIScrollView。然后需要把我们用的 mj_footer , mj_header 添加到父视图对象 scrollView 上面。 那个 scrollView 就是我们业务代码里面的 tableView , collectionView .
要想实现我们的下拉刷新,上拉加载, 就需要实现相关基础设施。对用户下拉和上拉作出反馈,MJRefreshComponent 采用的是观察三个属性。scrollView 的 contentOffset 和 contentSize ,scrollView 自带的平移手势 panGestureRecognizer 的 state.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
通过 KVO,把这三个属性转化为三个方法,
// 偏移量改变
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 内容大小改变
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
// 拖拽状态发生改变
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
复制代码
Header 只调用 scrollViewContentOffsetDidChange:
, Footer 调用上面三个方法。 Header 只关注 偏移量, Footer 都要关注。
相当于 MJ 使用 KVO 生造了三个类似系统 scrollView 的代理方法,出来了。
MJRefresh 处理的相当有节操, 直接采用 KVO , 不涉及任何的 scrollView 的系统代理方法。 非常的框架。不打扰业务代码的可能实现。
MJRefreshComponent 里面有一个内部方法 - (void)executeRefreshingCallback
, 处理我们传入的事件,业务上一般是网络请求方法,上拉刷新第一页的数据,下拉加载下一页的数据。
可以检测到用户的操作了, 就要作出响应,显示 UI ,出现一朵菊花,执行我们的业务代码(作为外部传入给框架的)。
KVO 检测到的 scrollView 的偏移量 contentOffset, 内容大小 contentSize 是不断变化的,一触发, 啪啪啪, 一打数据甩过去了。
我们需要的是,在合适的时机,调用一次方法就好了。
MJRefreshComponent 采用的是状态管理。
/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
MJRefreshStateIdle = 1, /** 普通闲置状态 */
MJRefreshStatePulling, /** 松开就可以进行刷新的状态 */
MJRefreshStateRefreshing, /** 正在刷新中的状态 */
MJRefreshStateWillRefresh, /** 即将刷新的状态 */
MJRefreshStateNoMoreData /** 所有数据加载完毕,没有更多的数据了 */
};
复制代码
把监测到的 scrollView 的偏移量和内容大小的变化,转化为状态的改变,把连续的数据变化转化为离散的一两次操作,挺不错的。具体在子类头部刷新(下拉刷新)MJRefreshHeader 和尾巴刷新(上拉加载)MJRefreshFooter 中实现。
执行开发者的操作 (一般是网络请求)
检测到用户的列表滚动情况了,可以转化为操作了,就要显示UI, 放在 MJRefreshComponent 的子类头部/尾部类中实现了。然后就是执行开发者传入的方法。
这个内部方法 - (void)executeRefreshingCallback
,提供两种实现,一种是匿名函数,self.refreshingBlock();
, 另一种是 target - action, MJ 封装了一个运行时的发消息方法 MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
继承,MJRefreshComponent 作为父类,提供基础设施,Header 和 Footer 具体功能,更进一步的子类提供样式 ( UI )
MJRefresh 在继承上使用的比较出彩,MJRefreshComponent 建立基础设施,MJRefreshHeader 完成了下拉加载的功能,MJRefreshStateHeader 添加了基本的样式(主要是文本标签 UILabel , 更新的时间文本, 状态文本), MJRefreshNormalHeader 添加了箭头和菊花(活动指示器,UIActivityIndicatorView )
具体上拉加载和下拉刷新的 UI 部分,MJRefreshComponent 提供了大致如下四个空方法实现。
/** 摆放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 当scrollView的contentOffset发生改变的时候调用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的contentSize发生改变的时候调用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的拖拽状态发生改变的时候调用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
复制代码
MJRefresh 在继承上有两步干得漂亮, 从 MJRefreshComponent 到 Header 和 Footer , 这里有一个区分。
还有就是 MJMJRefresh 有很多的样式,比较有特色的是上拉刷新部分, 会回弹到底部的默认的 footer , 会回弹到底部的带动图的 footer , 会自动刷新的默认的 footer ,会自动刷新的带动图的 footer.
内存管理上,
MJRefreshComponent 在内存管理上,也有优点,
@interface MJRefreshComponent: UIView
{
/** 父控件 */
__weak UIScrollView * _scrollView;
}
/** 父控件 */
@property (weak, nonatomic, readonly) UIScrollView *scrollView;
复制代码
父视图 scrollView,我们的表视图,格子视图,外部 readonly 属性调用, 内部的 _scrollView 成员变量修改。
Runtime 上, 给系统类 scrollView 动态添加属性
- (void)setMj_footer:(MJRefreshFooter *)mj_footer{
if (mj_footer != self.mj_footer) {
// 删除旧的,添加新的
[self.mj_footer removeFromSuperview];
[self insertSubview:mj_footer atIndex:0];
// 存储新的
objc_setAssociatedObject(self, &MJRefreshFooterKey, mj_footer, OBJC_ASSOCIATION_RETAIN);
}
}
- (MJRefreshFooter *)mj_footer{
return objc_getAssociatedObject(self, &MJRefreshFooterKey);
}
复制代码
这一点上,NSHipster 的对象关联 Associated Objects 讲得很不错。
End
MJRefresh 对设计模式中继承, 观察者的使用, 使用继承的设计模式,分层加功能, 对 Runtime 的使用,对宏的大量使用, 都挺精彩的。