00『 基础知识 』ios开发学习iOS高级

自定义MJRefresh:“什么值得买”的下拉刷新实现

2016-08-18  本文已影响1788人  ZZZEoEv

写在前面

“什么值得买”是我这种剁手族常用的软件,最近发现它的下拉刷新做得挺好的,而且也算是一种经常见到的样式,正好前几天刚好分析完了MJRefresh,趁热打铁,这次就来尝试实现一下它的下拉刷新吧。

一、总体构成

原厂效果图

从上图可以看出,RefreshView主要是两部分组成:

Label就不多介绍了,我们来重点看一下Image部分。

实现难度不大,下面我们就开始动手吧。

二、刷新部分

最终实现效果

最后的效果大概就是这个样子的,还算合格,我们来详细分析下。

(一)资源

提取ipa包内图片资源的方法有很多,而我这人比较懒,所以喜欢直接用工具,这里推荐给大家一款我一直用的:iOS-Images-Extractor,国内的某Coder写的,很好用,分享给大家,好用的话别忘了点个星,是给作者最大的鼓舞。

iOS-Images-Extractor使用界面

使用方法很简单,把ipa包拖进去,点击start等待分析完成,之后点击Output Dir就会自动跳转到输出目录。


OK,工具介绍完,我已经把图片找出来了,一共俩:

看到这俩角色,就明了了,一开始我以为缺了一块的Circle是用ShapeLayer画的,原来是美工做的,那就直接用吧,省事。

(二)动画实现

图片"zhi"在下,"circle"在上,然后对circle做旋转动画就OK了。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.logoView addSubview:self.circleView];
    [self.view addSubview:self.logoView];
}

- (void)viewDidLayoutSubviews{
    self.logoView.center = self.view.center;
    self.logoView.bounds = CGRectMake(0, 0, 30, 30);
    self.circleView.frame = self.logoView.bounds;
}

- (void)viewDidAppear:(BOOL)animated{
    [self.circleView.layer addAnimation:[self getTransformAnimation] forKey:nil];
}

-(CABasicAnimation *)getTransformAnimation{
    CABasicAnimation *animation   = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; //指定对transform.rotation属性做动画
    animation.duration            = 2.0f; //设定动画持续时间
    animation.byValue             = @(M_PI*2); //设定旋转角度,单位是弧度
    animation.fillMode            = kCAFillModeForwards;//设定动画结束后,不恢复初始状态之设置一
    animation.repeatCount         = 1000;//设定动画执行次数
    animation.removedOnCompletion = NO;//设定动画结束后,不恢复初始状态之设置二
    return animation;
}

- (UIImageView *)logoView{
    if (!_logoView) {
        _logoView = [[UIImageView alloc] init];
        _logoView.image = [UIImage imageNamed:@"zhi"];
    }
    return _logoView;
}

- (UIImageView *)circleView{
    if (!_circleView) {
        _circleView = [[UIImageView alloc] init];
        _circleView.image = [UIImage imageNamed:@"circle"];
    }
    return _circleView;
}

很简单,主要就是动画部分,如果对动画不熟悉的童鞋,推荐ios核心动画高级技巧

三、下拉部分

这部分,主要是要实现Circle随我们手势改变自身完成度,先上效果图:

实现效果图

(一) 用ShapeLayer画个圆:

这里的Circle部分,我们用CAShapeLayer来做:

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。

形象点来说,就是你给CAShapeLayer指定脚本(Path),并设定好各属性(Color,Width)之后,CAShapeLayer就自动完成了。

-(CAShapeLayer *)getShape{
    UIBezierPath *path       = [UIBezierPath bezierPathWithOvalInRect:self.logoView.bounds];//先写剧本

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path          = path.CGPath;//安排剧本
    shapeLayer.fillColor     = [UIColor clearColor].CGColor;//填充色要为透明,不然会遮挡下面的图层
    shapeLayer.strokeColor   = [UIColor redColor].CGColor;
    shapeLayer.lineWidth     = 1.0;
    shapeLayer.frame         = self.logoView.bounds;
    return shapeLayer;
}

- (void)viewDidAppear:(BOOL)animated{
    [self.logoView.layer addSublayer:[self getShape]]; //将ShapeLayer图层增加到logoView上
}
画个圆

(二)控制ShapeLayer的绘制进度

圆画完了,下面是和Slider.value关联,让我们能控制圆的绘制进度。
关键属性:strokeStartstrokeEnd

我们设定我们的圆起始点为:

    shapeLayer.strokeStart   = 0;
    shapeLayer.strokeEnd     = 0.9;
stroke起始点

可以出,stroke属性的特点:

有了这个属性,我们就可以很方便的实现我们的目标了。
我们把strokeEnd的初始值设为0,再与我们的Slider.value挂钩就好了。

完整代码:

- (void)viewDidAppear:(BOOL)animated{
    [self.logoView.layer addSublayer:self.circleLayer];
}

- (CALayer *)circleLayer{
    if (!_circleLayer) {
        _circleLayer = [self getShape];
    }
    return _circleLayer;
}

- (CAShapeLayer *)getShape{
    UIBezierPath *path       = [UIBezierPath bezierPathWithOvalInRect:self.logoView.bounds];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor     = [UIColor clearColor].CGColor;
    shapeLayer.strokeColor   = [UIColor redColor].CGColor;
    shapeLayer.lineWidth     = 1.0;
    shapeLayer.path          = path.CGPath;
    shapeLayer.frame         = self.logoView.bounds;
    shapeLayer.strokeEnd     = 0;
    return shapeLayer;
}

- (IBAction)didSlide:(UISlider *)sender {
    self.circleLayer.strokeEnd = sender.value;
}

四、自定义MJRefresh

经过上面两个步骤,我们已经实现了下拉刷新的核心视图动画,接下来该自定义MJRefresh了。
老规矩,先上完成图:


完成效果图

自定义MJRefreshHeader,需要继承自MJRefreshHeader,看过我之前文章的小伙伴一定很熟悉了。
不熟悉也不要紧,不过就有点死记硬背的感觉了。

(一)布局

#pragma mark - Const
CGRect kZZZLogoViewBounds = {0,0,25,25};
#pragma mark 在这里做一些初始化配置(比如添加子控件)
- (void)prepare
{
    [super prepare];
    [self.logoView addSubview:self.circleView];
    [self.logoView.layer addSublayer:self.circleLayer];

    [self addSubview:self.logoView];
}

#pragma mark 在这里设置子控件的位置和尺寸

- (void)placeSubviews
{
    [super placeSubviews];
    self.logoView.center = CGPointMake(self.mj_w/2.0, self.mj_h/2.0 + 10.0);// +10是为了logoView在中心点往下一点的位置,方便观看
    self.logoView.bounds = kZZZLogoViewBounds;
    self.circleView.frame = self.logoView.bounds;
}

#pragma mark - setter & getter

- (UIImageView *)logoView{
    if (!_logoView) {
        _logoView = [[UIImageView alloc] init];
        _logoView.image = [UIImage imageNamed:@"zhi"];
    }
    return _logoView;
}

- (UIImageView *)circleView{
    if (!_circleView) {
        _circleView = [[UIImageView alloc] init];
        _circleView.image = [UIImage imageNamed:@"circle"];
        _circleLayer.hidden = YES; //刷新时候的图片,开始的时候不需要显示出来
    }
    return _circleView;
}

- (CAShapeLayer *)circleLayer{
    if (!_circleLayer) {
        _circleLayer = [self creatCircleShapeLayerWithBounds:kZZZLogoViewBounds];//跟上面的getShapeLayer方法一样,不过这里我稍微改写了原函数,减少依赖
    }
    return _circleLayer;
}

有几点需要说明的:

自定义的时候,慢慢来,出了BUG一般是Frame没设置好,多利用调试工具。

(二)设置动态响应

我们只需要做两件事情:

(1)将下拉位移量与我们的strokeEnd属性关联

关联这件事情,MJRefresh已经帮我们处理了前半部分,我们只需要在相应方法里写个等式就可以了。

(2) 处理状态

注意的是,我们的需要在endRefreshing方法中,手动移除动画(因为我们在动画定义部分为了动画的流畅性,设置了animation.removedOnCompletion = NO),不然CircleView上的动画会一直运行。

#pragma mark 监听控件的刷新状态
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState;
    
    switch (state) {
        case MJRefreshStateIdle:
            self.circleView.hidden = YES;
            self.circleLayer.hidden = NO;
            break;
        case MJRefreshStatePulling:
            break;
        case MJRefreshStateRefreshing:
            self.circleView.hidden = NO;
            self.circleLayer.hidden = YES;
            [self.circleView.layer addAnimation:[self creatTransformAnimation] forKey:nil];
            break;
        default:
            break;
    }
}

- (void)setPullingPercent:(CGFloat)pullingPercent
{
        self.circleLayer.strokeEnd = pullingPercent;
}

- (void)endRefreshing{


    [self.circleView.layer removeAllAnimations];
    [super endRefreshing];
}

来看一下运行结果:

对比原版,貌似有几点问题:

有问题,就解决问题呗。

第一个问题
self.circleLayer.hidden = YES;

问题出在这行代码上。
这涉及到了CoreAnimation的隐式动画部分,说白了,你对Layer做的属性修改,会触发系统的隐藏动画,所以我们取消系统隐藏动画就好了。取消方法如下:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.circleLayer.hidden = YES;
[CATransaction commit];
运行结果

好的,这个问题已经不是问题了。

第二个问题

原厂的动画是,刷新完成之后,CircleLayer要保持StrokeEnd = 1.0的状态。

也就是说,需要个参数,能区分进入Idle状态之前是否刷新过,那我们就加个参数呗。

改动部分代码如下:


- (void)prepare
{
    [super prepare];
    [self.logoView addSubview:self.circleView];
    [self.logoView.layer addSublayer:self.circleLayer];
    [self addSubview:self.logoView];
    self.hasRefreshed = NO;//初始化的时候,肯定是没有刷新过的
}

#pragma mark 监听控件的刷新状态
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState;
    
    switch (state) {
        case MJRefreshStateIdle:
            self.circleView.hidden = YES;
            self.circleLayer.hidden = NO;
            break;
        case MJRefreshStatePulling:
            break;
        case MJRefreshStateRefreshing:
            
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            self.circleLayer.hidden = YES;
            [CATransaction commit];
        
            self.circleView.hidden = NO;
            [self.circleView.layer addAnimation:[self creatTransformAnimation] forKey:nil];

            self.hasRefreshed = YES;//刷新过了
            break;
        default:
            break;
    }
}

#pragma mark 监听拖拽比例(控件被拖出来的比例)

- (void)setPullingPercent:(CGFloat)pullingPercent
{
    if (self.hasRefreshed) {//刷新返回的时候,strokeEnd = 1.0 
        [CATransaction begin];
        [CATransaction setDisableActions:YES];
        self.circleLayer.strokeEnd = 1.0;
        [CATransaction commit];
        self.hasRefreshed = NO;//重置状态为未刷新
    }else{
        self.circleLayer.strokeEnd = pullingPercent;
    }
}

搞定。

总结

MJRefresh给我们提供了很好的底层实现,我们可以在它的基础上,进行丰富的自定义,基本都能满足自己的需求。
哪怕是实在满足不了你了,也可以借鉴MJRefresh的整体思路,自己写一个简单的框架。

我在分析完MJRefresh的技术细节之后,不再感觉自己面对的是一个黑匣子,修改起来是相当地轻松。
所以,读源码果然是提高自己技术水平的有效手段(就是有点累)。

至此,MJRefresh的旅程就算结束了。

希望大家以后都能做出独具个性的刷新控件。

上一篇下一篇

猜你喜欢

热点阅读