【问题标题】:CAGradientLayer diagonal gradientCAGradientLayer 对角渐变
【发布时间】:2016-12-13 18:45:26
【问题描述】:

我使用以下 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 应用图像的结果(见上文)。

我怎样才能做到这一点?

【问题讨论】:

  • 也分享帧大小
  • 其实没关系,320x60就是一个很好的例子。

标签: ios swift calayer cagradientlayer


【解决方案1】:

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
}

【讨论】:

  • 这更接近了,但与 Sketch 给你的距离还很远。
  • 适用于任何矩形大小的实际公式似乎需要更多的数学运算。查看this answer 了解我的解决方案。
【解决方案2】:

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

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


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

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

这意味着中间渐变在新尺寸中将不再形成 90 度角。这与几乎所有其他绘画应用程序的行为相矛盾:Sketch、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 = 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)
  }
}

证明无论矩形大小如何都能正常工作

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

草图3:

使用上述代码实际生成的iOS截图:

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

【讨论】:

  • 非常好的答案,但我认为您对“转换为正方形”的想法并不准确。由于startPointendPoint 在单位坐标中(从0 到1),我认为它们在绘制渐变时也会在单位坐标中插值。这会产生拉伸或调整大小的位图的效果,但我很确定没有采取任何中间步骤来实现此结果。
【解决方案3】:

这是修复端点的数学方法

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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多