CAGradientLayer对角线渐变 [英] CAGradientLayer diagonal gradient

查看:131
本文介绍了CAGradientLayer对角线渐变的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



我使用以下CAGradientLayer:

 让图层= 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 app图像的结果(见上文)。



我怎样才能实现这个目标?

解决方案

更新:

< a href =https://i.stack.imgur.com/HtJcc.png =noreferrer>



代码



  import UIKit 

///最后更新时间:4/3/17。
///有关详细信息,请参阅https://stackoverflow.com/a/43176174。
public enum LinearGradientFixer {
public static func fixPoints(start:CGPoint,end:CGPoint,bounds:CGSize) - > (CGPoint,CGPoint){
//命名约定:
// - a:指向a
// - ab:从a到b
//的线段 - abLine:穿过a和b
//的线 - lineAB:穿过A和B
的线// lineSegmentAB:从A传递到B

的线段如果开始.x == end.x || start.y == end.y {
// Apple的水平和垂直渐变实现很好
返回(开始,结束)
}

// 1.转换为绝对坐标
let startEnd = LineSegment(start,end)
let ab = startEnd.multiplied(multipliers:(x:bounds.width,y:bounds.height))
设a = ab.p1
让b = ab.p2

// 2.计算垂直平分线
让cd = ab.perpendicularBisector

/ / 3.缩放到方坐标
let multipliers = calculateMultipliers(bounds:bounds)
let lineSegmentCD = cd.multiplied(multipliers:multipliers)

// 4.创建缩放垂直bisector
let lineSegmentEF = lineSegmentCD.perpendicularBisector

// 5.将尺寸缩小回矩形
let ef = lineSegmentEF.divided(除数:乘数)

// 6.扩展行
让efLine = ef.line

// 7.扩展两行从a和b并行到cd
让aParallelLine = Line(m:cd.slope,p:a)
let bParallelLine = Line(m:cd.slope,p:b)

// 8.找到这些行的交集
let g = efLine.intersection(with:aParallelLine)
let h = efLine.intersection(with:bParallelLine)

如果设g = g,设h = h {
// 9.转换为相对坐标
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)
设a = CGPoint(x:138.5,y:11.5)
让b = CGPoint(x:151.5,y:53.5)
让ab = LineSegment(a,b)
let startEnd = ab.divided(divisors:(x:bounds.width,y:bounds.height))
let start = startEnd.p1
le t 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)//预期:(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,距离: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
让dy = p2.y - p1.y
返回sqrt(dx * dx + dy * dy)
}
var距离:CGFloat {
返回p1.x< = p2.x?长度:-length
}
var midpoint:CGPoint {
返回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:verticalSlope,distance:-distance / 2).p2
let p2 = LineSegment(p1:midpoint,m:perpendicularSlope,distance:distance / 2).p2
返回LineSegment(p1,p2)
}

func multiplied(乘数:(x:CGFloat,y:CGFloat)) - > LineSegment {
返回LineSegment(
CGPoint(x:p1.x * multipliers.x,y:p1.y * multipliers.y),
CGPoint(x:p2.x *乘数。 x,y:p2.y * multipliers.y))
}
func divide(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 {
返回CGPoint(x:x,y:y(x:x))
}

func十字路口(带线:线) - > CGPoint? {
//第1行:y = mx + b
//第2行:y = nx + c
// mx + b = nx + c
// mx- nx = cb
// x(mn)= cb
// x =(cb)/(mn)
让n = line.m
让c = line.b
如果mn == 0 {
//行是并行
返回nil
}
让x =(cb)/(mn)
返回点(x:x)
}
}



无论矩形如何,证明它都有效尺寸



我尝试了这个视图 size = 320x60 gradient = [red @ 0,绿色@ 0.5,蓝色@ 1] startPoint =(0,1) endPoint =(1 ,0)



草图3:





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





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


I use the following 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]

But when I set bounds property for the layer, it just stretches a square gradient. I need a result like in Sketch 3 app image (see above).

How can I achieve this?

解决方案

Update: Use context.drawLinearGradient() instead of CAGradientLayer in a manner similar to the following. It will draw gradients that are consistent with Sketch/Photoshop.

If you absolutely must use CAGradientLayer, then here is the math you'll need to use...


It took some time to figure out, but from careful observation, I found out that Apple's implementation of gradients in CAGradientLayer is pretty odd:

  1. First it converts the view to a square.
  2. Then it applies the gradient using start/end points.
  3. The middle gradient will indeed form a 90 degree angle in this resolution.
  4. Finally, it squishes the view down to the original size.

This means that the middle gradient will no longer form a 90 degree angle in the new size. This contradicts the behavior of virtually every other paint application: Sketch, Photoshop, etc.

If you want to implement start/end points as it works in Sketch, you'll need to translate the start/end points to account for the fact that Apple is going to squish the view.


Steps to perform (Diagrams)

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

I tried this with a view size=320x60, gradient=[red@0,green@0.5,blue@1], startPoint = (0,1), and endPoint = (1,0).

Sketch 3:

Actual generated iOS screenshot using the code above:

Note that the angle of the green line looks 100% accurate. The difference lies in how the red and blue are blended. I can't tell if that's because I'm calculating the start/end points incorrectly, or if it's just a difference in how Apple blends gradients vs. how Sketch blends gradients.

这篇关于CAGradientLayer对角线渐变的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆