倾斜的渐变图层

12

我有一个自定义的UIView类,在Swift 2中渲染一个渐变。我正在努力制作一个角度渐变,以使其从左上角向右下角绘制。有人可以帮我一下吗?

import UIKit

class GradientView: UIView {

    let gradientLayer = CAGradientLayer()

    override func awakeFromNib() {
        // 1
        self.backgroundColor = ColorPalette.White

        // 2
        gradientLayer.frame = self.bounds

        // 3
        let color1 = ColorPalette.GrdTop.CGColor as CGColorRef
        let color2 = ColorPalette.GrdBottom.CGColor as CGColorRef
        gradientLayer.colors = [color1, color2]

        // 4
        gradientLayer.locations = [0.0, 1.0]

        // 5
        self.layer.addSublayer(gradientLayer)
    }

}

我怀疑这应该是其他的东西,但无论我输入什么都没有改变。

gradientLayer.locations = [0.0, 1.0]

你确定你的 awakeFromNib() 方法确实被触发了吗? - Travis Griggs
5个回答

30

不要使用locations来指定渐变的方向,而是使用startPointendPoint

locations数组用于在startPointendPoint之间指定渐变发生的位置。例如,如果您想要颜色仅在起始点和结束点范围的中间10%位置发生,请使用:

locations = [0.45, 0.55]

locations 数组不决定方向,startPointendPoint 决定。所以,对于从左上到右下的对角线渐变,你应该将 startPoint 设置为 CGPoint(x: 0, y: 0),将 endPoint 设置为 CGPoint(x: 1, y: 1)

例如:

@IBDesignable
class GradientView: UIView {

    override class var layerClass: AnyClass { return CAGradientLayer.self }

    private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }

    @IBInspectable var color1: UIColor = .white { didSet { updateColors() } }
    @IBInspectable var color2: UIColor = .blue  { didSet { updateColors() } }

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configureGradient()
    }

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

    private func configureGradient() {
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        updateColors()
    }

    private func updateColors() {
        gradientLayer.colors = [color1.cgColor, color2.cgColor]
    }

}

例如:

enter image description here

注意,以下内容与当前问题无关:

  • 如果你要将渐变作为子层添加,则需要在layoutSubviews中更新此子层的frame,以便随着视图的bounds更改,gradientLayerframe也会更改。但是,比这更好的方法是重写视图的layerClass,它不仅会为你实例化CAGradientLayer,而且您还可以享受当视图大小更改时,特别是更优雅地处理动画更改。

  • 同样,我设置了color1color2,以触发渐变的更新,因此任何颜色的更改都会立即反映在视图中。

  • 我将其标记为@IBDesignable,这样,如果我将其放入自己的框架中,然后在IB中添加GradientView,我将看到渲染效果。

有关Swift 2实现,请参见此答案的以前版本


我使用了你的代码来解决这个问题,它可以工作,但是我遇到了奇怪的错误:“忽略用户定义的运行时关键路径颜色1...” - Kira
1
@KiraArik - 在IB中,选择渐变视图,然后查看右侧面板上的识别检查器(选项-命令-3),您将看到“用户定义的运行时属性”,您可能会发现其中有一些不再是视图属性。也许您重命名了某个属性? - Rob
你可以(有时应该)仍然使用 CAGradientLayer().locations 以及 startPointendPoint。使用 locations 可以定义 gradientLayer 中每种颜色的相对起始位置。 - ZGski
@ZGski - 同意。我已经尝试在上面澄清了答案。 - Rob

27

任意角度的渐变起点和终点

Swift 4.2, Xcode 10.0

给定任意角度,我的代码将设置渐变图层的相应起点和终点。

如果输入的角度大于360°,它将使用除以360的余数。

  • 415°的输入将产生与55°的输入相同的结果

如果输入的角度小于,它将反转顺时针旋转方向。

  • -15°的输入将产生与345°的输入相同的结果

代码:

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

        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

        }
    }

    /// Private function to aid with the math when calculating the gradient angle
    private func tanx(_ : CGFloat) -> CGFloat {
        return tan( * CGFloat.pi / 180)
    }


    // Overloads

    /// Sets the start and end points on a gradient layer for a given angle.
    public func calculatePoints(for angle: Int) {
        calculatePoints(for: CGFloat(angle))
    }

    /// Sets the start and end points on a gradient layer for a given angle.
    public func calculatePoints(for angle: Float) {
        calculatePoints(for: CGFloat(angle))
    }

    /// Sets the start and end points on a gradient layer for a given angle.
    public func calculatePoints(for angle: Double) {
        calculatePoints(for: CGFloat(angle))
    }

}

使用方法:

let gradientLayer = CAGradientLayer()

// Setup gradient layer...

// Gradient Direction: →
gradient.calculatePoints(for: 0)

// Gradient Direction: ↗︎
gradient.calculatePoints(for: -45)

// Gradient Direction: ←
gradient.calculatePoints(for: 180)

// Gradient Direction: ↓
gradient.calculatePoints(for: 450)

数学解释

我最近花了很多时间来尝试回答这个问题。以下是一些示例角度,以帮助理解和可视化顺时针旋转方向。

示例角度

如果您对我是如何解决它感兴趣,我制作了一张表格来可视化我从360°所做的事情。

表格


太棒了。谢谢Noah! - Zack Shapiro
你好 @Noah-Wilder,关于你的方程。n代表什么?为什么它的值是0.5而不是其他的值? - hamada147
1
@hamada147,我只是将n用作值0.5的占位符。将0.5提取为变量n只是一种风格选择,以帮助使方程更简洁。 - Noah Wilder

4

看起来您忘记在 CAGradientLayer() 上设置 startPoint。以下是您提供的代码以及我的添加。

import UIKit

class GradientView: UIView {

    let gradientLayer = CAGradientLayer()

    override func awakeFromNib() {
        // 1
        self.backgroundColor = ColorPalette.White

        // 2
        gradientLayer.frame = self.bounds

        // 3
        let color1 = ColorPalette.GrdTop.CGColor as CGColorRef
        let color2 = ColorPalette.GrdBottom.CGColor as CGColorRef
        gradientLayer.colors = [color1, color2]

        //** This code should do the trick... **//
        gradientLayer.startPoint = CGPointMake(0.0, 0.5)

        // 4
        gradientLayer.locations = [0.0, 1.0]

        // 5
        self.layer.addSublayer(gradientLayer)
    }
}

3

我不确定是什么原因导致你的代码无法工作,但是我有一个GradientView控件,可以水平或垂直,而且可以与UI构建器配合使用。您可以随意使用并根据需要进行调整:

import UIKit

@IBDesignable  class  GradientView: UIView {
    var gradient:CAGradientLayer
    @IBInspectable var startColor:UIColor = UIColor.whiteColor() {
        didSet {
            self.updateGradient()
        }
    }

    @IBInspectable var color1:UIColor? = nil {
        didSet {
            self.updateGradient()
        }
    }

    @IBInspectable var stop1:Double = (1.0 / 3.0) {
        didSet {
            self.updateGradient()
        }
    }

    @IBInspectable var color2:UIColor? = nil {
        didSet {
            self.updateGradient()
        }
    }

    @IBInspectable var stop2:Double = (2.0 / 3.0) {
        didSet {
            self.updateGradient()
        }
    }

    @IBInspectable var endColor:UIColor = UIColor.blackColor() {
        didSet {
            self.updateGradient()
        }
    }

    @IBInspectable var isHorizontal:Bool {
        get {
            return self.gradient.endPoint.y == self.gradient.startPoint.y
        }
        set {
            self.gradient.endPoint = newValue ? CGPoint(x: 1, y: 0) : CGPoint(x: 0, y: 1)
        }
    }

    override init(frame: CGRect) {
        gradient = CAGradientLayer()
        super.init(frame: frame)
        self.configGradient()
    }

    required init?(coder aDecoder: NSCoder) {
        gradient = CAGradientLayer()
        super.init(coder: aDecoder)
        self.configGradient()
    }

    func configGradient() {
        self.backgroundColor = UIColor.clearColor()
        self.layer.insertSublayer(self.gradient, atIndex: 0)
        self.gradient.masksToBounds = true
        self.gradient.frame = self.bounds
        self.gradient.startPoint = CGPoint(x: 0, y: 0)
        self.gradient.endPoint = CGPoint(x: 1, y: 0)
        self.updateGradient()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.gradient.frame = self.bounds
    }

    func updateGradient() {
        var colors:[CGColorRef] = []
        var locations:[NSNumber] = []
        colors.append(self.startColor.CGColor)
        locations.append(0.0.nsNumber)

        if let color = self.color1 {
            colors.append(color.CGColor)
            locations.append(self.stop1)}

        if let color = self.color2 {
            colors.append(color.CGColor)
            locations.append(self.stop2)
        }

        colors.append(self.endColor.CGColor)
        locations.append(1.0.nsNumber)

        self.gradient.colors = colors
        self.gradient.locations = locations

        self.layer.setNeedsDisplay()
    }
}

0

可以使用一些基本的三角函数来实现一个倾斜的渐变。您可以通过子类化UIView来实现它,就像我在主题博客文章中所描述的那样。

首先定义一些变量:

// The end point of the gradient when drawn in the layer’s coordinate space. Animatable.
var endPoint: CGPoint

// The start point of the gradient when drawn in the layer’s coordinate space. Animatable.
var startPoint: CGPoint

// the gradient angle, in degrees anticlockwise from 0 (east/right)
@IBInspectable var angle: CGFloat = 270

核心函数如下,获取单位空间中的起始点和结束点。
// create vector pointing in direction of angle
private func gradientPointsForAngle(_ angle: CGFloat) -> (CGPoint, CGPoint) {
    // get vector start and end points
    let end = pointForAngle(angle)
    let start = oppositePoint(end)
    // convert to gradient space
    let p0 = transformToGradientSpace(start)
    let p1 = transformToGradientSpace(end)
    return (p0, p1)
}

这个简单的程序会根据用户指定的角度创建一个指向该方向的向量。该角度指定了向量相对于0度的旋转角度,按照惯例,在核心动画中,0度指向东方,并逆时针(顺时针)增加。

其余相关代码如下,与事实有关的是结果点在单位圆上。然而,我们需要的点在一个单位正方形内:向量被外推到单位正方形内。

private func pointForAngle(_ angle: CGFloat) -> CGPoint {
    // convert degrees to radians
    let radians = angle * .pi / 180.0
    var x = cos(radians)
    var y = sin(radians)
    // (x,y) is in terms unit circle. Extrapolate to unit square to get full vector length
    if (fabs(x) > fabs(y)) {
        // extrapolate x to unit length
        x = x > 0 ? 1 : -1
        y = x * tan(radians)
    } else {
        // extrapolate y to unit length
        y = y > 0 ? 1 : -1
        x = y / tan(radians)
    }
    return CGPoint(x: x, y: y)
}

private func oppositePoint(_ point: CGPoint) -> CGPoint {
    return CGPoint(x: -point.x, y: -point.y)
}

private func transformToGradientSpace(_ point: CGPoint) -> CGPoint {
    // input point is in signed unit space: (-1,-1) to (1,1)
    // convert to gradient space: (0,0) to (1,1), with flipped Y axis
    return CGPoint(x: (point.x + 1) * 0.5, y: 1.0 - (point.y + 1) * 0.5)
}

最终,所有的东西都必须从一个更新函数中调用:

private func updateGradient() {
    if let gradient = self.gradient {
        let (start, end) = gradientPointsForAngle(self.angle)
        gradient.startPoint = start
        gradient.endPoint = end
        gradient.frame = self.bounds
    }
}

请查看我的博客文章以获取完整实现。

我测试了 angel = 0,方向颜色是从左到右,这是错误的,必须是从上到下。 - famfamfam

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