我一直在尝试找出一种绘制段的方法,如下图所示:
我想:
- 画段
- 包括渐变
- 包括阴影
- 从0到n角度对图形进行动画处理
我一直在尝试通过CGContextAddArc
类似的电话进行此操作,但距离还很远。
有人可以帮忙吗?
我一直在尝试找出一种绘制段的方法,如下图所示:
我想:
我一直在尝试通过CGContextAddArc
类似的电话进行此操作,但距离还很远。
有人可以帮忙吗?
Answers:
您的问题有很多部分。
为此段创建路径应该不会太困难。有两条弧线和两条直线。前面我已经解释了如何分解这样的路径,所以在这里我不会做。取而代之的是,我会幻想并通过抚摸另一条路径来创建路径。您当然可以阅读故障信息并自己构建路径。我所说的抚摸弧是灰色虚线最终结果中的橙色弧。
要摸索路径,我们首先需要它。这基本上很简单,只需移动到起点并从当前角度到您要分割的角度围绕中心画一条弧。
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:
带有Core Animation的Core Graphics或Shape层。Core Graphics将为您提供更强大的图形,而Core Animation将为您提供更好的动画性能。由于涉及路径,因此纯Cora动画将不起作用。您最终将得到奇怪的文物。但是,我们可以通过绘制图层的图形上下文来结合使用图层和Core Graphics。
我们已经具有基本形状,但是在向其添加渐变和阴影之前,我将进行基本的填充和描边(图像中有黑色描边)。
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);
由于我们当前具有此结果,因此完成了绘图
关于形状的动画,所有内容均已写过:使用自定义CALayer动画饼图。如果尝试通过简单设置动画路径属性来进行绘制,则会在动画过程中看到路径的一些真正时髦的变形。下图出于说明目的将阴影和渐变保留完整。
我建议您采用我在此答案中发布的绘图代码,并将其应用于该文章中的动画代码。然后,您应该得到您想要的。
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;
《Quartz 2D编程指南》中介绍了您需要的所有内容。我建议您仔细阅读。
但是,将所有内容放在一起可能很困难,因此我将带您逐步了解。我们将编写一个函数,该函数具有一个大小并返回一个大致类似于您的细分受众群之一的图像:
我们开始这样的函数定义:
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
以后打电话。
由于我们需要调用的许多函数是Core Graphics(又名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将所有操作都剪切到路径内部。这样做将使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
,Core Graphics会重置上下文的路径。该路径不是保存的图形状态的一部分。这就是我们较早制作副本的原因。现在是时候使用该副本在上下文中再次设置路径了:
CGContextAddPath(gc, path);
CGPathRelease(path);
现在我们可以描画路径,以绘制该段的黑色轮廓:
CGContextSetLineWidth(gc, kLineWidth);
CGContextSetLineJoin(gc, kCGLineJoinMiter);
[[UIColor blackColor] setStroke];
CGContextStrokePath(gc);
接下来,我们告诉Core Graphics结束透明层。这将使其看起来像我们绘制的内容,并在下面添加阴影:
} CGContextEndTransparencyLayer(gc);
现在我们都完成了绘图。我们要求UIKitUIImage
从图像上下文创建一个,然后销毁上下文并返回图像:
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
您可以在本要点中找到所有代码。
这是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)
为了进行度到弧度的转换,我创建了°后缀运算符。因此,您现在可以使用例如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
}
}
将此代码放入实用程序快速文件中。