如何用颜色渐变描边绘制圆形path

我想在iOS和MacOS上用下面的图片绘制一个带有颜色渐变笔划的圆:

圆路径

是否有可能用CAShapeLayerNSBezierPath / CGPath ? 或者其他方式?

不是真的,但是你可以用不同的颜色画出一系列的弧线:

 import Cocoa /// This draws an arc, of length `maxAngle`, ending at `endAngle. This is `@IBDesignable`, so if you /// put this in a separate framework target, you can use this class in Interface Builder. The only /// property that is not `@IBInspectable` is the `lineCapStyle` (as IB doesn't know how to show that). /// /// If you want to make this animated, just use a `CADisplayLink` update the `endAngle` property (and /// this will automatically re-render itself whenever you change that property). @IBDesignable class GradientArcView: NSView { /// Width of the stroke. @IBInspectable var lineWidth: CGFloat = CGFloat(3) { didSet { setNeedsDisplay(bounds) } } /// Color of the stroke (at full alpha, at the end). @IBInspectable var strokeColor: NSColor = NSColor.blue { didSet { setNeedsDisplay(bounds) } } /// Where the arc should end, measured in degrees, where 0 = "3 o'clock". @IBInspectable var endAngle: CGFloat = 0 { didSet { setNeedsDisplay(bounds) } } /// What is the full angle of the arc, measured in degrees, eg 180 = half way around, 360 = all the way around, etc. @IBInspectable var maxAngle: CGFloat = 360 { didSet { setNeedsDisplay(bounds) } } /// What is the shape at the end of the arc. var lineCapStyle: NSLineCapStyle = .squareLineCapStyle { didSet { setNeedsDisplay(bounds) } } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) let gradations = 255 let startAngle = -endAngle + maxAngle let center = NSPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2) let radius = (min(bounds.size.width, bounds.size.height) - lineWidth) / 2 var angle = startAngle for i in 1 ... gradations { let percent = CGFloat(i) / CGFloat(gradations) let endAngle = startAngle - percent * maxAngle let path = NSBezierPath() path.lineWidth = lineWidth path.lineCapStyle = lineCapStyle path.appendArc(withCenter: center, radius: radius, startAngle: angle, endAngle: endAngle, clockwise: true) strokeColor.withAlphaComponent(percent).setStroke() path.stroke() angle = endAngle } } } 

在这里输入图像说明

由于你的path是一个圆,你要求的东西相当于一个angular度的梯度 ,也就是说,当我们在圆饼周围扫描一个半径时,就会改变颜色。 没有内置的方法来做到这一点,但有一个伟大的图书馆,为您做到这一点:

https://github.com/paiv/AngleGradientLayer

诀窍是你画圆angular中心的angular度渐变,然后把一个面具 ,以便它只出现在你的圆形笔触应该是的地方。

在这里输入图像说明

这里有一些代码适合我。 里面有animation,但是你可以使用相同的原理来制作一个渐变的中风。

A.创build一个自定义视图“甜甜圈”,并把它放在标题中:

 @interface Donut : UIView @property UIColor * fromColour; @property UIColor * toColour; @property UIColor * baseColour; @property float lineWidth; @property float duration; -(void)layout; -(void)animateTo:(float)percentage; 

B.然后做了基本的视图设置,并写了这两个方法:

 -(void)layout{ //vars float dimension = self.frame.size.width; //1. layout views //1.1 layout base track UIBezierPath * donut = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(lineWidth/2, lineWidth/2, dimension-lineWidth, dimension-lineWidth)]; CAShapeLayer * baseTrack = [CAShapeLayer layer]; baseTrack.path = donut.CGPath; baseTrack.lineWidth = lineWidth; baseTrack.fillColor = [UIColor clearColor].CGColor; baseTrack.strokeStart = 0.0f; baseTrack.strokeEnd = 1.0f; baseTrack.strokeColor = baseColour.CGColor; baseTrack.lineCap = kCALineCapButt; [self.layer addSublayer:baseTrack]; //1.2 clipView has mask applied to it UIView * clipView = [UIView new]; clipView.frame = self.bounds; [self addSubview:clipView]; //1.3 rotateView transforms with strokeEnd rotateView = [UIView new]; rotateView.frame = self.bounds; [clipView addSubview:rotateView]; //1.4 radialGradient holds an image of the colours UIImageView * radialGradient = [UIImageView new]; radialGradient.frame = self.bounds; [rotateView addSubview:radialGradient]; //2. create colours fromColour --> toColour and add to an array //2.1 holds all colours between fromColour and toColour NSMutableArray * spectrumColours = [NSMutableArray new]; //2.2 get RGB values for both colours double fR, fG, fB; //fromRed, fromGreen etc double tR, tG, tB; //toRed, toGreen etc [fromColour getRed:&fR green:&fG blue:&fB alpha:nil]; [toColour getRed:&tR green:&tG blue:&tB alpha:nil]; //2.3 determine increment between fromRed and toRed etc. int numberOfColours = 360; double dR = (tR-fR)/(numberOfColours-1); double dG = (tG-fG)/(numberOfColours-1); double dB = (tB-fB)/(numberOfColours-1); //2.4 loop through adding incrementally different colours //this is a gradient fromColour --> toColour for (int n = 0; n < numberOfColours; n++){ [spectrumColours addObject:[UIColor colorWithRed:(fR+n*dR) green:(fG+n*dG) blue:(fB+n*dB) alpha:1.0f]]; } //3. create a radial image using the spectrum colours //go through adding the next colour at an increasing angle //3.1 setup float radius = MIN(dimension, dimension)/2; float angle = 2 * M_PI/numberOfColours; UIBezierPath * bezierPath; CGPoint center = CGPointMake(dimension/2, dimension/2); UIGraphicsBeginImageContextWithOptions(CGSizeMake(dimension, dimension), true, 0.0); UIRectFill(CGRectMake(0, 0, dimension, dimension)); //3.2 loop through pulling the colour and adding for (int n = 0; n<numberOfColours; n++){ UIColor * colour = spectrumColours[n]; //colour for increment bezierPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:n * angle endAngle:(n + 1) * angle clockwise:YES]; [bezierPath addLineToPoint:center]; [bezierPath closePath]; [colour setFill]; [colour setStroke]; [bezierPath fill]; [bezierPath stroke]; } //3.3 create image, add to the radialGradient and end [radialGradient setImage:UIGraphicsGetImageFromCurrentImageContext()]; UIGraphicsEndImageContext(); //4. create a dot to add to the rotating view //this covers the connecting line between the two colours //4.1 set up vars float containsDots = (M_PI * dimension) /*circumference*/ / lineWidth; //number of dots in circumference float colourIndex = roundf((numberOfColours / containsDots) * (containsDots-0.5f)); //the nearest colour for the dot UIColor * closestColour = spectrumColours[(int)colourIndex]; //the closest colour //4.2 create dot UIImageView * dot = [UIImageView new]; dot.frame = CGRectMake(dimension-lineWidth, (dimension-lineWidth)/2, lineWidth, lineWidth); dot.layer.cornerRadius = lineWidth/2; dot.backgroundColor = closestColour; [rotateView addSubview:dot]; //5. create the mask mask = [CAShapeLayer layer]; mask.path = donut.CGPath; mask.lineWidth = lineWidth; mask.fillColor = [UIColor clearColor].CGColor; mask.strokeStart = 0.0f; mask.strokeEnd = 0.0f; mask.strokeColor = [UIColor blackColor].CGColor; mask.lineCap = kCALineCapRound; //5.1 apply the mask and rotate all by -90 (to move to the 12 position) clipView.layer.mask = mask; clipView.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(-90.0f)); } -(void)animateTo:(float)percentage { float difference = fabsf(fromPercentage - percentage); float fixedDuration = difference * duration; //1. animate stroke End CABasicAnimation * strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; strokeEndAnimation.duration = fixedDuration; strokeEndAnimation.fromValue = @(fromPercentage); strokeEndAnimation.toValue = @(percentage); strokeEndAnimation.fillMode = kCAFillModeForwards; strokeEndAnimation.removedOnCompletion = false; strokeEndAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; [mask addAnimation:strokeEndAnimation forKey:@"strokeEndAnimation"]; //2. animate rotation of rotateView CABasicAnimation * viewRotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; viewRotationAnimation.duration = fixedDuration; viewRotationAnimation.fromValue = @(DEGREES_TO_RADIANS(360 * fromPercentage)); viewRotationAnimation.toValue = @(DEGREES_TO_RADIANS(360 * percentage)); viewRotationAnimation.fillMode = kCAFillModeForwards; viewRotationAnimation.removedOnCompletion = false; viewRotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; [rotateView.layer addAnimation:viewRotationAnimation forKey:@"viewRotationAnimation"]; //3. update from percentage fromPercentage = percentage; } 

C.创build视图:

 Donut * donut = [Donut new]; donut.frame = CGRectMake(20, 100, 140, 140); donut.baseColour = [[UIColor blackColor] colorWithAlphaComponent:0.2f]; donut.fromColour = [UIColor redColor]; donut.toColour = [UIColor blueColor]; donut.lineWidth = 20.0f; donut.duration = 2.0f; [donut layout]; [tasteView addSubview:donut]; 

D.animation视图:

 [donut animateTo:0.5f]; 

E.说明:

甜甜圈视图通过创build基本轨道,clipView,rotateView和radialGradient imageView开始。 然后计算你想在甜甜圈中使用的两种颜色之间的360种颜色。 它通过增加颜色之间的rgb值来实现。 然后使用这些颜色创build径向渐变图像并添加到imageView。 因为我想使用kCALineCapRound,所以我添加了一个点来掩饰两种颜色相遇的地方。 整个事情需要旋转-90度,把它放在12点钟的位置。 然后一个面具应用到视图,给它的甜甜圈形状。

随着面具的strokeEnd被改变,它下面的'rotateView'视图也被旋转。 这给人的印象是,只要他们同步,线路正在增长/缩小。

你可能也需要这个:

 #define DEGREES_TO_RADIANS(x) (M_PI * (x) / 180.0) 

不幸的是, 只有线性和径向梯度; CG层(或以上)没有angular度梯度支撑。

这是非常烦人的,因为苹果已经开始大量使用这些渐变,例如在“观看活动”圈子中,或在计时器应用程序的“就寝时间”区域。

有黑客,你从你的圈子的外缘绘制成千上万行到中心,然后掩盖他们到你的形状。 这工作,但速度很慢(当我试图在屏幕上滚动时,帧下降)。

SpriteKit看起来最有前途: SKShader有一些有用的制服 (沿path的path长度和当前距离) – 将SKView拖放到您的应用程序很容易。 (在我有机会尝试这个之前,我的devise师放弃了渐变要求!)

或者,您可以使用Metal(MTKView)或Core Image(CIFilter)并编写自己的着色器。 不幸的是,使用Metal将意味着在iOS模拟器中没有更多的工作,所以CI将是两个更现实的解决scheme。