从圆圈或圆环画出细分

我一直在试图找出一种方法来绘制段如下图所示:

在这里输入图像说明

我想:

  1. 绘制段
  2. 包括渐变
  3. 包括阴影
  4. 从0到n的angular度animation绘图

我一直在尝试使用CGContextAddArc和类似的调用来做到这一点,但没有得到很大的进展。

谁能帮忙?

你的问题有很多部分。

获得path

为这样的细分市场创buildpath不应该太难。 有两条弧线和两条直线。 我以前解释过如何分解这样的path,所以我不会在这里做。 相反,我会变得幻想,并通过抚摸另一条path来创造path。 你当然可以阅读故障并自行构buildpath。 我正在谈论的弧线是灰色虚线最终结果中的橙色弧线。

要抚摸的路径

抚摸我们首先需要的path。 这基本上就像移动到起始点一样简单,并从当前angular度到中心线所需的angular度围绕中心绘制一条弧线

 CGMutablePathRef arc = CGPathCreateMutable(); CGPathMoveToPoint(arc, NULL, startPoint.x, startPoint.y); CGPathAddArc(arc, NULL, centerPoint.x, centerPoint.y, radius, startAngle, endAngle, YES); 

然后,当你有这个path(单一的弧),你可以创build一个特定的宽度的新的段。 由此产生的path将有两条直线和两条弧线。 中风发生在中心向内和向外的等距离。

 CGFloat lineWidth = 10.0; CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL, lineWidth, kCGLineCapButt, kCGLineJoinMiter, // the default 10); // 10 is default miter limit 

画画

接下来是绘图,通常有两个主要的select: drawRect:核心graphicsdrawRect:或Core Animation的形状图层。 核心graphics将给你更强大的绘图,但核心animation将给你更好的animation性能。 由于path涉及纯Coraanimation将无法正常工作。 你最终会得到奇怪的文物。 但是,我们可以通过绘制图层的graphics上下文来组合图层和核心graphics。

填充和抚摸段

我们已经有了基本的形状,但在添加渐变和阴影之前,我会做一个基本的填充和描边(在图像中有一个黑色的描边)。

 CGContextRef c = UIGraphicsGetCurrentContext(); CGContextAddPath(c, strokedArc); CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor); CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor); CGContextDrawPath(c, kCGPathFillStroke); 

这会在屏幕上放置这样的东西

填充和抚摸形状

添加阴影

我要改变顺序,在渐变之前做阴影。 为了绘制阴影,我们需要为上下文configuration一个阴影,并绘制填充形状以用阴影绘制。 然后我们需要恢复上下文(到阴影之前)并再次描边形状。

 CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor; CGContextSaveGState(c); CGContextSetShadowWithColor(c, CGSizeMake(0, 2), // Offset 3.0, // Radius shadowColor); CGContextFillPath(c); CGContextRestoreGState(c); // Note that filling the path "consumes it" so we add it again CGContextAddPath(c, strokedArc); CGContextStrokePath(c); 

在这一点上,结果是这样的

在这里输入图像说明

绘制渐变

对于渐变,我们需要一个渐变层。 我在这里做了一个非常简单的双色渐变,但是你可以自定义所有你想要的。 要创build渐变,我们需要获取颜色和合适的颜色空间。 然后,我们可以在填充顶部绘制渐变(但在中风之前)。 我们还需要将渐变掩盖到与以前相同的path。 要做到这一点,我们剪辑path。

 CGFloat colors [] = { 0.75, 1.0, // light gray (fully opaque) 0.90, 1.0 // lighter gray (fully opaque) }; CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2); CGColorSpaceRelease(baseSpace), baseSpace = NULL; CGContextSaveGState(c); CGContextAddPath(c, strokedArc); CGContextClip(c); CGRect boundingBox = CGPathGetBoundingBox(strokedArc); CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox)); CGPoint gradientEnd = CGPointMake(0, CGRectGetMaxY(boundingBox)); CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0); CGGradientRelease(gradient), gradient = NULL; CGContextRestoreGState(c); 

这完成了绘图,因为我们目前有这个结果

被掩盖的梯度

animation

当涉及到形状的animation时,它已经被写入:使用自定义CALayeranimation馅饼切片 。 如果您尝试通过简单的animation设置path属性来绘制graphics,则会在animation过程中看到一些非常时髦的path翘曲。 在下面的图片中,阴影和渐变已经完整保留。

时髦的路径翘曲

我build议你把我在这个答案中发布的绘图代码,并采用它从该文章的animation代码。 那么你应该结束你所要求的。


供参考:使用Core Animation的同一张图纸

平原的形状

 CAShapeLayer *segment = [CAShapeLayer layer]; segment.fillColor = [UIColor lightGrayColor].CGColor; segment.strokeColor = [UIColor blackColor].CGColor; segment.lineWidth = 1.0; segment.path = strokedArc; [self.view.layer addSublayer:segment]; 

添加阴影

该图层具有一些与阴影相关的属性,您可以自定义。 无论您应该设置shadowPath属性以提高性能。

 segment.shadowColor = [UIColor blackColor].CGColor; segment.shadowOffset = CGSizeMake(0, 2); segment.shadowOpacity = 0.75; segment.shadowRadius = 3.0; segment.shadowPath = segment.path; // Important for performance 

绘制渐变

 CAGradientLayer *gradient = [CAGradientLayer layer]; gradient.colors = @[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor, // light gray (id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray gradient.frame = CGPathGetBoundingBox(segment.path); 

如果我们现在绘制渐变,它将在形状的顶部,而不是在它的内部。 不,我们不能有渐变填充的形状(我知道你在想这个)。 我们需要掩盖渐变,以便它走出细分市场。 要做到这一点,我们创build另一个层作为该部分的面具。 它必须是另外一层,如果掩码是图层层次结构的一部分,则文档清楚地表明该行为是“未定义的”。 由于蒙版的坐标系将与渐变的子图层的坐标系相同,因此在设置蒙版之前,我们必须翻译细分的形状。

 CAShapeLayer *mask = [CAShapeLayer layer]; CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame), -CGRectGetMinY(gradient.frame)); mask.path = CGPathCreateCopyByTransformingPath(segment.path, &translation); gradient.mask = mask; 

Quartz 2D编程指南”介绍了您需要的一切。 我build议你看看它。

但是,把它们放在一起可能很难,所以我会带你走过去。 我们将编写一个函数,它需要一个大小,并返回一个粗略的像一个段的图像:

圆弧,轮廓,渐变和阴影

我们开始这样的函数定义:

 static UIImage *imageWithSize(CGSize size) { 

我们需要一个常数来表示段的厚度:

  static CGFloat const kThickness = 20; 

以及概述该线段的线的宽度的常数:

  static CGFloat const kLineWidth = 1; 

和阴影的大小不变:

  static CGFloat const kShadowWidth = 8; 

接下来我们需要创build一个图像上下文来绘制:

  UIGraphicsBeginImageContextWithOptions(size, NO, 0); { 

我在该行的末尾放置了左括号,因为我喜欢额外的缩进级别,以提醒我稍后调用UIGraphicsEndImageContext

由于我们需要调用的许多函数是Core Graphics(又名Quartz 2D)函数,而不是UIKit函数,我们需要获取CGContext

  CGContextRef gc = UIGraphicsGetCurrentContext(); 

现在我们准备好开始了。 首先我们在path中添加一条弧线。 弧线沿着我们要绘制的线段的中心运行:

  CGContextAddArc(gc, size.width / 2, size.height / 2, (size.width - kThickness - kLineWidth) / 2, -M_PI / 4, -3 * M_PI / 4, YES); 

现在我们要求Core Graphics用一个描述path的“描边”版本replacepath。 我们首先将笔画的粗细设置为我们想要的片段的厚度:

  CGContextSetLineWidth(gc, kThickness); 

我们将线条样式设置为“对接”,这样我们就可以平衡两端了 :

  CGContextSetLineCap(gc, kCGLineCapButt); 

然后我们可以要求Core Graphics用一个描边的版本replacepath:

  CGContextReplacePathWithStrokedPath(gc); 

要用线性渐变填充此path,我们必须告诉Core Graphics将所有操作剪辑到path的内部。 这样做会使Core Graphics重置path,但稍后我们需要path在边缘绘制黑线。 所以我们在这里复制path:

  CGPathRef path = CGContextCopyPath(gc); 

由于我们希望该段投下阴影,所以在执行任何绘制之前,我们将设置阴影参数:

  CGContextSetShadowWithColor(gc, CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2, [UIColor colorWithWhite:0 alpha:0.3].CGColor); 

我们要填充段(用渐变)和笔触(绘制黑色轮廓)。 我们希望两个操作都有一个影子。 我们通过开始一个透明层告诉Core Graphics:

  CGContextBeginTransparencyLayer(gc, 0); { 

我在该行的末尾放置了一个左括号,因为我喜欢有一个额外的缩进级别来提醒我稍后调用CGContextEndTransparencyLayer

由于我们要改变上下文的剪辑区域进行填充,但是当我们稍后画出轮廓时我们不想剪切,我们需要保存graphics状态:

  CGContextSaveGState(gc); { 

我在该行的末尾放置了一个左括号,因为我喜欢有一个额外的缩进级别来提醒我稍后调用CGContextRestoreGState

要用渐变填充path,我们需要创build一个渐变对象:

  CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)@[ (__bridge id)[UIColor grayColor].CGColor, (__bridge id)[UIColor whiteColor].CGColor ], (CGFloat[]){ 0.0f, 1.0f }); CGColorSpaceRelease(rgb); 

我们还需要找出梯度的起点和终点。 我们将使用path边界框:

  CGRect bbox = CGContextGetPathBoundingBox(gc); CGPoint start = bbox.origin; CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox)); 

我们将强制梯度绘制水平或垂直,以较长者为准:

  if (bbox.size.width > bbox.size.height) { end.y = start.y; } else { end.x = start.x; } 

现在我们终于有了我们需要绘制渐变的一切。 首先我们夹到path:

  CGContextClip(gc); 

然后我们绘制渐变:

  CGContextDrawLinearGradient(gc, gradient, start, end, 0); 

然后我们可以释放渐变并恢复保存的graphics状态:

  CGGradientRelease(gradient); } CGContextRestoreGState(gc); 

当我们调用CGContextClip ,Core Graphics重置了上下文的path。 path不是保存的graphics状态的一部分; 这就是为什么我们提前做了一个副本。 现在可以使用该副本再次在上下文中设置path:

  CGContextAddPath(gc, path); CGPathRelease(path); 

现在我们可以画出path,画出段的黑色轮廓:

  CGContextSetLineWidth(gc, kLineWidth); CGContextSetLineJoin(gc, kCGLineJoinMiter); [[UIColor blackColor] setStroke]; CGContextStrokePath(gc); 

接下来我们告诉Core Graphics来结束透明层。 这将使它看起来我们已经绘制,并添加下面的阴影:

  } CGContextEndTransparencyLayer(gc); 

现在我们都在完成绘图。 我们要求UIKit从图像上下文创buildUIImage ,然后销毁上下文并返回图像:

  UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } 

你可以在这个要点中find所有的代码。

这是Rob Mayoff答案的Swift 3版本。 只要看看这种语言有多高效! 这可能是MView.swift文件的内容:

 import UIKit class MView: UIView { var size = CGSize.zero override init(frame: CGRect) { super.init(frame: frame) size = frame.size } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } var niceImage: UIImage { let kThickness = CGFloat(20) let kLineWidth = CGFloat(1) let kShadowWidth = CGFloat(8) UIGraphicsBeginImageContextWithOptions(size, false, 0) let gc = UIGraphicsGetCurrentContext()! gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2), radius: (size.width - kThickness - kLineWidth)/2, startAngle: -45°, endAngle: -135°, clockwise: true) gc.setLineWidth(kThickness) gc.setLineCap(.butt) gc.replacePathWithStrokedPath() let path = gc.path! gc.setShadow( offset: CGSize(width: 0, height: kShadowWidth/2), blur: kShadowWidth/2, color: UIColor.gray.cgColor ) gc.beginTransparencyLayer(auxiliaryInfo: nil) gc.saveGState() let rgb = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient( colorsSpace: rgb, colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray, locations: [CGFloat(0), CGFloat(1)])! let bbox = path.boundingBox let startP = bbox.origin var endP = CGPoint(x: bbox.maxX, y: bbox.maxY); if (bbox.size.width > bbox.size.height) { endP.y = startP.y } else { endP.x = startP.x } gc.clip() gc.drawLinearGradient(gradient, start: startP, end: endP, options: CGGradientDrawingOptions(rawValue: 0)) gc.restoreGState() gc.addPath(path) gc.setLineWidth(kLineWidth) gc.setLineJoin(.miter) UIColor.black.setStroke() gc.strokePath() gc.endTransparencyLayer() let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image } override func draw(_ rect: CGRect) { niceImage.draw(at:.zero) } } 

从这样的viewController中调用它:

 let vi = MView(frame: self.view.bounds) self.view.addSubview(vi) 

为了度量弧度转换,我创build了° postfix运算符。 所以你现在可以使用例如45° ,这是从45度转换为弧度。 这个例子是用于Ints的,如果你有这个需要的话,也可以把它们扩展为Floattypes:

 postfix operator ° protocol IntegerInitializable: ExpressibleByIntegerLiteral { init (_: Int) } extension Int: IntegerInitializable { postfix public static func °(lhs: Int) -> CGFloat { return CGFloat(lhs) * .pi / 180 } } 

把这段代码放到一个实用的swift文件中。