iOS开发-CALayer的探赜索隐应用

序言

先上本文讲述的demo效果图

图片 1

这几天博主在看kitten yang的A GUIDE TO IOS
ANIMATION,作者对动画的使用令自己感动很深(同为同龄人实在感觉羞愧),于是决定重新学习五次layer。
coreAnimation作为iOS最重点的框架之一,CALayer的重要性毋庸置疑,本文将从上图的demo讲起,我会分成常规用法跟自己想想实现的用法来促成,以此来一发无时或忘的学习layer。
ps:本文不包括CALayer的属性讲解以及利用。如有需要,请自行百度上学

进度条

常规做法

如上图所示,进度条并不是独自的线性增长,在50%以前,每四回进度扩充,进度条就会在y轴下边偏移一段距离,直到增长到一半进度的时候偏移地点达到顶点,然后趁着速度继续加码,y轴的偏移越来越小,直到变回一条直线。
从贯彻角度而言,使用CAShapeLayer下一场在历次进度改变的时候更新其path值就可以落实。如若选择CAShapeLayer的艺术,我们需要创立三个实例对象,一个放在下面作为进度条背景,另一个在下边随着速度改变而变更。图示如下:

图片 2

每回进度发生变更的时候,我们都要遵照当前速度总结出进度坐标地方,然后更新多少个图层的path,代码如下:

- (void)updatePath
{
    UIBezierPath * path = [UIBezierPath bezierPath];
    [path moveToPoint: CGPointMake(25, 150)];
    [path addLineToPoint: CGPointMake((CGRectGetWidth([UIScreen mainScreen].bounds) - 50) * _progress + 25, 150 + (25.f * (1 - fabs(_progress - 0.5) * 2)))];
    [path addLineToPoint: CGPointMake(CGRectGetWidth([UIScreen mainScreen].bounds) - 25, 150)];
    self.background.path = path.CGPath;
    self.top.path = path.CGPath;
    self.top.strokeEnd = _progress;
}

实际,使用这种模式实现速度效果的时候,进度会比一贯在时下上下文绘制的响应上要慢上几帧,即是我们肉眼可以见到那种延时更新的功力,是不便利用户体验的。其次,大家需要非凡成立一个背景图层,在内存上有了额外的开销。

自定义layer

这小节我们要经过自定义CALayer的子类来实现地点的进度条效果,我们需要对外开放progress属性。每一回这些值产生变更的时候大家要调用[self setNeedsDisplay]来再一次绘制进度条

@property(nonatomic, assign) CGFloat progress;

重写setter方法,检测进度值范围以及重复绘制进度条

  • (void)setProgress: (CGFloat)progress
    {
    _progress = MIN(1.f, MAX(0.f, progress));
    [self setNeedsDisplay];
    }
    双返记念一下进度条,我们得以把进度条分成两条线,分别是肉色的已形成进度条和绿色的进度条。遵照进度条的不比,分为<0.5,
    =0.5, >0.5二种状态:

图片 3

从上图可知,在进度达到一半的时候,大家的进度条在Y轴上的偏移量达到最大值。由此,我们相应定义一个最大偏移值MAX_OFFSET。

#define MAX_OFFSET 25.f

一面,当前进度条的y轴偏移量是按照进度按百分比举行偏移的。在大家改变进度_progress的时候,重新绘制进度条。下边是红色进度条的绘图

  • (void)drawInContext: (CGContextRef)ctx
    {
    CGFloat offsetX = _origin.x + MAX_LENGTH * _progress;
    CGFloat offsetY = _origin.y + _maxOffset * (1 – fabs((_progress –
    0.5f) * 2));

      CGMutablePathRef mPath = CGPathCreateMutable();
      CGPathMoveToPoint(mPath, NULL, _origin.x, _origin.y);
      CGPathAddLineToPoint(mPath, NULL, offsetX, offsetY);
    
      CGContextAddPath(ctx, mPath);
      CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
      CGContextSetLineWidth(ctx, 5.f);
      CGContextSetLineCap(ctx, kCGLineCapRound);
      CGContextStrokePath(ctx);
    
      CGPathRelease(mPath);
    

    }

ps:
这里存在一个很要紧的问题,自定义的layer必须加在大家自定义的view上边,才能实现drawInContext:方法开展连发的重绘。关于coreGraphics相关方法的更多采纳,请参见这篇作品

其次局部的绿色线条基于当前偏移的坐标为起源举办绘图,在此处有多个小陷阱:

  • 不在行的开发者很容易直接把绘制绿色线条的代码放在下边这段代码的末尾。这样会造成青色线条在黑色线条前边绘制而将青色线条遮住了一部分使得青色线条端末非圆形
  • 没有对_progress的值进行判定。当_progress为0时,下边的代码也会在线条左边生成一个红色小圆点,这是不规范的。

之所以,我们在确定好脚下进度对应的晃动坐标时,应该间接绘制黄色线条,再绘制青色进度条。在绘制青色线条前应该对_progress举办一回判断

  • (void)drawInContext: (CGContextRef)ctx
    {
    CGFloat offsetX = _origin.x + MAX_LENGTH * _progress;
    CGFloat offsetY = _origin.y + _maxOffset * (1 – fabs((_progress –
    0.5f) * 2));

      CGMutablePathRef mPath = CGPathCreateMutable();
      CGPathMoveToPoint(mPath, NULL, offsetX, offsetY);
      CGPathAddLineToPoint(mPath, NULL, _origin.x + MAX_LENGTH, _origin.y);
    
      CGContextAddPath(ctx, mPath);
      CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor);
      CGContextSetLineWidth(ctx, 5.f);
      CGContextSetLineCap(ctx, kCGLineCapRound);
      CGContextStrokePath(ctx);
      CGPathRelease(mPath);
    
      if (_progress != 0.f) {
          mPath = CGPathCreateMutable();
          CGPathMoveToPoint(mPath, NULL, _origin.x, _origin.y);
          CGPathAddLineToPoint(mPath, NULL, offsetX, offsetY);
    
          CGContextAddPath(ctx, mPath);
          CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
          CGContextSetLineWidth(ctx, 5.f);
          CGContextSetLineCap(ctx, kCGLineCapRound);
          CGContextStrokePath(ctx);
          CGPathRelease(mPath);
      }
    

    }

这时在controller里面加上一个UISlider拖拉来决定你的进度条进度,看看是不是想要的效益完成了。

扩展

下面我们在实现绘制的时候,对填充色彩颜色是写死的,这样不便宜代码扩充。回顾CAShapeLayer,在后续CALayer的底蕴上添加了fillColor、strokeColor等看似属性,我们得以因此抬高类似的分子属性来成功封装,这里大家需要为进度条添加三个属性,分别表示进度条颜色跟背景颜色

@property(nonatomic, assign) CGColorRef backgroundColor;
@property(nonatomic, assign) CGColorRef strokeColor;

大家在安装颜色的时候一贯传入color.CGColor就可以形成赋值了,我们把下面的设置颜色代码分别改成下边所示后再度运行

CGContextSetStrokeColorWithColor(ctx, _backgroundColor);
CGContextSetStrokeColorWithColor(ctx, _strokeColor);

局部朋友们会意识一个坑爹的事情,崩溃了,现身了EXC_BAD_ACCESS荒谬——假设你利用系统提供的[UIColor xxxColor].CGColor,那么这里不会出题目。
这是因为大家扩张的五个特性为assign类型,在我们运用这么些color的时候,它早已被放飞了。由这里我们可以观看两件业务:

  • CAShapeLayer会对非对象且属于coreGraphics的特性举办桥接或者引用操作
  • [UIColor
    xxxColor]方法重临的目标应当是大局或者静态对象。为了省去内存消耗,应该是应用懒加载格局。有必不可少的场合下,可以不调用那个主意来促成优化内存的效能

因此,我们理应重写这五个属性的setter方法来落实引用(欢迎来到MRC)

  • (void)setStrokeColor: (CGColorRef)strokeColor
    {
    CGColorRelease(_strokeColor);
    _strokeColor = strokeColor;
    CGColorRetain(_strokeColor);
    [self setNeedsDisplay];
    }
    除去,CAShapeLayer还有一个幽默的性质strokeEnd,这些特性决定了方方面面图层有稍许有些需要被渲染的。想查看这一个特性的看官们得以在最起先的正常化代码中为layer设置这多少个特性,然后您会意识此时不管我们的progress设置为多少,进度条的黑色部分总是一样strokeEnd。效果如下图所示

图片 4

可以看出,基于strokeEnd举办绘图的时候,界面的绘图难度进一步错综复杂了。不过我们一致可以把这么些拆分,分为二种境况
1、strokeEnd>progress
本条情状对应图中下边五个图,当然,在progress=1跟progress=0的场合是同样的。可以看到,当progress不为零的时候,进度条分为三有的:

  • 偏移点左边的青色线条
  • 出手多出的红色线条
  • 最终的紫色线条

交接点的y坐标应当是由strokeEnd超出progress的百分比部分除以当前右边总长度占线条总长度的比例,如下图所示

图片 5

从而我们需要看清多少个坐标点,其中偏移点依照上边代码一样依据progress得出,统计背景象和进度颜色交接点的代码如下:

CGFloat contactX = _origin.x + MAX_LENGTH * _strokeEnd;
CGFloat contactY = _origin.y + (offsetY - _origin.y) * ((1 - (_strokeEnd - _progress) / (1 - _progress)));

2、strokeEnd<=progress
这时就对应下边的两张图了,同样的,大家得以把进度条拆分成三片段:

  • 最左侧的棕色进度条
  • 远在进度条和偏移点中间的背景颜色条
  • 左侧的背景颜色条

遵照下边的图解格局展开剖析,相当于把左边的职务音信放到了左手,我们可以随便的查获颜色交接点坐标的臆想情势

CGFloat contactX = _origin.x + MAX_LENGTH * _strokeEnd;
CGFloat contactY = (offsetY - _origin.y) * (_progress == 0 ?: _strokeEnd / _progress) + _origin.y;

有了下边的解析总结,drawInContext的代码如下

  • (void)drawInContext: (CGContextRef)ctx
    {
    CGFloat offsetX = _origin.x + MAX_LENGTH * _progress;
    CGFloat offsetY = _origin.y + _maxOffset * (1 – fabs(_progress –
    0.5f) * 2);
    CGFloat contactX = 25.f + MAX_LENGTH * _strokeEnd;
    CGFloat contactY = _origin.y + _maxOffset * (1 – fabs(_strokeEnd –
    0.5f) * 2);

      CGRect textRect = CGRectOffset(_textRect, MAX_LENGTH * _progress, _maxOffset * (1 - fabs(_progress - 0.5f) * 2));
      if (_report) {
          _report((NSUInteger)(_progress * 100), textRect, _strokeColor);
      }
      CGMutablePathRef linePath = CGPathCreateMutable();
    
      //绘制背景线条
      if (_strokeEnd > _progress) {
          CGFloat scale =  _progress == 0 ?: (1 - (_strokeEnd - _progress) / (1 - _progress));
          contactY = _origin.y + (offsetY - _origin.y) * scale;
          CGPathMoveToPoint(linePath, NULL, contactX, contactY);
      } else {
          CGFloat scale = _progress == 0 ?: _strokeEnd / _progress;
          contactY = (offsetY - _origin.y) * scale + _origin.y;
          CGPathMoveToPoint(linePath, NULL, contactX, contactY);
          CGPathAddLineToPoint(linePath, NULL, offsetX, offsetY);
      }
      CGPathAddLineToPoint(linePath, NULL, _origin.x + MAX_LENGTH, _origin.y);
      [self setPath: linePath onContext: ctx color: [UIColor colorWithRed: 204/255.f green: 204/255.f blue: 204/255.f alpha: 1.f].CGColor];
    
      CGPathRelease(linePath);
      linePath = CGPathCreateMutable();
    
      //绘制进度线条
      if (_progress != 0.f) {
          CGPathMoveToPoint(linePath, NULL, _origin.x, _origin.y);
          if (_strokeEnd > _progress) { CGPathAddLineToPoint(linePath, NULL, offsetX, offsetY); }
              CGPathAddLineToPoint(linePath, NULL, contactX, contactY);
          } else {
              if (_strokeEnd != 1.f && _strokeEnd != 0.f) {
                  CGPathMoveToPoint(linePath, NULL, _origin.x, _origin.y);
              CGPathAddLineToPoint(linePath, NULL, contactX, contactY);
          }
      }
      [self setPath: linePath onContext: ctx color: [UIColor colorWithRed: 66/255.f green: 1.f blue: 66/255.f alpha: 1.f].CGColor];
      CGPathRelease(linePath);
    

    }

大家把添加CGPathRef以及安装线条颜色、大小等参数的代码封装成setPath: onContext: color方法,以此来减弱代码量。
coreAnimation以及coreGraphics用作最主题的框架之一,有广大值得大家去研商的性状,那一个特点是怎么落实的对大家来说是一个迷,可是大家可以尝试去探索这个特点。
文集:iOS开发

转载讲明链接:CALayer的钻探应用

相关文章