从圆圈或甜甜圈中画出分段

我一直试图找到一种方法来绘制片段,如下图所示:

enter image description here

我想:

  1. 画一段
  2. 包括梯度
  3. 包括阴影
  4. 从0到 n 的角度动画绘图

我一直试图这样做与 CGContextAddArc和类似的呼叫,但没有得到很远。

有人能帮忙吗?

33403 次浏览

你需要的一切都在 < em > 石英二维编程指南 里,我建议你看一下。

然而,把它们放在一起是很困难的,所以我将带领你通过它。我们将编写一个函数,该函数接受一个大小,并返回一个看起来大致类似于片段之一的图像:

arc with outline, gradient, and shadow

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

static UIImage *imageWithSize(CGSize size) {

我们需要一个常数来测量这个部分的厚度:

    static CGFloat const kThickness = 20;

以及一个用于描述该段的线的宽度的常数:

    static CGFloat const kLineWidth = 1;

以及阴影大小的常数:

    static CGFloat const kShadowWidth = 8;

接下来,我们需要创建一个图像上下文,在其中绘制:

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); {

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

因为我们需要调用的许多函数是核心图形(又名 Quartz 2D)函数,而不是 UIKit 函数,所以我们需要得到 CGContext:

        CGContextRef gc = UIGraphicsGetCurrentContext();

现在我们可以开始了。首先我们在路径上添加一个弧线。这条弧线沿着我们要画的部分的中心延伸:

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

现在,我们将要求 Core Graphics 用描绘路径的“笔画”版本替换路径。我们首先将行程的厚度设置为我们希望线段具有的厚度:

        CGContextSetLineWidth(gc, kThickness);

我们把线帽的样式设置为“臀部”,这样我们就有了平方的两端:

        CGContextSetLineCap(gc, kCGLineCapButt);

然后我们可以让 Core Graphics 用笔画版本替换路径:

        CGContextReplacePathWithStrokedPath(gc);

为了用线性渐变填充这个路径,我们必须告诉 Core Graphics 将所有操作剪切到路径的内部。这样做将使核心图形重置的路径,但我们需要的路径,以后绘制黑线周围的边缘。我们在这里复制路径:

        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

因为我们要更改上下文的剪辑区域以便填充,但是我们不想在稍后中风轮廓时剪辑,所以我们需要保存图形状态:

            CGContextSaveGState(gc); {

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

为了用渐变填充路径,我们需要创建一个渐变对象:

                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);

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

                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;
}

现在我们终于有了绘制渐变所需的所有东西。首先我们剪切到路径:

                CGContextClip(gc);

然后我们画出梯度:

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

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

                CGGradientRelease(gradient);
} CGContextRestoreGState(gc);

当我们调用 CGContextClip时,核心图形重置了上下文的路径。路径不是保存的图形状态的一部分; 这就是我们之前复制的原因。现在是时候使用该副本在上下文中再次设置路径了:

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

现在我们可以描绘这条路线,画出这个部分的黑色轮廓:

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

接下来,我们告诉核心图形结束透明层。这将使它看到我们所绘制的,并添加下面的阴影:

        } CGContextEndTransparencyLayer(gc);

现在我们都画完了。我们让 UIKit 根据图像上下文创建一个 UIImage,然后销毁上下文并返回图像:

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

你可以找到所有的代码一起 在这个要点

你的问题有很多部分。

找到路径

为这样一个细分市场开辟道路应该不会太难。有两条弧线和两条直线。我有 前面解释了如何打破这样的路径,所以我不会在这里做。取而代之的是,我要变得更有想象力,通过抚摸另一条道路来创造这条道路。你当然可以阅读分解并自己构建路径。我所说的弧线是灰色虚线最终结果中的橙色弧线。

Path to be stroked

我们首先需要它来指引我们的道路。这基本上是简单的作为 搬去的起点和画一个 环绕中心的圆弧从当前角度的角度,你想要的部分覆盖。

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

然后,当你有了那条路径(单个弧线) ,你就可以用一定的宽度抚摸它来创建新的线段。得到的路径有两条直线和两条弧。中风发生在从中心向内和向外相等的距离。

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

画画

接下来是绘图,通常有两个主要的选择: 核心图形在 drawRect:或形状层与核心动画。核心图形将给你更强大的绘图,但核心动画将给你更好的动画性能。因为路径涉及纯 Cora 动画将不工作。你最终会得到奇怪的艺术品。然而,我们可以通过绘制图层的图形上下文来使用图层和核心图形的组合。

填充和抚摸部分

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

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

会把这样的东西放到屏幕上

Filled and stroked shape

添加阴影

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

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);

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

enter image description here

绘制渐变

对于渐变,我们需要一个渐变层。我正在做一个非常简单的两色渐变在这里,但你可以自定义它所有你想要的。为了创建渐变,我们需要得到的颜色和适当的颜色空间。然后我们可以在填充顶部(但在笔画之前)绘制渐变。我们还需要将渐变掩盖到与之前相同的路径。为了做到这一点,我们剪断路径。

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);

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

Masked gradient

动画

当谈到动画的形状,它有 以前都写过: 使用定制的分层器动画馅饼切片。如果您尝试通过简单地动画路径属性来完成绘图,那么您将在动画过程中看到一些真正时髦的路径变形。阴影和渐变在下面的图片中完好无损,用于说明目的。

Funky warping of path

我建议您使用我在这个答案中发布的绘图代码,并将其应用到那篇文章中的动画代码中。那么你应该得到你想要的。


供参考: 同一绘图使用核心动画

形状很普通

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);

如果我们现在画渐变,它会在形状的顶部,而不是内部。不,我们不能有一个梯度填充的形状(我知道你正在考虑它)。我们需要掩盖梯度,这样它就会出现在线段之外。为此,我们创建 另一个层作为该段的掩码。它是 肯定是的另一层,文档清楚地表明,如果掩码是层次结构的一部分,行为是“未定义的”。由于掩模的坐标系和子图层的相同,我们必须在设置之前转换段的形状。

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

这是 Swift 3版本的 Rob Mayoff 的回答。看看这种语言的效率有多高!这可能是 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)
}
}

从这样的视角调用它控制器:

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

为了进行弧度转换,我创建了 °后缀操作符。所以你现在可以使用例如 45 ° 这样就可以从45度转换到弧度。 这个例子是针对 Ints 的,如果需要,也可以扩展 Float 类型:

postfix operator °


protocol IntegerInitializable: ExpressibleByIntegerLiteral {
init (_: Int)
}


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

将此代码放入一个工具快速文件。