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?
There are many ways to create background gradients, below is just one simple approach: In a new Xcode iOS project, open Main. storyboard, from the Object Library drag a new View onto the View Controller. Set the View's top, bottom, left and right constraints to be zero and ensure 'Constrain to margins' is deselected.
You use a gradient layer to create a color gradient containing an arbitrary number of colors. By default, the colors are spread uniformly across the layer, but you can optionally specify locations for control over the color positions through the gradient.
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:
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.
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)
}
}
I tried this with a view size=320x60
, gradient=[red@0,green@0.5,blue@1]
, startPoint = (0,1)
, and endPoint = (1,0)
.
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.
Here's the math to fix the 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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With