MJRefresh自定义"L"形刷新

MJRefresh现在已经12000多颗星.是目前为止使用最广泛的刷新控件.基本满足所有刷新需求

该框架的结构设计得很清晰,使用一个基类MJRefreshComponent来做一些基本的设定,然后通过继承的方式,让MJRefreshHeaderMJRefreshFooter分别具备下拉刷新和上拉加载的功能。具体可以从下面的图里看出来:

image

其中.简单的区分一下MJRefreshBackFooterMJRefreshAutoFooter

MJRefreshBackFooter :会回弹到底部的上拉刷新控件

image

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形状.背景接近灰色

image
-(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”.好处在于处理动画

image
-(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.根据拖拽的进度.不断地更新路径.

image
-(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.开始刷新:覆盖蓝色逐渐减少至形成圆形的动画.并一直旋转.直至刷新完成

image
  /**

   开始动画

  */

 -(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];

});

 }

看看最终的效果

image

你可能感兴趣的:(MJRefresh自定义"L"形刷新)