MJRefresh现在已经12000多颗星.是目前为止使用最广泛的刷新控件.基本满足所有刷新需求
该框架的结构设计得很清晰,使用一个基类MJRefreshComponent
来做一些基本的设定,然后通过继承的方式,让MJRefreshHeader
和MJRefreshFooter
分别具备下拉刷新和上拉加载的功能。具体可以从下面的图里看出来:
其中.简单的区分一下MJRefreshBackFooter
与MJRefreshAutoFooter
MJRefreshBackFooter
:会回弹到底部的上拉刷新控件
MJRefreshAutoFooter
:会自动刷新的上拉刷新控件
MJRefrshComponent类
基本主要思想与逻辑脉络均在基类里面.让我们详细看一下MJRefrshComponent
类
在.h中声明了
1.所有的刷新状态.
2.刷新方法相关回调
3.刷新状态的控制
4.添加子类们需要实现的方法
1.所有的刷新状态:记录每一个当前刷新状态.并且单独对每一种状态做单独的处理
/**刷新控件的状态*/
typedef NS_ENUM(NSInteger,MJRefreshState){
/**普通闲置状态*/
MJRefreshStateIdle = 1,
/**松开就可以进行刷新的状态 */
MJRefreshStatePulling,
/**正在刷新中的状态*/
MJRefreshStateRefreshing,
/**即将刷新的状态*/
MJRefreshStateWillRefresh,
/**所有数据加载完毕,没有更多的数据了 */
MJRefreshStateNoMoreData
};
2.刷新方法相关回调:回调给外界处理相关状态完成以后的操作
/** 进入刷新状态的回调 */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** 开始刷新后的回调(进入刷新状态后的回调) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** 结束刷新后的回调 */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();
3.刷新状态的控制:
#pragma mark - 刷新状态控制
/** 进入刷新状态 */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock;
/** 开始刷新后的回调(进入刷新状态后的回调) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 结束刷新的回调 */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 结束刷新状态 */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 刷新状态 一般交给子类内部实现 */
@property (assign, nonatomic) MJRefreshState state;
4.添加子类们需要实现的方法
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 摆放子控件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;
/** 拉拽的百分比(交给子类重写) */
@property (assign, nonatomic) CGFloat pullingPercent;
/** 根据拖拽比例自动切换透明度 */
@property (assign, nonatomic, getter=isAutoChangeAlpha) BOOL autoChangeAlpha MJRefreshDeprecated("请使用automaticallyChangeAlpha属性");
/** 根据拖拽比例自动切换透明度 */
@property (assign, nonatomic, getter=isAutomaticallyChangeAlpha) BOOL automaticallyChangeAlpha;
在.m中还添加了对scrollView的offSize以及contentSize等状态进行监听.并获取当当前的刷新状态.做对应的操作.相关代码如下
- (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];
}
2.状态变化的响应
- (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];
}
}
3.随着相关偏移状态变化.留给子类做相关操作的函数
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
MJRefreshHeader类
主要看一个覆盖父类函数的处理:
-(void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
//在刷新的refreshing状态
if(self.state == MJRefreshStateRefreshing){
if(self.window == nil)return;
// sectionheader停留解决-(-124)> 64 ? -(-124): 64.
//也就是说.获取当前的偏移值.当偏移值大于顶部的inset时(下拉).那么就取偏移值.当偏移值小于inset时(向上滚动).以顶部偏移值为偏移值. - 至少保证偏移值为正常的insetTop值.即还原!
CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
//一旦insetT大于了响应刷新的最大值.(self.mj_h + _scrollViewOriginalInset.top).那么就以该最大值作为其偏移值.否则.是多少偏移值就是多少偏移值
insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
//这样能保证在刷新状态时.刷新的偏移值为top偏移值+最大自身高度
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 - 即刚出现的偏移值.一般是-64.或者-88.即状态栏与导航栏的高度值
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
//如果是向上滚动到看不见头部控件,直接返回 - 不需要处理
// >= -> >
if(offsetY > happenOffsetY)return;
//普通 和 即将刷新 的临界点 . 即一般为导航栏+状态栏+自身刷新控件的高度. -124
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
//下拉进度值.0 -无穷大.
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;
}
}
JTFQ_ActivityHeaderView
继承至MJRefreshHeader.需要考虑的东西会更少 - 只需要重写一些父类方法即可
pragma mark 在这里做一些初始化配置(比如添加子控件)
-(void)prepare
{
[super prepare];
#添加子控件.设定刷新控件的高度等.
}
pragma mark监听控件的刷新状态
-(void)setState:(MJRefreshState)state
{
MJRefreshCheckState;
switch(state){
case MJRefreshStateIdle:
/*
普通闲置状态
*/
[self refreshStateIdle];
break;
case MJRefreshStatePulling:
/*
松开就可以进行刷新的状态
*/
break;
case MJRefreshStateWillRefresh:
/*
无用-不是每次调用
*/
break;
case MJRefreshStateRefreshing:
/*
正在刷新状态
*/
[self startAnimation];
break;
default:
break;
}
}
pragma mark 在这里设置子控件的位置和尺寸
-(void)placeSubviews
{
[super placeSubviews];
#在这里设置子控件的位置和尺寸
}
pragma mark 监听拖拽比例(控件被拖出来的比例)
-(void)setPullingPercent:(CGFloat)pullingPercent
{
#监听拖拽比例(根据控件被拖出来的比例.进行一些动画渲染).
}
现在只需要重写以上几个方法即可:
了解了整个MJRefresh的大致刷新思路以及相关类与函数.现在就”L”形刷新进行一个简单的分析.
0.布局总刷新控件
1.闲置状态:首先是一个背景L.
2.拖拽进度状态:随后用蓝色的layer进行覆盖.最后拉至全部覆盖.并且达到可以刷新的条件时.
3.开始刷新:覆盖蓝色逐渐减少至形成圆形的动画.并一直旋转.直至刷新完成
4.刷新结束.将圆圈逐渐变细进而消失-而后再恢复原状态.
代码解析:
0.布局总刷新控件
-(void)placeSubviews
{
[super placeSubviews];
self.ltfAnimationView.center = CGPointMake(self.mj_w/2.0,self.mj_h /2.0);;
self.ltfAnimationView.bounds = kZHeaderLogoViewBounds;
}
1.闲置状态:首先是一个背景L.使用Bezier曲线画出L形状.背景接近灰色
-(CAShapeLayer *)bgLlayer
{
if(!_bgLlayer){
_bgLlayer =[[CAShapeLayer alloc]init];
UIBezierPath * bgPath =[UIBezierPath bezierPath];
[bgPath moveToPoint:CGPointMake(15,30)];
[bgPath addLineToPoint:CGPointMake(15,7.5)];
[bgPath moveToPoint:CGPointMake(25,27.5)];
[bgPath addLineToPoint:CGPointMake(15,27.5)];
_bgLlayer.fillColor = RGB(224.0,224.0,224.0).CGColor;
_bgLlayer.strokeColor = RGB(224.0,224.0,224.0).CGColor;
_bgLlayer.lineWidth = 5;
_bgLlayer.frame = CGRectMake(0,0,40,40);
_bgLlayer.path = bgPath.CGPath;
}
return _bgLlayer;
}
2.拖拽进度状态:随后用蓝色的layer进行覆盖.最后拉至全部覆盖.并且达到可以刷新的条件时.
a.首先使用Bezier曲线画出两个元素.一个”I”与”-”.组装为”L”.好处在于处理动画
-(CAShapeLayer *)bottomLlayer
{
if(!_bottomLlayer){
_bottomLlayer =[CAShapeLayer layer];
_bottomLlayer.fillColor =[UIColor clearColor].CGColor;
_bottomLlayer.strokeColor =[UIColor colorWithRed:0.00 green:0.45 blue:0.98 alpha:1.00].CGColor;
_bottomLlayer.lineWidth = 5;
_bottomLlayer.frame = CGRectMake(0,0,40,40);
[self.ltfAnimationView.layer addSublayer:_bottomLlayer];
}
return _bottomLlayer;
}
-(CAShapeLayer *)topLlayer
{
if(!_topLlayer){
_topLlayer =[CAShapeLayer layer];
_topLlayer.fillColor =[UIColor clearColor].CGColor;
_topLlayer.strokeColor =[UIColor colorWithRed:0.00 green:0.45 blue:0.98 alpha:1.00].CGColor;
_topLlayer.lineWidth = 5;
_topLlayer.frame = CGRectMake(0,0,40,40);
[self.ltfAnimationView.layer addSublayer:_topLlayer];
}
return _topLlayer;
}
b.根据拖拽的进度.不断地更新路径.
-(void)setPullingPercent:(CGFloat)pullingPercent
{
if(pullingPercent <= 0.5){
self.topLlayer.path = nil;
UIBezierPath * bottomLpath =[UIBezierPath bezierPath];
[bottomLpath moveToPoint:CGPointMake(25,27.5)];
[bottomLpath addLineToPoint:CGPointMake(25 - 25 * pullingPercent,27.5)];
self.bottomLlayer.path = bottomLpath.CGPath;
}else{
if(pullingPercent > 1){
pullingPercent = 1.0;
}
UIBezierPath * topLpath =[UIBezierPath bezierPath];
[topLpath moveToPoint:CGPointMake(15,25)];
[topLpath addLineToPoint:CGPointMake(15,25 - 35 *(pullingPercent - 0.5))];
self.topLlayer.path = topLpath.CGPath;
UIBezierPath * bottomLpath =[UIBezierPath bezierPath];
[bottomLpath moveToPoint:CGPointMake(12.5,27.5)];
[bottomLpath addLineToPoint:CGPointMake(25,27.5)];
self.bottomLlayer.path = bottomLpath.CGPath;
}
}
3.开始刷新:覆盖蓝色逐渐减少至形成圆形的动画.并一直旋转.直至刷新完成
/**
开始动画
*/
-(void)startAnimation{
self.bgLlayer.path = nil;
UIBezierPath * topLpath =[UIBezierPath bezierPath];
[topLpath moveToPoint:CGPointMake(15,25)];
[topLpath addLineToPoint:CGPointMake(15,7.5)];
self.topLlayer.path = topLpath.CGPath;
UIBezierPath * bottomLpath =[UIBezierPath bezierPath];
[bottomLpath moveToPoint:CGPointMake(25,27.5)];
[bottomLpath addLineToPoint:CGPointMake(12.5,27.5)];
self.bottomLlayer.path = bottomLpath.CGPath;
if([self.topLlayer.animationKeys containsObject:@"topLLayerStrokeEnd”]){
return;
}
if([self.bottomLlayer.animationKeys containsObject:@"bottomLLayerStrokeEnd”]){
return;
}
if([self.arcLlayer.animationKeys containsObject:@"arcLLayerStrokeEnd”]){
return;
}
//执行动画
CABasicAnimation * topLanimation =[CABasicAnimation animationWithKeyPath:@"strokeEnd”];
topLanimation.fromValue =@(1);
topLanimation.toValue = @(0);
topLanimation.duration = 2;//0.1;
topLanimation.removedOnCompletion = NO;
topLanimation.fillMode = kCAFillModeForwards;
[self.topLlayer addAnimation:topLanimation forKey:@"topLLayerStrokeEnd”];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
CABasicAnimation * bottomLanimation =[CABasicAnimation animationWithKeyPath:@"strokeEnd”];
bottomLanimation.fromValue =@(1);
bottomLanimation.toValue = @(0);
bottomLanimation.duration =2;// 0.1;
bottomLanimation.removedOnCompletion = NO;
bottomLanimation.fillMode = kCAFillModeForwards;
[self.bottomLlayer addAnimation:bottomLanimation forKey:@"bottomLLayerStrokeEnd”];
//再次延后3秒执行圆形的动画//2
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
UIBezierPath * arcLpath =[UIBezierPath bezierPathWithArcCenter:CGPointMake(20,20)radius:10 startAngle:M_PI * 2.0 / 3.0 endAngle:M_PI * 2.0 / 3.0+0.1 clockwise:NO];
self.arcLlayer.path = arcLpath.CGPath;
CABasicAnimation * arcBottomLanimation =[CABasicAnimation animationWithKeyPath:@"strokeEnd”];
arcBottomLanimation.fromValue =@(0);
arcBottomLanimation.toValue = @(1);
arcBottomLanimation.duration = 2;
[self.arcLlayer addAnimation:arcBottomLanimation forKey:@"arcLLayerStrokeEnd”];
//随后一直旋转
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
CABasicAnimation * arcLanimation =[CABasicAnimation animationWithKeyPath:@"transform.rotation.z”];
arcLanimation.fromValue =@(2 * M_PI);
arcLanimation.toValue = @(0);
arcLanimation.duration = 0.5;
arcLanimation.repeatCount = MAXFLOAT;
[self.arcLlayer addAnimation:arcLanimation forKey:@"runaroundAnim”];
self.hasRefreshed = YES;
});
});
});
}
4.刷新结束.将圆圈逐渐变细进而消失-而后再恢复原状态.
/**
没有结束的回调
*/
-(void)stopAnimation{
CABasicAnimation * arcLanimation =[CABasicAnimation animationWithKeyPath:@"lineWidth”];
arcLanimation.toValue = @(0);
arcLanimation.duration = 0.5;
self.arcLlayer.lineWidth = 0;
[self.arcLlayer addAnimation:arcLanimation forKey:nil];
}
-(void)endRefreshing
{
[self stopAnimation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(0.5 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
[super endRefreshing];
});
}
看看最终的效果