如何用倾斜的渐变填充 CAShapeLayer
How to fill a CAShapeLayer with an angled gradient
如何使用渐变和 45 度角填充 CAShapeLayer()
?
例如,在Image 1中,下面的代码绘制一个正方形并填充图层蓝色(UIColor.blueColor().CGColor
)。
但是,我如何用从蓝色到红色的 45 度角的渐变填充它,就像 Image 2(即 UIColor.blueColor().CGColor
到 UIColor.redColor().CGColor
)?
代码:
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: 100))
path.addLineToPoint(CGPoint(x: 100, y: 100))
path.addLineToPoint(CGPoint(x: 100, y: 0))
path.closePath()
let shape = CAShapeLayer()
shape.path = path.CGPath
shape.fillColor = UIColor.blueColor().CGColor
为什么不使用具有 startPoint
和 endPoint
属性的 CAGradientLayer
。
你可以这样做:
import UIKit
import PlaygroundSupport
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let view = UIView(frame: frame)
PlaygroundPage.current.liveView = view
let path = UIBezierPath(ovalIn: frame)
let shape = CAShapeLayer()
shape.frame = frame
shape.path = path.cgPath
shape.fillColor = UIColor.blue.cgColor
let gradient = CAGradientLayer()
gradient.frame = frame
gradient.colors = [UIColor.blue.cgColor,
UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.mask = shape
view.layer.addSublayer(gradient)
注意:为圆形添加了贝塞尔曲线路径,因为它可以在没有正方形遮罩的情况下工作。
我认为是
shape.startPoint = CGPoint(x: 1.0, y: 0.0)
shape.endPoint = CGPoint(x: 0.0, y: 1.0)
,即右下角第一种颜色到左上角第二种颜色。如果你想要右上角的第一种颜色和左下角的第二种颜色,那么你应该
shape.startPoint = CGPoint(x: 1.0, y: 1.0)
shape.endPoint = CGPoint(x: 0.0, y: 0.0)
第一种颜色在左上角,第二种颜色在右下角
shape.startPoint = NSMakePoint(x: 0.0, y: 1.0)
shape.endPoint = NSMakePoint(x: 1.0, y: 0.0)
第一种颜色在左下角,第二种颜色在右上角
shape.startPoint = CGPoint(x: 0.0, y: 0.0)
shape.endPoint = CGPoint(x: 1.0, y: 1.0)
@Alistra 如果您的形状位于屏幕的左上角,则答案有效。如果您尝试移动形状的位置,您会注意到形状被切断(如果它甚至完全显示,具体取决于您的 x 和 y 值)
要解决此问题,请为渐变图层和形状图层使用两个不同的框架。将形状图层的 x,y
坐标设置为 0,0
。然后将渐变层的 x,y
坐标设置为您希望它在屏幕上的位置。
let gradientFrame = CGRect(x: 100,
y: 150,
width: 150,
height: 150)
let circleFrame = CGRect(x: 0,
y: 0,
width: 150,
height: 150)
let circle = CAShapeLayer()
circle.frame = circleFrame
circle.path = UIBezierPath(ovalIn: circleFrame).cgPath
let gradient = CAGradientLayer()
gradient.frame = gradientFrame
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.colors = [UIColor.blue.cgColor,
UIColor.red.cgColor]
gradient.mask = circle
view.layer.addSublayer(gradient)
轻松将渐变应用于 CALayer
Swift4.2,Xcode10.0
虽然上述解决方案仅适用于 45° 等微不足道的角度,但我的代码能够将渐变设置为任何给定角度。
public extension CALayer {
public func applyGradient(of colors: UIColor..., atAngle angle: CGFloat) -> CAGradientLayer {
let gradient = CAGradientLayer()
gradient.frame = frame
gradient.colors = colors
gradient.calculatePoints(for: angle)
gradient.mask = self
return gradient
}
}
public extension CAGradientLayer {
/// Sets the start and end points on a gradient layer for a given angle.
///
/// - Important:
/// *0°* is a horizontal gradient from left to right.
///
/// With a positive input, the rotational direction is clockwise.
///
/// * An input of *400°* will have the same output as an input of *40°*
///
/// With a negative input, the rotational direction is clockwise.
///
/// * An input of *-15°* will have the same output as *345°*
///
/// - Parameters:
/// - angle: The angle of the gradient.
///
public func calculatePoints(for angle: CGFloat) {
var ang = (-angle).truncatingRemainder(dividingBy: 360)
if ang < 0 { ang = 360 + ang }
let n: CGFloat = 0.5
let tanx: (CGFloat) -> CGFloat = { tan([=10=] * CGFloat.pi / 180) }
switch ang {
case 0...45, 315...360:
let a = CGPoint(x: 0, y: n * tanx(ang) + n)
let b = CGPoint(x: 1, y: n * tanx(-ang) + n)
startPoint = a
endPoint = b
case 45...135:
let a = CGPoint(x: n * tanx(ang - 90) + n, y: 1)
let b = CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
startPoint = a
endPoint = b
case 135...225:
let a = CGPoint(x: 1, y: n * tanx(-ang) + n)
let b = CGPoint(x: 0, y: n * tanx(ang) + n)
startPoint = a
endPoint = b
case 225...315:
let a = CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
let b = CGPoint(x: n * tanx(ang - 90) + n, y: 1)
startPoint = a
endPoint = b
default:
let a = CGPoint(x: 0, y: n)
let b = CGPoint(x: 1, y: n)
startPoint = a
endPoint = b
}
}
}
用法:
let layer = CAShapeLayer()
// Setup layer...
// Gradient Direction: →
let gradientLayer1 = layer.applyGradient(of: UIColor.yellow, UIColor.red, at: 0)
// Gradient Direction: ↗︎
let gradientLayer2 = layer.applyGradient(of: UIColor.purple, UIColor.yellow, UIColor.green, at: -45)
// Gradient Direction: ←
let gradientLayer3 = layer.applyGradient(of: UIColor.yellow, UIColor.blue, UIColor.green, at: 180)
// Gradient Direction: ↓
let gradientLayer4 = layer.applyGradient(of: UIColor.red, UIColor.blue, at: 450)
数学解释
所以我实际上最近花了很多时间试图自己回答这个问题。这里有一些示例角度,只是为了帮助理解和可视化顺时针旋转方向。
如果您对我的计算方法感兴趣,我制作了一个 table 来从本质上可视化我在 0° - 中所做的事情360°。
如果您不想使用 CAGradientLayer,根据 Noah Wilder 的回答,对于 Objective-C:
-(void)drawRect:(CGRect)rect {
//create theoretical circle
float w = self.frame.size.width;
float h = self.frame.size.height;
NSDictionary * points = [self pointsForAngle:angle width:w height:h];
CGPoint start = [points[@"start"] CGPointValue];
CGPoint end = [points[@"end"] CGPointValue];
//1. create vars
float increment = 1.0f / (colours.count-1);
CGFloat * locations = (CGFloat *)malloc((int)colours.count*sizeof(CGFloat));
CFMutableArrayRef mref = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
//2. go through the colours, creating cgColors and locations
for (int n = 0; n < colours.count; n++){
CFArrayAppendValue(mref, (id)[colours[n] CGColor]);
locations[n]=(n*increment);
}
//3. create gradient
CGContextRef ref = UIGraphicsGetCurrentContext();
CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradientRef = CGGradientCreateWithColors(spaceRef, mref, locations);
CGContextDrawLinearGradient(ref, gradientRef, start, end, kCGGradientDrawsAfterEndLocation);
free(locations);
CFRelease(mref);
CGColorSpaceRelease(spaceRef);
CGGradientRelease(gradientRef);
}
-(NSDictionary *)pointsForAngle:(float)angle width:(float)width height:(float)height{
float n = 0.5f;
CGPoint start = CGPointZero;
CGPoint end = CGPointZero;
if (angle >= 315 || angle < 45){
start = CGPointMake(n * [self tanThis:angle] + n, 0);
end = CGPointMake(n * [self tanThis:-angle] + n, 1);
} else if (angle >= 45 && angle < 135){
start = CGPointMake(0, n * [self tanThis:angle-90] + n);
end = CGPointMake(1, n * [self tanThis:-angle-90] + n);
} else if (angle >= 135 && angle < 225){
start = CGPointMake(n * [self tanThis:-angle] + n, 1);
end = CGPointMake(n * [self tanThis:angle] + n, 0);
} else if (angle >= 225 && angle < 315){
start = CGPointMake(1, n * [self tanThis:-angle-90] + n);
end = CGPointMake(0, n * [self tanThis:angle-90] + n);
}
start = CGPointMake(start.x * width, start.y * height);
end = CGPointMake(end.x * width, end.y * height);
return @{@"start":@(start), @"end":@(end)};
}
-(float)tanThis:(float)angle{
return tan(angle * M_PI / 180);
}
如何使用渐变和 45 度角填充 CAShapeLayer()
?
例如,在Image 1中,下面的代码绘制一个正方形并填充图层蓝色(UIColor.blueColor().CGColor
)。
但是,我如何用从蓝色到红色的 45 度角的渐变填充它,就像 Image 2(即 UIColor.blueColor().CGColor
到 UIColor.redColor().CGColor
)?
代码:
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: 100))
path.addLineToPoint(CGPoint(x: 100, y: 100))
path.addLineToPoint(CGPoint(x: 100, y: 0))
path.closePath()
let shape = CAShapeLayer()
shape.path = path.CGPath
shape.fillColor = UIColor.blueColor().CGColor
为什么不使用具有 startPoint
和 endPoint
属性的 CAGradientLayer
。
你可以这样做:
import UIKit
import PlaygroundSupport
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let view = UIView(frame: frame)
PlaygroundPage.current.liveView = view
let path = UIBezierPath(ovalIn: frame)
let shape = CAShapeLayer()
shape.frame = frame
shape.path = path.cgPath
shape.fillColor = UIColor.blue.cgColor
let gradient = CAGradientLayer()
gradient.frame = frame
gradient.colors = [UIColor.blue.cgColor,
UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.mask = shape
view.layer.addSublayer(gradient)
注意:为圆形添加了贝塞尔曲线路径,因为它可以在没有正方形遮罩的情况下工作。
我认为是
shape.startPoint = CGPoint(x: 1.0, y: 0.0)
shape.endPoint = CGPoint(x: 0.0, y: 1.0)
,即右下角第一种颜色到左上角第二种颜色。如果你想要右上角的第一种颜色和左下角的第二种颜色,那么你应该
shape.startPoint = CGPoint(x: 1.0, y: 1.0)
shape.endPoint = CGPoint(x: 0.0, y: 0.0)
第一种颜色在左上角,第二种颜色在右下角
shape.startPoint = NSMakePoint(x: 0.0, y: 1.0)
shape.endPoint = NSMakePoint(x: 1.0, y: 0.0)
第一种颜色在左下角,第二种颜色在右上角
shape.startPoint = CGPoint(x: 0.0, y: 0.0)
shape.endPoint = CGPoint(x: 1.0, y: 1.0)
@Alistra 如果您的形状位于屏幕的左上角,则答案有效。如果您尝试移动形状的位置,您会注意到形状被切断(如果它甚至完全显示,具体取决于您的 x 和 y 值)
要解决此问题,请为渐变图层和形状图层使用两个不同的框架。将形状图层的 x,y
坐标设置为 0,0
。然后将渐变层的 x,y
坐标设置为您希望它在屏幕上的位置。
let gradientFrame = CGRect(x: 100,
y: 150,
width: 150,
height: 150)
let circleFrame = CGRect(x: 0,
y: 0,
width: 150,
height: 150)
let circle = CAShapeLayer()
circle.frame = circleFrame
circle.path = UIBezierPath(ovalIn: circleFrame).cgPath
let gradient = CAGradientLayer()
gradient.frame = gradientFrame
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.colors = [UIColor.blue.cgColor,
UIColor.red.cgColor]
gradient.mask = circle
view.layer.addSublayer(gradient)
轻松将渐变应用于 CALayer
Swift4.2,Xcode10.0
虽然上述解决方案仅适用于 45° 等微不足道的角度,但我的代码能够将渐变设置为任何给定角度。
public extension CALayer {
public func applyGradient(of colors: UIColor..., atAngle angle: CGFloat) -> CAGradientLayer {
let gradient = CAGradientLayer()
gradient.frame = frame
gradient.colors = colors
gradient.calculatePoints(for: angle)
gradient.mask = self
return gradient
}
}
public extension CAGradientLayer {
/// Sets the start and end points on a gradient layer for a given angle.
///
/// - Important:
/// *0°* is a horizontal gradient from left to right.
///
/// With a positive input, the rotational direction is clockwise.
///
/// * An input of *400°* will have the same output as an input of *40°*
///
/// With a negative input, the rotational direction is clockwise.
///
/// * An input of *-15°* will have the same output as *345°*
///
/// - Parameters:
/// - angle: The angle of the gradient.
///
public func calculatePoints(for angle: CGFloat) {
var ang = (-angle).truncatingRemainder(dividingBy: 360)
if ang < 0 { ang = 360 + ang }
let n: CGFloat = 0.5
let tanx: (CGFloat) -> CGFloat = { tan([=10=] * CGFloat.pi / 180) }
switch ang {
case 0...45, 315...360:
let a = CGPoint(x: 0, y: n * tanx(ang) + n)
let b = CGPoint(x: 1, y: n * tanx(-ang) + n)
startPoint = a
endPoint = b
case 45...135:
let a = CGPoint(x: n * tanx(ang - 90) + n, y: 1)
let b = CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
startPoint = a
endPoint = b
case 135...225:
let a = CGPoint(x: 1, y: n * tanx(-ang) + n)
let b = CGPoint(x: 0, y: n * tanx(ang) + n)
startPoint = a
endPoint = b
case 225...315:
let a = CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
let b = CGPoint(x: n * tanx(ang - 90) + n, y: 1)
startPoint = a
endPoint = b
default:
let a = CGPoint(x: 0, y: n)
let b = CGPoint(x: 1, y: n)
startPoint = a
endPoint = b
}
}
}
用法:
let layer = CAShapeLayer()
// Setup layer...
// Gradient Direction: →
let gradientLayer1 = layer.applyGradient(of: UIColor.yellow, UIColor.red, at: 0)
// Gradient Direction: ↗︎
let gradientLayer2 = layer.applyGradient(of: UIColor.purple, UIColor.yellow, UIColor.green, at: -45)
// Gradient Direction: ←
let gradientLayer3 = layer.applyGradient(of: UIColor.yellow, UIColor.blue, UIColor.green, at: 180)
// Gradient Direction: ↓
let gradientLayer4 = layer.applyGradient(of: UIColor.red, UIColor.blue, at: 450)
数学解释
所以我实际上最近花了很多时间试图自己回答这个问题。这里有一些示例角度,只是为了帮助理解和可视化顺时针旋转方向。
如果您对我的计算方法感兴趣,我制作了一个 table 来从本质上可视化我在 0° - 中所做的事情360°。
如果您不想使用 CAGradientLayer,根据 Noah Wilder 的回答,对于 Objective-C:
-(void)drawRect:(CGRect)rect {
//create theoretical circle
float w = self.frame.size.width;
float h = self.frame.size.height;
NSDictionary * points = [self pointsForAngle:angle width:w height:h];
CGPoint start = [points[@"start"] CGPointValue];
CGPoint end = [points[@"end"] CGPointValue];
//1. create vars
float increment = 1.0f / (colours.count-1);
CGFloat * locations = (CGFloat *)malloc((int)colours.count*sizeof(CGFloat));
CFMutableArrayRef mref = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
//2. go through the colours, creating cgColors and locations
for (int n = 0; n < colours.count; n++){
CFArrayAppendValue(mref, (id)[colours[n] CGColor]);
locations[n]=(n*increment);
}
//3. create gradient
CGContextRef ref = UIGraphicsGetCurrentContext();
CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradientRef = CGGradientCreateWithColors(spaceRef, mref, locations);
CGContextDrawLinearGradient(ref, gradientRef, start, end, kCGGradientDrawsAfterEndLocation);
free(locations);
CFRelease(mref);
CGColorSpaceRelease(spaceRef);
CGGradientRelease(gradientRef);
}
-(NSDictionary *)pointsForAngle:(float)angle width:(float)width height:(float)height{
float n = 0.5f;
CGPoint start = CGPointZero;
CGPoint end = CGPointZero;
if (angle >= 315 || angle < 45){
start = CGPointMake(n * [self tanThis:angle] + n, 0);
end = CGPointMake(n * [self tanThis:-angle] + n, 1);
} else if (angle >= 45 && angle < 135){
start = CGPointMake(0, n * [self tanThis:angle-90] + n);
end = CGPointMake(1, n * [self tanThis:-angle-90] + n);
} else if (angle >= 135 && angle < 225){
start = CGPointMake(n * [self tanThis:-angle] + n, 1);
end = CGPointMake(n * [self tanThis:angle] + n, 0);
} else if (angle >= 225 && angle < 315){
start = CGPointMake(1, n * [self tanThis:-angle-90] + n);
end = CGPointMake(0, n * [self tanThis:angle-90] + n);
}
start = CGPointMake(start.x * width, start.y * height);
end = CGPointMake(end.x * width, end.y * height);
return @{@"start":@(start), @"end":@(end)};
}
-(float)tanThis:(float)angle{
return tan(angle * M_PI / 180);
}