标签:
今天博主有一个CALayer与coreAnimation的需求,遇到了一些困难点,在此和大家分享,希望能够共同进步.
进度条并不是单纯的线性增长,在50%之前,每一次进度增加,进度条就会在y轴上面偏移一段距离,直到增长到一半进度的时候偏移位置达到顶点,然后随着进度继续增加,y轴的偏移越来越小,直到变回一条直线。
从实现角度而言,使用CAShapeLayer
然后在每次进度改变的时候更新其path
值就能够实现。如果使用CAShapeLayer
的方式,我们需要创建两个实例对象,一个放在下面作为进度条背景,另一个在上面随着进度改变而改变。图示如下:
每次进度发生改变的时候,我们都要根据当前进度计算出进度坐标位置,然后更新两个图层的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;
}
事实上,使用这种方式实现进度效果的时候,进度会比直接在当前上下文绘制的响应上要慢上几帧,即是我们肉眼可以看到这种延时更新的效果,是不利于用户体验的。其次,我们需要额外创建一个背景图层,在内存上有了额外的花销。
这小节我们要通过自定义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三种状态:
从上图可知,在进度达到一半的时候,我们的进度条在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进行一次判断
- (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的时候,它早就被释放了。由这里我们可以看到两件事情:
因此,我们应该重写这两个属性的setter方法来实现引用(欢迎来到MRC)
- (void)setStrokeColor: (CGColorRef)strokeColor
{
CGColorRelease(_strokeColor);
_strokeColor = strokeColor;
CGColorRetain(_strokeColor);
[self setNeedsDisplay];
}
除此之外,CAShapeLayer还有一个有趣的属性strokeEnd
,这个属性决定了整个图层有多少部分需要被渲染的。想查看这个属性的看官们可以在最开始的常规代码中为layer设置这个属性,然后你会发现这时候不管我们的progress设置为多少,进度条的绿色部分总是等同于strokeEnd
。效果如下图所示
可以看到,基于strokeEnd进行绘制的时候,界面的绘制难度更加复杂了。但是我们同样可以把这个拆分,分为两种情况
1、strokeEnd>progress
这个情况对应图中上面两个图,当然,在progress=1跟progress=0的状态是一样的。可以看到,当progress不为零的时候,进度条分为三部分:
交接点的y坐标应当是由strokeEnd超出progress的百分比部分除以当前右侧总长度占线条总长度的百分比,如下图所示
因此我们需要判断两个坐标点,其中偏移点按照上面代码一样根据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
方法,以此来减少代码量。
http://www.cocoachina.com/ios/20141022/10005.html?1413946650
http://www.cnblogs.com/wendingding/p/3800736.html
iOS开发日记52-CALayer与coreAnimation
标签:
原文地址:http://www.cnblogs.com/Twisted-Fate/p/5062003.html