我一直试图找到一种方法来绘制片段,如下图所示:
我想:
我一直试图这样做与 CGContextAddArc和类似的呼叫,但没有得到很远。
CGContextAddArc
有人能帮忙吗?
你需要的一切都在 < em > 石英二维编程指南 里,我建议你看一下。
然而,把它们放在一起是很困难的,所以我将带领你通过它。我们将编写一个函数,该函数接受一个大小,并返回一个看起来大致类似于片段之一的图像:
我们像这样开始函数定义:
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。
UIGraphicsEndImageContext
因为我们需要调用的许多函数是核心图形(又名 Quartz 2D)函数,而不是 UIKit 函数,所以我们需要得到 CGContext:
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。
CGContextEndTransparencyLayer
因为我们要更改上下文的剪辑区域以便填充,但是我们不想在稍后中风轮廓时剪辑,所以我们需要保存图形状态:
CGContextSaveGState(gc); {
我在该行的末尾放了一个左大括号,因为我喜欢有一个额外的缩进级别来提醒我稍后调用 CGContextRestoreGState。
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时,核心图形重置了上下文的路径。路径不是保存的图形状态的一部分; 这就是我们之前复制的原因。现在是时候使用该副本在上下文中再次设置路径了:
CGContextClip
CGContextAddPath(gc, path); CGPathRelease(path);
现在我们可以描绘这条路线,画出这个部分的黑色轮廓:
CGContextSetLineWidth(gc, kLineWidth); CGContextSetLineJoin(gc, kCGLineJoinMiter); [[UIColor blackColor] setStroke]; CGContextStrokePath(gc);
接下来,我们告诉核心图形结束透明层。这将使它看到我们所绘制的,并添加下面的阴影:
} CGContextEndTransparencyLayer(gc);
现在我们都画完了。我们让 UIKit 根据图像上下文创建一个 UIImage,然后销毁上下文并返回图像:
UIImage
UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
你可以找到所有的代码一起 在这个要点。
你的问题有很多部分。
为这样一个细分市场开辟道路应该不会太难。有两条弧线和两条直线。我有 前面解释了如何打破这样的路径,所以我不会在这里做。取而代之的是,我要变得更有想象力,通过抚摸另一条道路来创造这条道路。你当然可以阅读分解并自己构建路径。我所说的弧线是灰色虚线最终结果中的橙色弧线。
我们首先需要它来指引我们的道路。这基本上是简单的作为 搬去的起点和画一个 环绕中心的圆弧从当前角度的角度,你想要的部分覆盖。
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 动画将不工作。你最终会得到奇怪的艺术品。然而,我们可以通过绘制图层的图形上下文来使用图层和核心图形的组合。
drawRect:
我们已经有了基本的形状,但在我们添加渐变和阴影之前,我会做一个基本的填充和笔触(你有一个黑色的笔触在你的图像)。
CGContextRef c = UIGraphicsGetCurrentContext(); CGContextAddPath(c, strokedArc); CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor); CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor); CGContextDrawPath(c, kCGPathFillStroke);
会把这样的东西放到屏幕上
我要改变顺序,在渐变之前做阴影处理。为了绘制阴影,我们需要为上下文配置一个阴影,并绘制填充形状来用阴影绘制它。然后我们需要恢复上下文(到阴影之前)并再次描边形状。
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);
在这一点上,结果是这样的
对于渐变,我们需要一个渐变层。我正在做一个非常简单的两色渐变在这里,但你可以自定义它所有你想要的。为了创建渐变,我们需要得到的颜色和适当的颜色空间。然后我们可以在填充顶部(但在笔画之前)绘制渐变。我们还需要将渐变掩盖到与之前相同的路径。为了做到这一点,我们剪断路径。
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);
这完成了绘图,因为我们目前有这个结果
当谈到动画的形状,它有 以前都写过: 使用定制的分层器动画馅饼切片。如果您尝试通过简单地动画路径属性来完成绘图,那么您将在动画过程中看到一些真正时髦的路径变形。阴影和渐变在下面的图片中完好无损,用于说明目的。
我建议您使用我在这个答案中发布的绘图代码,并将其应用到那篇文章中的动画代码中。那么你应该得到你想要的。
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属性。
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 } }
将此代码放入一个工具快速文件。