更新:使用context.drawLinearGradient()代替CAGradientLayer,方式与以下方式类似。这将绘制与Sketch/Photoshop一致的渐变。
如果您非常需要使用CAGradientLayer,则需要使用以下数学公式...
经过仔细观察,我花了一些时间才发现苹果在CAGradientLayer中实现渐变的方式相当奇怪:
- 首先它将视图转换为正方形。
- 然后使用起始/结束点应用渐变。
- 在这种分辨率下,中间渐变确实会形成90度角。
- 最后,它将视图缩小到原始大小。
这意味着中间渐变在新尺寸中将不再形成90度角。这与几乎所有其他绘画应用程序的行为相矛盾:如Sketch、Photoshop等。
如果您想要像Sketch那样实现起始/结束点,则需要将其转换为考虑到苹果将要压缩视图的起始/结束点。
执行步骤(图示)
![enter image description here](https://istack.dev59.com/HtJcc.webp)
代码
import UIKit
public enum LinearGradientFixer {
public static func fixPoints(start: CGPoint, end: CGPoint, bounds: CGSize) -> (CGPoint, CGPoint) {
if start.x == end.x || start.y == end.y {
return (start, end)
}
let startEnd = LineSegment(start, end)
let ab = startEnd.multiplied(multipliers: (x: bounds.width, y: bounds.height))
let a = ab.p1
let b = ab.p2
let cd = ab.perpendicularBisector
let multipliers = calculateMultipliers(bounds: bounds)
let lineSegmentCD = cd.multiplied(multipliers: multipliers)
let lineSegmentEF = lineSegmentCD.perpendicularBisector
let ef = lineSegmentEF.divided(divisors: multipliers)
let efLine = ef.line
let aParallelLine = Line(m: cd.slope, p: a)
let bParallelLine = Line(m: cd.slope, p: b)
let g = efLine.intersection(with: aParallelLine)
let h = efLine.intersection(with: bParallelLine)
if let g = g, let h = h {
let gh = LineSegment(g, h)
let result = gh.divided(divisors: (x: bounds.width, y: bounds.height))
return (result.p1, result.p2)
}
return (start, end)
}
private static func unitTest() {
let w = 320.0
let h = 60.0
let bounds = CGSize(width: w, height: h)
let a = CGPoint(x: 138.5, y: 11.5)
let b = CGPoint(x: 151.5, y: 53.5)
let ab = LineSegment(a, b)
let startEnd = ab.divided(divisors: (x: bounds.width, y: bounds.height))
let start = startEnd.p1
let end = startEnd.p2
let points = fixPoints(start: start, end: end, bounds: bounds)
let pointsSegment = LineSegment(points.0, points.1)
let result = pointsSegment.multiplied(multipliers: (x: bounds.width, y: bounds.height))
print(result.p1)
print(result.p2)
}
}
private func calculateMultipliers(bounds: CGSize) -> (x: CGFloat, y: CGFloat) {
if bounds.height <= bounds.width {
return (x: 1, y: bounds.width/bounds.height)
} else {
return (x: bounds.height/bounds.width, y: 1)
}
}
private struct LineSegment {
let p1: CGPoint
let p2: CGPoint
init(_ p1: CGPoint, _ p2: CGPoint) {
self.p1 = p1
self.p2 = p2
}
init(p1: CGPoint, m: CGFloat, distance: CGFloat) {
self.p1 = p1
let line = Line(m: m, p: p1)
let measuringPoint = line.point(x: p1.x + 1)
let measuringDeltaH = LineSegment(p1, measuringPoint).distance
let deltaX = distance/measuringDeltaH
self.p2 = line.point(x: p1.x + deltaX)
}
var length: CGFloat {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
return sqrt(dx * dx + dy * dy)
}
var distance: CGFloat {
return p1.x <= p2.x ? length : -length
}
var midpoint: CGPoint {
return CGPoint(x: (p1.x + p2.x)/2, y: (p1.y + p2.y)/2)
}
var slope: CGFloat {
return (p2.y-p1.y)/(p2.x-p1.x)
}
var perpendicularSlope: CGFloat {
return -1/slope
}
var line: Line {
return Line(p1, p2)
}
var perpendicularBisector: LineSegment {
let p1 = LineSegment(p1: midpoint, m: perpendicularSlope, distance: -distance/2).p2
let p2 = LineSegment(p1: midpoint, m: perpendicularSlope, distance: distance/2).p2
return LineSegment(p1, p2)
}
func multiplied(multipliers: (x: CGFloat, y: CGFloat)) -> LineSegment {
return LineSegment(
CGPoint(x: p1.x * multipliers.x, y: p1.y * multipliers.y),
CGPoint(x: p2.x * multipliers.x, y: p2.y * multipliers.y))
}
func divided(divisors: (x: CGFloat, y: CGFloat)) -> LineSegment {
return multiplied(multipliers: (x: 1/divisors.x, y: 1/divisors.y))
}
}
private struct Line {
let m: CGFloat
let b: CGFloat
init(m: CGFloat, b: CGFloat) {
self.m = m
self.b = b
}
init(m: CGFloat, p: CGPoint) {
self.m = m
self.b = p.y - m*p.x
}
init(_ p1: CGPoint, _ p2: CGPoint) {
self.init(m: LineSegment(p1, p2).slope, p: p1)
}
func y(x: CGFloat) -> CGFloat {
return m*x + b
}
func point(x: CGFloat) -> CGPoint {
return CGPoint(x: x, y: y(x: x))
}
func intersection(with line: Line) -> CGPoint? {
let n = line.m
let c = line.b
if m-n == 0 {
return nil
}
let x = (c-b)/(m-n)
return point(x: x)
}
}
证明它可以适用于任何矩形大小
我尝试了一个视图,大小为320x60
,渐变=[红@0,绿@0.5,蓝@1]
,起始点 = (0,1)
,结束点 = (1,0)
。
草图3:
![enter image description here](https://istack.dev59.com/5xP2D.webp)
使用上述代码生成的实际iOS截图:
![enter image description here](https://istack.dev59.com/KwuXZ.webp)
请注意,绿线的角度看起来100%准确。差异在于红色和蓝色的混合方式。我无法确定这是因为我计算起始/结束点不正确,还是仅仅是苹果和Sketch混合渐变的方式不同。