有角度的渐变层

Posted

技术标签:

【中文标题】有角度的渐变层【英文标题】:Angled Gradient Layer 【发布时间】:2016-05-04 21:08:57 【问题描述】:

我有自定义 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() 确实在触发吗? 【参考方案1】:

您不想使用locations 来指定渐变的方向。而是使用startPointendPoint

locations 数组用于指定在startPointendPoint 之间应该发生渐变的位置。例如,如果您希望颜色仅出现在起点和终点的中间 10% 范围内,您可以使用:

locations = [0.45, 0.55]

locations 数组不指定方向。 startPointendPoint 可以。因此,对于从左上角到右下角的对角线渐变,您可以将startPointCGPoint(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]
    


例如

注意,与当前问题无关:

1234563 gradientLayer。但是,更好的是,覆盖视图的layerClass,它不仅会为您实例化CAGradientLayer,而且您还可以享受随着视图大小的变化而动态调整渐变,尤其是更优雅地处理动画变化。

同样,我设置了color1color2,这样它们就会触发渐变的更新,这样颜色的任何变化都会立即反映在视图中。

我做了这个@IBDesignable,所以如果我把它放到它自己的框架中,然后在IB中添加GradientView,我会看到在IB中呈现的效果。

李>

有关 Swift 2 的实现,请参阅 previous revision of this answer。

【讨论】:

我用你的代码解决了这个问题,它可以工作,但我收到奇怪的错误“忽略用户定义的颜色 1 的 keypath 运行时...” @KiraArik - 在 IB 中,选择渐变视图,然后查看右侧面板上的识别检查器(option-command-3),您将看到“用户定义的运行时属性”,你可能有一些不再是视图属性的东西。也许你重命名了一个属性? 您可以(有时应该)仍然使用CAGradientLayer().locations 以及startPointendPoint。使用locations 可以定义gradientLayer 中每种颜色的相对起始位置。 @ZGski - 同意。我试图澄清上面的答案。【参考方案2】:

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

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-Wilder,关于你的方程式。 n 代表什么,为什么它的值是 0.5 而不是别的? @hamada147,我只是使用 n 作为值 0.5 的占位符。我提取 0.5 作为变量 n 的事实只是一种有助于使方程更简洁的风格选择。【参考方案3】:

您似乎忘记在您的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)
    

【讨论】:

【参考方案4】:

我不确定是什么导致你的不工作,但我确实有一个 GradientView,我使用它可以是水平的或垂直的,并且可以与 ui builder 的东西一起使用。随意运行它并根据您的需要进行改进:

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()
    

【讨论】:

【参考方案5】:

可以使用一些基本的三角函数来实现有角度的渐变。正如我在blog post on the subject 中描述的那样,您可以通过子类化 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 度开始的旋转,按照惯例在 Core Animation 中指向东方,并逆时针(逆时针)增加。

其余相关代码如下,关注结果点在单位圆上的事实。然而,我们需要的点在单位平方中:向量被外推到单位平方。

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
    

完整的实现请看我的blog post。

【讨论】:

我用angel = 0测试,方向颜色是从左->右,错了,一定是上->BOttom

以上是关于有角度的渐变层的主要内容,如果未能解决你的问题,请参考以下文章

将渐变层添加到表格视图

android渐变中的角度属性

ImageView 上的渐变层重用

如何在 SwiftUI 中定义结构属性以接受线性或角度渐变?

为啥我的 UITableView 单元格的渐变子层没有正确显示?

为啥我的渐变层覆盖了我的 collectionView 单元格?