Угловой градиентный слой

У меня есть пользовательский класс 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]

person Kira    schedule 04.05.2016    source источник
comment
Убедились ли вы, что ваш awakeFromNib() действительно срабатывает?   -  person Travis Griggs    schedule 05.05.2016


Ответы (5)


Вы не хотите использовать locations для указания направления градиента. Вместо этого используйте для этого startPoint и endPoint.

Массив locations используется, когда нужно указать, где между startPoint и endPoint должен происходить градиент. Например, если вы хотите, чтобы цвета располагались только в середине 10% диапазона от начальной и конечной точек, вы должны использовать:

locations = [0.45, 0.55]

Массив locations не определяет направление. startPoint и endPoint делают. Таким образом, для диагонального градиента от верхнего левого угла к нижнему правому вы должны установить 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]
    }

}

E.g.

введите описание изображения здесь

Примечание, не относящееся к непосредственной проблеме:

  • Если вы собираетесь добавить градиент в качестве подслоя, вам нужно обновить frame этого подслоя в layoutSubviews, чтобы по мере изменения bounds представления менялось и frame gradientLayer. Но, что еще лучше, переопределите layerClass представления, и оно не только создаст экземпляр CAGradientLayer для вас, но вы также получите удовольствие от динамической настройки градиента при изменении размера представления, в частности более изящной обработки анимированных изменений.

  • Точно так же я установил color1 и color2 так, чтобы они вызывали обновление градиента, чтобы любые изменения в цветах немедленно отражались в представлении.

  • Я сделал это @IBDesignable, так что если я перенесу это в его собственный фреймворк, а затем добавлю GradientView в IB, я увижу эффект, отрендеренный в IB.

Для реализации Swift 2 см. предыдущую версию этого ответа.

person Rob    schedule 04.05.2016
comment
Я использовал ваш код для решения этой проблемы, и он работает, но я получаю странную ошибку Игнорирование определяемой пользователем среды выполнения для пути к ключу для цвета 1... - person Kira; 05.05.2016
comment
@KiraArik - в IB выберите представление градиента, а затем посмотрите на инспектор идентификации на правой панели (option-command-3), вы увидите определенные пользователем атрибуты времени выполнения, и у вас может быть что-то там, чего больше нет свойство представления. Возможно, вы переименовали свойство? - person Rob; 05.05.2016
comment
Вы можете (а иногда и должны) по-прежнему использовать CAGradientLayer().locations вместе с startPoint и endPoint. Использование locations позволяет вам определить относительные начальные местоположения каждого цвета в вашем gradientLayer. - person ZGski; 05.05.2016
comment
@ZGski - Согласен. Я попытался уточнить ответ выше. - person Rob; 05.05.2016

Начальная и конечная точки градиента для любого угла

Свифт 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°.

Таблица

person Noah Wilder    schedule 30.10.2018
comment
Это круто. Спасибо Ной! - person Zack Shapiro; 26.07.2019
comment
Здравствуйте, @Noah-Wilder. Что касается вашего уравнения. Что означает n и почему оно имеет значение 0,5, а не что-то другое? - person hamada147; 03.12.2019
comment
@ hamada147, я просто использовал n в качестве заполнителя для значения 0,5. Тот факт, что я извлек 0,5 в качестве переменной n, был просто стилистическим выбором, чтобы сделать уравнения более краткими. - person Noah Wilder; 04.12.2019

Похоже, вы забыли установить startPoint на CAGradientLayer(). Код ниже — это код, который вы предоставили, плюс мое дополнение.

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)
    }
}
person ZGski    schedule 04.05.2016

Я не уверен, что заставляет вас не работать, но у меня есть 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()
    }
}
person Travis Griggs    schedule 04.05.2016

Угловой градиент можно получить, используя базовую тригонометрию. Вы можете добиться этого, создав подкласс 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
    }
}

Полную реализацию см. в моем сообщении в блоге.

person Echelon    schedule 25.09.2018