CAGradientLayer对角线渐变

时间:2016-08-08 05:05:28

标签: ios swift calayer cagradientlayer

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应用程序图像中的结果(见上文)。

我怎样才能做到这一点?

3 个答案:

答案 0 :(得分:34)

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

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

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

  1. 首先,它将视图转换为正方形。
  2. 然后使用起点/终点应用渐变。
  3. 中间渐变确实会在此分辨率下形成90度角。
  4. 最后,它将视图缩小到原始大小。
  5. 这意味着中间渐变将不再在新尺寸中形成90度角。这与几乎所有其他绘画应用程序的行为相矛盾:Sketch,Photoshop等。

    如果你想在Sketch中使用起点/终点,那么你需要翻译起点/终点,以便考虑到苹果会破坏视图的事实。

    执行步骤(图表)

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

    代码

    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:

    enter image description here

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

    enter image description here

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

答案 1 :(得分:0)

这是修复端点的数学方法

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 :(得分:-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
}