CAGradientLayer gradiente diagonal

introduzca la descripción de la imagen aquí

Utilizo el siguiente CAGradientLayer:

let layer = CAGradientLayer() layer.colors = [ UIColor.networkingColor().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] 

Pero cuando configuro la propiedad de límites para la capa, simplemente se estira un gradiente cuadrado. Necesito un resultado como en la image de la aplicación Sketch 3 (ver arriba).

¿Cómo puedo conseguir esto?

Actualización: utilice context.drawLinearGradient () en lugar de CAGradientLayer de una manera similar a la siguiente . Dibujará gradientes que son consistentes con Sketch / Photoshop.

Si debes utilizar CAGradientLayer, entonces estas son las matemáticas que debes usar …


Tardé un time en descubrirlo, pero a partir de una observación cuidadosa, descubrí que la implementación de gradientes de Apple en CAGradientLayer es bastante extraña:

  1. Primero convierte la vista en un cuadrado.
  2. A continuación, aplica el gradiente utilizando los puntos de inicio / finalización.
  3. El gradiente medio de hecho formará un ángulo de 90 grados en esta resolución.
  4. Finalmente, aplasta la vista hasta el tamaño original.

Esto significa que el gradiente medio ya no formará un ángulo de 90 grados en el nuevo tamaño. Esto contradice el comportamiento de prácticamente cualquier otra aplicación de pintura: Sketch, Photoshop, etc.

Si desea implementar puntos de inicio / finalización ya que funciona en Sketch, deberá traducir los puntos de inicio / finalización para tener en count el hecho de que Apple va a aplastar la vista.


Pasos para realizar (Diagtwigs)

introduzca la descripción de la imagen aquí introduzca la descripción de la imagen aquí introduzca la descripción de la imagen aquí

Código

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

Prueba de que funciona independientemente del tamaño del rectángulo.

Intenté esto con un size=320x60 vista size=320x60 , gradient=[networking@0,green@0.5,blue@1] , startPoint = (0,1) y endPoint = (1,0) .

Dibujo 3:

introduzca la descripción de la imagen aquí

Pantalla de iOS generada real usando el código anterior:

introduzca la descripción de la imagen aquí

Tenga en count que el ángulo de la línea verde se ve 100% preciso. La diferencia radica en cómo se mezclan el rojo y el azul. No puedo decir si eso es porque estoy calculando los puntos de inicio / finalización incorrectamente, o si es solo una diferencia en cómo Apple mezcla los gradientes frente a la forma en que Sketch mezcla los gradientes.

Código completo del método layoutSubviews es

 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 }