如何在CALayer中绘制径向渐变?

28

我知道 CAGradientLayer 目前不支持径向渐变,只有 kCAGradientLayerAxial 选项。

我想要像下面这样的效果:

图片描述

我查找了这个问题并发现有一种方法解决。但是对我来说解释不清楚。因此,我想知道是否可以使用 CAGradientLayer 绘制径向渐变,如果可以,那么如何做呢?


为什么不能使用CGContextDrawRadialGradient?您可以始终渲染到图像,然后将其分配给CALayer的内容。使用CALayerrenderInContext:也是一种选择,但最终都会导致CGContextDrawRadialGradient - Mazyod
@Mazyod 我在我的UIView中添加了多个图层作为子图层,并在这些图层上绘制bezierPath。因此,所有属性,如实心填充、描边宽度等都与图层相关联。所以我只想在代码中保持一致性。 - blancos
@Mazyod,我还没有理解你渲染图像并将其分配给CALayer内容的概念。您能否再详细说明一下? - blancos
4个回答

49

据我理解,您只需要一个绘制渐变的图层,而CGContextDrawRadialGradient正好满足这个需求。并且为了重申您所说的,CAGradientLayer不支持径向渐变,除了可以通过CALayer子类进行干净地无用替换外我们无法做任何事情。

(注意:渐变绘制代码来源于此处,这不是本答案的主题。)


viewDidLoad

GradientLayer *gradientLayer = [[GradientLayer alloc] init];
gradientLayer.frame = self.view.bounds;

[self.view.layer addSublayer:gradientLayer];

CALayer 的子类:

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self setNeedsDisplay];
    }
    return self;
}

- (void)drawInContext:(CGContextRef)ctx
{

    size_t gradLocationsNum = 2;
    CGFloat gradLocations[2] = {0.0f, 1.0f};
    CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.5f};
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum);
    CGColorSpaceRelease(colorSpace);

    CGPoint gradCenter= CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
    CGFloat gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ;

    CGContextDrawRadialGradient (ctx, gradient, gradCenter, 0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation);


    CGGradientRelease(gradient);
}

这里输入图片描述


这在 SWIFT 中如何实现? - DrPatience
1
@DrPatience 上述代码直接适用于Swift。只需像这样:override func drawInContext(ctx: CGContext!) { … - Andy Poes
1
@DrPatience 不确定您遇到的具体问题是什么,但也许这可以帮助您?https://gist.github.com/MrBendel/f10d46a3160ffdf28989 - Andy Poes
我尝试过这个,但是渐变显示在所有子视图(UIButton、UILabel等)的上方,因此它们的颜色被渐变“污染”了。有什么线索可以解决这个问题吗? - WedgeSparda
@WedgeSparda,除非这些控件是非不透明的,否则这是不可能的。只需确保CALayer位于subLayers的底部即可。 - Mazyod
显示剩余5条评论

21

这是我使用的Swift3代码:

import UIKit

class RadialGradientLayer: CALayer {

    required override init() {
        super.init()
        needsDisplayOnBoundsChange = true
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    required override init(layer: Any) {
        super.init(layer: layer)
    }

    public var colors = [UIColor.red.cgColor, UIColor.blue.cgColor]

    override func draw(in ctx: CGContext) {
        ctx.saveGState()

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        var locations = [CGFloat]()
        for i in 0...colors.count-1 {
            locations.append(CGFloat(i) / CGFloat(colors.count))
        }
        let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations)
        let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
        let radius = min(bounds.width / 2.0, bounds.height / 2.0)
        ctx.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: radius, options: CGGradientDrawingOptions(rawValue: 0))
    }
}

7
没错,这对我起作用了。几个注意点:
  • 如果你在混编项目中(Obj-C + Swift)编码,定义一个CGColors数组会导致错误,因为Objective-C不允许创建原始数据类型的数组。为了克服这个问题,您可以创建一个UIColors数组,然后在“draw”函数中将其转换为它们的cgColor值。
  • 当添加到较小的子视图时,此圆形边缘会出现锯齿状。我通过设置图层的'masksToBounds = true'来解决这个问题,然后添加了一个圆角使包含图层的视图成为一个圆形,使边缘平滑。
- Iron John Bonney
1
这对我也非常有效 - 我改变的唯一事情是使用 max 而不是 min 作为半径,并使用 .drawsAfterEndLocation 而不是 CGGradientDrawingOptions(rawValue: 0),因为这样会得到更令人愉悦的结果(在我看来)。 - atomoil
您可以像这样添加您创建的径向CALayerview.layer.insertSublayer(gradientLayer, below: view.layer) - Chintan Shah

20

我不知道什么时候开始,但现在你可以将 CAGradientLayertype 改为 kCAGradientLayerRadial,它可以正常工作。

从理论上讲,核心动画的性能比核心图形更好。

一些示例代码:

class View : UIView {

    let gradientLayer = CAGradientLayer()
    override init(frame: CGRect) {
        super.init(frame: frame)

        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        gradientLayer.locations = [
            NSNumber(value: 0.6),
            NSNumber(value: 0.8)
        ]
        gradientLayer.type = .radial
        gradientLayer.colors = [
            UIColor.green.cgColor,
            UIColor.purple.cgColor,
            UIColor.orange.cgColor,
            UIColor.red.cgColor
        ]
        self.layer.addSublayer(gradientLayer)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func layoutSublayers(of layer: CALayer) {
        assert(layer === self.layer)
        gradientLayer.frame = layer.bounds
    }
}

运行完美。但是位置(0.6和0.8)以及起始/结束点的含义是什么? - Roman
好多了。感谢您添加这个功能。我甚至都没有去检查它。 - Nathan Hosselton

4

Swift 5

import UIKit

final class RadialGradientLayer: CALayer {

    let centerColor: CGColor
    let edgeColor: CGColor

    init(centerColor: UIColor, edgeColor: UIColor) {
        self.centerColor = centerColor.cgColor
        self.edgeColor = edgeColor.cgColor
        super.init()
        needsDisplayOnBoundsChange = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(in ctx: CGContext) {
        ctx.saveGState()
        let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                  colors: [centerColor, edgeColor] as CFArray,
                                  locations: [0.0, 1.0])

        let gradCenter = CGPoint(x: bounds.midX, y: bounds.midY)
        let gradRadius = min(bounds.width, bounds.height)

        ctx.drawRadialGradient(gradient!,
                               startCenter: gradCenter,
                               startRadius: 0.0,
                               endCenter: gradCenter,
                               endRadius: gradRadius,
                               options: .drawsAfterEndLocation)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接