CAGradientLayer对angular线渐变

在这里输入图像说明

我使用以下CAGradientLayer:

let layer = CAGradientLayer() layer.colors = [ UIColor.redColor().CGColor, UIColor.greenColor().CGColor, UIColor.blueColor().CGColor ] layer.startPoint = CGPointMake(0, 1) layer.endPoint = CGPointMake(1, 0) layer.locations = [0.0, 0.6, 1.0] 

但是当我为图层设置边界属性时,它只是拉伸一个方形渐变。 我需要一个类似于Sketch 3应用程序图像的结果(请参阅上文)。

我怎样才能做到这一点?

更新: 以类似于以下的方式使用context.drawLinearGradient()而不是CAGradientLayer。 它将绘制与Sketch / Photoshop一致的渐变。

如果你绝对必须使用CAGradientLayer,那么这里是你需要使用的math…


花了一些时间才弄清楚,但是仔细观察,我发现苹果在CAGradientLayer中实现渐变是很奇怪的:

  1. 首先它将视图转换为方形。
  2. 然后它使用开始/结束点来应用渐变。
  3. 在这个分辨率中,中间梯度确实会形成90度的angular度。
  4. 最后,它将视图压缩到原来的大小。

这意味着中等坡度将不再以新的尺寸形成90度的angular度。 这与几乎所有其他油漆应用程序的行为相矛盾:素描,Photoshop等

如果您想要在Sketch中执行开始/结束点,则需要翻译开始/结束点来说明Apple将压缩视图的事实。


执行步骤(图)

在这里输入图像说明 在这里输入图像说明 在这里输入图像说明

 import UIKit /// Last updated 4/3/17. /// See https://stackoverflow.com/a/43176174 for more information. public enum LinearGradientFixer { public static func fixPoints(start: CGPoint, end: CGPoint, bounds: CGSize) -> (CGPoint, CGPoint) { // Naming convention: // - a: point a // - ab: line segment from a to b // - abLine: line that passes through a and b // - lineAB: line that passes through A and B // - lineSegmentAB: line segment that passes from A to B if start.x == end.x || start.y == end.y { // Apple's implementation of horizontal and vertical gradients works just fine return (start, end) } // 1. Convert to absolute coordinates let startEnd = LineSegment(start, end) let ab = startEnd.multiplied(multipliers: (x: bounds.width, y: bounds.height)) let a = ab.p1 let b = ab.p2 // 2. Calculate perpendicular bisector let cd = ab.perpendicularBisector // 3. Scale to square coordinates let multipliers = calculateMultipliers(bounds: bounds) let lineSegmentCD = cd.multiplied(multipliers: multipliers) // 4. Create scaled perpendicular bisector let lineSegmentEF = lineSegmentCD.perpendicularBisector // 5. Unscale back to rectangle let ef = lineSegmentEF.divided(divisors: multipliers) // 6. Extend line let efLine = ef.line // 7. Extend two lines from a and b parallel to cd let aParallelLine = Line(m: cd.slope, p: a) let bParallelLine = Line(m: cd.slope, p: b) // 8. Find the intersection of these lines let g = efLine.intersection(with: aParallelLine) let h = efLine.intersection(with: bParallelLine) if let g = g, let h = h { // 9. Convert to relative coordinates 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) // expected: (90.6119039567129, 26.3225059181603) print(result.p2) // expected: (199.388096043287, 38.6774940818397) } } 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 /// y = mx+b init(m: CGFloat, b: CGFloat) { self.m = m self.b = b } /// y-y1 = m(x-x1) init(m: CGFloat, p: CGPoint) { // y = m(x-x1) + y1 // y = mx-mx1 + y1 // y = mx + (y1 - mx1) // b = y1 - mx1 self.m = m self.b = py - m*px } 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? { // Line 1: y = mx + b // Line 2: y = nx + c // mx+b = nx+c // mx-nx = cb // x(mn) = cb // x = (cb)/(mn) let n = line.m let c = line.b if mn == 0 { // lines are parallel return nil } let x = (cb)/(mn) return point(x: x) } } 

certificate它可以工作,不pipe矩形大小

我尝试了这个视图size=320x60gradient=[red@0,green@0.5,blue@1]startPoint = (0,1)endPoint = (1,0)

素描3:

在这里输入图像说明

使用上面的代码实际生成的iOS屏幕截图:

在这里输入图像说明

请注意,绿线的angular度看起来100%准确。 区别在于红色和蓝色是如何混合的。 我不知道是不是因为我错误地计算了开始/结束点,或者只是苹果混合渐变与Sketch如何混合渐变的区别。

完整的layoutSubviews方法是

 override func layoutSubviews() { super.layoutSubviews() let gradientOffset = self.bounds.height / self.bounds.width / 2 self.gradientLayer.startPoint = CGPointMake(0, 0.5 + gradientOffset) self.gradientLayer.endPoint = CGPointMake(1, 0.5 - gradientOffset) self.gradientLayer.frame = self.bounds }