ios 从圆形或甜甜圈中绘制线段

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15866179/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:05:24  来源:igfitidea点击:

Draw segments from a circle or donut

ioscore-animationcore-graphics

提问by Chris Baxter

I've been trying to figure out a way to draw segments as illustrated in the following image:

我一直试图找出一种绘制线段的方法,如下图所示:

enter image description here

在此处输入图片说明

I'd like to:

我想:

  1. draw the segment
  2. include gradients
  3. include shadows
  4. animate the drawing from 0 to n angle
  1. 绘制线段
  2. 包括渐变
  3. 包括阴影
  4. 动画从 0 到 n 角度的绘图

I've been attempting to do this with CGContextAddArcand similar calls but not getting very far.

我一直在尝试使用CGContextAddArc和类似的电话来做到这一点,但并没有走得太远。

Can anyone help ?

任何人都可以帮忙吗?

回答by David R?nnqvist

There are many parts to your question.

你的问题有很多部分。

Getting the path

获取路径

Creating the path for such a segment shouldn't be too hard. There are two arcs and two straight lines. I've previously explained how you can break down a path like thatso I won't do it here. Instead I'm going to be fancy and create the path by stroking another path. You can of course read the breakdown and construct the path yourself. The arc I'm talking about stroking is the orange arc inside the gray dashed end-result.

为这样的段创建路径应该不会太难。有两条弧线和两条直线。我之前已经解释过如何分解这样的路径,所以我不会在这里做。相反,我会很花哨,并通过抚摸另一条路径来创建路径。您当然可以阅读细分并自己构建路径。我所说的抚摸的弧线是灰色虚线最终结果内的橙色弧线。

Path to be stroked

要抚摸的路径

To stroke the path we first need it. that is basically as simple as moving tothe start point and drawing an arc around the centerfrom the current angle to the angle you want the segment to cover.

要抚摸路径,我们首先需要它。这基本上就像移动到起点并围绕中心从当前角度绘制到您希望线段覆盖的角度一样简单。

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

Then when you have that path (the single arc) you can create the new segment by stroking it with a certain width. The resulting path is going to have the two straight lines and the two arcs. The stroke happens from the center an equal distance inwards and outwards.

然后,当您拥有该路径(单弧)时,您可以通过以特定宽度抚摸它来创建新线段。结果路径将有两条直线和两条弧线。中风从中心向内和向外发生相等的距离。

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

Drawing

画画

Next up is drawing and there are generally two main choices: Core Graphics in drawRect:or shape layers with Core Animation. Core Graphics is going to give you the more powerful drawing but Core Animation is going to give you the better animation performance. Since paths are involved pure Cora Animation won't work. You will end up with strange artifacts. We can however use a combination of layers and Core Graphics by drawing the the graphics context of the layer.

接下来是绘图,通常有两个主要选择:Core Graphics in drawRect:or shape layers with Core Animation。Core Graphics 将为您提供更强大的绘图功能,但 Core Animation 将为您提供更好的动画性能。由于涉及路径,因此纯 Cora 动画将不起作用。你最终会得到奇怪的文物。然而,我们可以通过绘制图层的图形上下文来使用图层和核心图形的组合。

Filling and stroking the segment

填充和抚摸段

We already have the basic shape but before we add gradients and shadows to it I will do a basic fill and stroke (you have a black stroke in your image).

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

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

That will put something like this on screen

这将在屏幕上显示这样的内容

Filled and stroked shape

Filled and stroked shape

Adding shadows

添加阴影

I'm going to change the order and do the shadow before the gradient. To draw the shadow we need to configure a shadow for the context and draw fill the shape to draw it with the shadow. Then we need to restore the context (to before the shadow) and stroke the shape again.

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

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

At this point the result is something like this

此时的结果是这样的

enter image description here

enter image description here

Drawing the gradient

绘制渐变

For the gradient we need a gradient layer. I'm doing a very simple two color gradient here but you can customize it all you want. To create the gradient we need to get the colors and the suitable color space. Then we can draw the gradient on top of the fill (but before the stroke). We also need to mask the gradient to the same path as before. To do this we clip the 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);

This finishes the drawing as we currently have this result

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

Masked gradient

Masked gradient

Animation

动画片

When it comes to the animation of the shape it has all been written before: Animating Pie Slices Using a Custom CALayer. If you try doing the drawing by simply animating the path property you are going to see some really funky warping of the path during the animation. The shadow and gradient has been left intact for illustrative purposes in the image below.

说到形状的动画,之前都写过:Animating Pie Slices Using a Custom CALayer。如果您尝试通过简单地为 path 属性设置动画来进行绘图,您将在动画过程中看到一些非常时髦的路径扭曲。为了便于说明,下图中的阴影和渐变保持不变。

Funky warping of path

Funky warping of path

I suggest that you take the drawing code that I've posted in this answer and adopt it to the animation code from that article. Then you should end up with the what you are asking for.

我建议您采用我在此答案中发布的绘图代码,并将其应用于该文章中的动画代码。然后你应该得到你所要求的。



For reference: the same drawing using Core Animation

供参考:使用 Core Animation 的相同绘图

Plain shape

素色

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

Adding shadows

添加阴影

The layer has some shadow related properties that it's up to you to customize. Howereveryou should set the shadowPathproperty for improved performance.

该图层有一些与阴影相关的属性,由您来自定义。但是,您应该设置该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

Drawing the gradient

绘制渐变

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

If we drew the gradient now it would be on top of the shape and not inside it. No, we can't have a gradient fill of the shape (I know you were thinking of it). We need to mask the gradient so that it go outside the segment. To do that we create anotherlayer to be the mask of that segment. It has to beanother layer, the documentation is clear that the behavior is "undefined" if the mask is part of the layer hierarchy. Since the mask's coordinate system is going to be the same as that of sublayers to the gradient we will have to translate the segment shape before setting it.

如果我们现在绘制渐变,它将位于形状的顶部而不是内部。不,我们不能对形状进行渐变填充(我知道您正在考虑它)。我们需要屏蔽渐变,使其在段之外。为此,我们创建另一个图层作为该段的蒙版。它必须是另一层,如果掩码是层层次结构的一部分,则文档清楚地表明行为是“未定义的”。由于蒙版的坐标系将与子图层的坐标系相同,因此我们必须在设置之前转换线段形状。

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

回答by rob mayoff

Everything you need is covered in the Quartz 2D Programming Guide. I suggest you look through it.

Quartz 2D 编程指南中涵盖了您需要的一切。我建议你仔细看看。

However, it can be difficult to put it all together, so I'll walk you through it. We'll write a function that takes a size and returns an image that looks roughly like one of your segments:

但是,将它们放在一起可能很困难,因此我将引导您完成。我们将编写一个函数,该函数接受一个大小并返回一张与您的片段大致相似的图像:

arc with outline, gradient, and shadow

arc with outline, gradient, and shadow

We start the function definition like this:

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

static UIImage *imageWithSize(CGSize size) {

We'll need a constant for the thickness of the segment:

我们需要一个用于分段厚度的常数:

    static CGFloat const kThickness = 20;

and a constant for the width of the line outlining the segment:

以及用于勾勒出线段的线宽的常量:

    static CGFloat const kLineWidth = 1;

and a constant for the size of the shadow:

以及阴影大小的常数:

    static CGFloat const kShadowWidth = 8;

Next we need to create an image context in which to draw:

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

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); {

I put a left brace on the end of that line because I like an extra level of indentation to remind me to call UIGraphicsEndImageContextlater.

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

Since a lot of the functions we need to call are Core Graphics (aka Quartz 2D) functions, not UIKit functions, we need to get the CGContext:

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

        CGContextRef gc = UIGraphicsGetCurrentContext();

Now we're ready to really get started. First we add an arc to the path. The arc runs along the center of the segment we want to draw:

现在我们已准备好真正开始。首先,我们向路径添加一条弧线。弧沿我们要绘制的线段的中心运行:

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

Now we'll ask Core Graphics to replace the path with a “stroked” version that outlines the path. We first set the thickness of the stroke to the thickness we want the segment to have:

现在,我们将要求 Core Graphics 将路径替换为勾勒出路径轮廓的“描边”版本。我们首先将笔划的粗细设置为我们希望该段具有的粗细:

        CGContextSetLineWidth(gc, kThickness);

and we set the line cap style to “butt” so we'll have squared-off ends:

并且我们将线帽样式设置为“对接”,这样我们就会有方形的末端

        CGContextSetLineCap(gc, kCGLineCapButt);

Then we can ask Core Graphics to replace the path with a stroked version:

然后我们可以要求 Core Graphics 用描边版本替换路径:

        CGContextReplacePathWithStrokedPath(gc);

To fill this path with a linear gradient, we have to tell Core Graphics to clip all operations to the interior of the path. Doing so will make Core Graphics reset the path, but we'll need the path later to draw the black line around the edge. So we'll copy the path here:

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

        CGPathRef path = CGContextCopyPath(gc);

Since we want the segment to cast a shadow, we'll set the shadow parameters before we do any drawing:

由于我们希望该段投射阴影,因此我们将在进行任何绘图之前设置阴影参数:

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

We're going to both fill the segment (with a gradient) and stroke it (to draw the black outline). We want a single shadow for both operations. We tell Core Graphics that by beginning a transparency layer:

我们将填充该段(使用渐变)并对其进行描边(绘制黑色轮廓)。我们希望两个操作都有一个阴影。我们告诉 Core Graphics 通过开始一个透明层:

        CGContextBeginTransparencyLayer(gc, 0); {

I put a left brace on the end of that line because I like to have an extra level of indentation to remind me to call CGContextEndTransparencyLayerlater.

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

Since we're going to change the context's clip region for filling, but we won't want to clip when we stroke the outline later, we need to save the graphics state:

由于我们要更改上下文的剪辑区域以进行填充,但是我们不想在稍后描边轮廓时进行剪辑,因此我们需要保存图形状态:

            CGContextSaveGState(gc); {

I put a left brace on the end of that line because I like to have an extra level of indentation to remind me to call CGContextRestoreGStatelater.

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

To fill the path with a gradient, we need to create a gradient object:

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

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

We also need to figure out a start point and an end point for the gradient. We'll use the path bounding box:

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

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

and we'll force the gradient to be drawn either horizontally or vertically, whichever is longer:

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

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

Now we finally have everything we need to draw the gradient. First we clip to the path:

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

                CGContextClip(gc);

Then we draw the gradient:

然后我们绘制渐变:

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

Then we can release the gradient and restore the saved graphics state:

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

                CGGradientRelease(gradient);
            } CGContextRestoreGState(gc);

When we called CGContextClip, Core Graphics reset the context's path. The path isn't part of the saved graphics state; that's why we made a copy earlier. Now it's time to use that copy to set the path in the context again:

当我们调用 时CGContextClip,Core Graphics 重置上下文的路径。路径不是保存的图形状态的一部分;这就是为什么我们早些时候制作了一份副本。现在是时候使用该副本再次在上下文中设置路径了:

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

Now we can stroke the path, to draw the black outline of the segment:

现在我们可以描边路径,绘制线段的黑色轮廓:

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

Next we tell Core Graphics to end the transparency layer. This will make it look at what we've drawn and add the shadow underneath:

接下来我们告诉 Core Graphics 结束透明层。这将使它查看我们绘制的内容并在下面添加阴影:

        } CGContextEndTransparencyLayer(gc);

Now we're all done drawing. We ask UIKit to create a UIImagefrom the image context, then destroy the context and return the image:

现在我们都画完了。我们要求 UIKitUIImage从图像上下文创建一个,然后销毁上下文并返回图像:

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

You can find the code all together in this gist.

你可以在这个 gist 中找到所有的代码。

回答by t1ser

This is a Swift 3version of Rob Mayoff's answer. Just see how much more efficient this language is! This could be the contents of a MView.swift file:

这是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)
    }
}

Call it from a viewController like this:

从这样的 viewController 调用它:

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

To do the degrees to radians conversions I have created the °postfix operator. So you can now use e.g. 45°and this does the conversion from 45 degrees to radians. This example is for Ints, extend these also for the Float types if you have the need:

为了将度数转换为弧度,我创建了°后缀运算符。因此,您现在可以使用例如45° 来完成从 45 度到弧度的转换。此示例适用于 Int,如果需要,也可以将它们扩展为 Float 类型:

postfix operator °

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

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

Put this code into a utilities swift file.

将此代码放入实用程序 swift 文件中。