角度渐变层
Angled Gradient Layer
我有自定义 UIView
class 在 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]
我不确定是什么让你的不起作用,但我确实有一个我使用的 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()
}
}
您不想使用locations
来指定渐变的方向。而是使用 startPoint
和 endPoint
。
locations
数组用于指定在 startPoint
和 endPoint
之间应该发生渐变的位置。例如,如果您希望颜色仅出现在起点和终点范围中间 10% 的范围内,您可以使用:
locations = [0.45, 0.55]
locations
数组不指定方向。 startPoint
和 endPoint
可以。因此,对于从左上角到右下角的对角渐变,您可以将 CGPoint(x: 0, y: 0)
的 startPoint
和 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]
}
}
例如
注意,与当前问题无关:
如果您要将渐变添加为子层,您需要在 layoutSubviews
中更新此子层的 frame
以便作为视图的 bounds
变化,gradientLayer
的 frame
也会变化。但是,比这更好的是,覆盖视图的 layerClass
,它不仅会为您实例化 CAGradientLayer
,而且您还可以享受随着视图大小变化而动态调整渐变,特别是处理动画变化更优雅。
同样,我设置了 color1
和 color2
,它们会触发渐变的更新,这样颜色的任何变化都会立即反映在视图中.
我做了这个@IBDesignable
,所以如果我把它放在它自己的框架中,然后在 IB 中添加 GradientView
,我会看到渲染的效果IB.
对于 Swift 2 的实现,参见 previous revision of this answer。
您似乎忘记在 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)
}
}
使用一些基本的三角函数可以实现成角度的渐变。您可以通过子类化 UIView 来实现它,正如我在 blog post on the subject.
中描述的那样
首先定义一些变量:-
// 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
下面的核心函数获取单元space中的起点和终点。
// 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。
任意角度的渐变起点和终点
Swift4.2,Xcode10.0
给定任何角度,我的代码将设置渐变层的相应起点和终点。
如果输入的角度大于360°,则取360的余数。
- 输入 415° 会产生与输入 55°
相同的结果
如果输入小于0°的角度,则顺时针方向反转
- 输入 -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)
数学解释
所以我实际上最近花了很多时间试图自己回答这个问题。这里有一些示例角度,只是为了帮助理解和可视化顺时针旋转方向。
如果您对我的计算方法感兴趣,我制作了一个 table 来从本质上可视化我在 0° - 中所做的事情360°。
我有自定义 UIView
class 在 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]
我不确定是什么让你的不起作用,但我确实有一个我使用的 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()
}
}
您不想使用locations
来指定渐变的方向。而是使用 startPoint
和 endPoint
。
locations
数组用于指定在 startPoint
和 endPoint
之间应该发生渐变的位置。例如,如果您希望颜色仅出现在起点和终点范围中间 10% 的范围内,您可以使用:
locations = [0.45, 0.55]
locations
数组不指定方向。 startPoint
和 endPoint
可以。因此,对于从左上角到右下角的对角渐变,您可以将 CGPoint(x: 0, y: 0)
的 startPoint
和 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]
}
}
例如
注意,与当前问题无关:
如果您要将渐变添加为子层,您需要在
layoutSubviews
中更新此子层的frame
以便作为视图的bounds
变化,gradientLayer
的frame
也会变化。但是,比这更好的是,覆盖视图的layerClass
,它不仅会为您实例化CAGradientLayer
,而且您还可以享受随着视图大小变化而动态调整渐变,特别是处理动画变化更优雅。同样,我设置了
color1
和color2
,它们会触发渐变的更新,这样颜色的任何变化都会立即反映在视图中.我做了这个
@IBDesignable
,所以如果我把它放在它自己的框架中,然后在 IB 中添加GradientView
,我会看到渲染的效果IB.
对于 Swift 2 的实现,参见 previous revision of this answer。
您似乎忘记在 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)
}
}
使用一些基本的三角函数可以实现成角度的渐变。您可以通过子类化 UIView 来实现它,正如我在 blog post on the subject.
中描述的那样首先定义一些变量:-
// 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
下面的核心函数获取单元space中的起点和终点。
// 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。
任意角度的渐变起点和终点
Swift4.2,Xcode10.0
给定任何角度,我的代码将设置渐变层的相应起点和终点。
如果输入的角度大于360°,则取360的余数。
- 输入 415° 会产生与输入 55° 相同的结果
如果输入小于0°的角度,则顺时针方向反转
- 输入 -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)
数学解释
所以我实际上最近花了很多时间试图自己回答这个问题。这里有一些示例角度,只是为了帮助理解和可视化顺时针旋转方向。
如果您对我的计算方法感兴趣,我制作了一个 table 来从本质上可视化我在 0° - 中所做的事情360°。