CAGradientLayer对角线渐变

问题描述 投票:20回答:3

enter image description here

我使用以下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]

但是当我为图层设置bounds属性时,它只是拉伸一个方形渐变。我需要像Sketch 3应用程序图像中的结果(见上文)。

我怎样才能做到这一点?

ios swift calayer cagradientlayer
3个回答
34
投票

更新:在manner similar to the following中使用context.drawLinearGradient()而不是CAGradientLayer。它将绘制与Sketch / Photoshop一致的渐变。

如果你绝对必须使用CAGradientLayer,那么这里是你需要使用的数学...


花了一些时间来弄明白,但仔细观察后,我发现Apple在CAGradientLayer中实现渐变是非常奇怪的:

  1. 首先,它将视图转换为正方形。
  2. 然后使用起点/终点应用渐变。
  3. 中间梯度确实在该分辨率下形成90度角。
  4. 最后,它将视图缩小到原始大小。

这意味着中间渐变将不再在新尺寸中形成90度角。这与几乎所有其他绘画应用程序的行为相矛盾:Sketch,Photoshop等。

如果你想在Sketch中使用起点/终点,那么你需要将起点/终点转换为考虑到Apple将要挤压视图的事实。


Steps to perform (Diagrams)

enter image description here enter image description here enter image description here

Code

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 = 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? {
    // Line 1: y = mx + b
    // Line 2: y = nx + c
    // mx+b = nx+c
    // mx-nx = c-b
    // x(m-n) = c-b
    // x = (c-b)/(m-n)
    let n = line.m
    let c = line.b
    if m-n == 0 {
      // lines are parallel
      return nil
    }
    let x = (c-b)/(m-n)
    return point(x: x)
  }
}

Proof it works regardless of rectangle size

我尝试了size=320x60gradient=[red@0,[email protected],blue@1]startPoint = (0,1)endPoint = (1,0)

Sketch 3:

enter image description here

Actual generated iOS screenshot using the code above:

enter image description here

请注意,绿线的角度看起来100%准确。不同之处在于红色和蓝色的混合方式。我不知道是不是因为我正在错误地计算起点/终点,或者这只是Apple混合渐变与Sketch混合渐变的方式之间的区别。


0
投票

这是修复endPoint的数学方法

let width = bounds.width
let height = bounds.height
let dx = endPoint.x - startPoint.x
let dy = endPoint.y - startPoint.y
if width == 0 || height == 0 || width == height || dx == 0 || dy == 0 {
  return
}
let ux = dx * width / height
let uy = dy * height / width
let coef = (dx * ux + dy * uy) / (ux * ux + uy * uy)
endPoint = CGPoint(x: startPoint.x + coef * ux, y: startPoint.y + coef * uy)

-2
投票

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
}
© www.soinside.com 2019 - 2024. All rights reserved.