[ACCEPTED]-How to draw a gradient line (fading in/out) with Core Graphics/iPhone?-gradient

Accepted answer
Score: 35

It is possible to stroke arbitrary paths with a gradient, or any other fill effect, such as a pattern.

As you have found, stroked paths are not 33 rendered with the current gradient. Only 32 filled paths use the gradient (when you 31 turn them in to a clip and then draw the 30 gradient).

However, Core Graphics has an amazingly 29 cool procedure CGContextReplacePathWithStrokedPath that will transform the 28 path you intend to stroke in to a path that is 27 equivalent when filled.

Behind the scenes, CGContextReplacePathWithStrokedPath builds up an edge polygon 26 around your stroke path and switches that 25 for the path you have defined. I'd speculate 24 that the Core Graphics rendering engine 23 probably does this anyway in calls to CGContextStrokePath.

Here's 22 Apple's documentation on this:

Quartz creates 21 a stroked path using the parameters of the 20 current graphics context. The new path is 19 created so that filling it draws the same 18 pixels as stroking the original path. You 17 can use this path in the same way you use 16 the path of any context. For example, you 15 can clip to the stroked version of a path 14 by calling this function followed by a call 13 to the function CGContextClip.

So, convert 12 your path in to something you can fill, turn 11 that in to a clip, and then draw your gradient. The 10 effect will be as if you had stroked the 9 path with the gradient.

Code

It'll look something 8 like this…

    // Get the current graphics context.
    //
    const CGContextRef context = UIGraphicsGetCurrentContext();

    // Define your stroked path. 
    //
    // You can set up **anything** you like here.
    //
    CGContextAddRect(context, yourRectToStrokeWithAGradient);

    // Set up any stroking parameters like line.
    //
    // I'm setting width. You could also set up a dashed stroke
    // pattern, or whatever you like.
    //
    CGContextSetLineWidth(context, 1);

    // Use the magical call.
    //
    // It turns your _stroked_ path in to a **fillable** one.
    //
    CGContextReplacePathWithStrokedPath(context);

    // Use the current _fillable_ path in to define a clipping region.
    //
    CGContextClip(context);

    // Draw the gradient.
    //
    // The gradient will be clipped to your original path.
    // You could use other fill effects like patterns here.
    //
    CGContextDrawLinearGradient(
      context, 
      yourGradient, 
      gradientTop, 
      gradientBottom, 
      0
    );

Further notes

It's worth emphasising part of 7 the documentation above:

Quartz creates 6 a stroked path using the parameters of the current graphics context.

The obvious parameter is 5 the line width. However, all line drawing state 4 is used, such as stroke pattern, mitre limit, line 3 joins, caps, dash patterns, etc. This makes 2 the approach extremely powerful.

For additional 1 details see this answer of this S.O. question.

Score: 20

After several tries I'm now sure that gradients 7 doesn't affect strokes, so I think it's 6 impossible to draw gradient lines with CGContextStrokePath(). For 5 horizontal and vertical lines the solution 4 is to use CGContextAddRect() instead, which fortunately is 3 what I need. I replaced

CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);

with

CGContextSaveGState(context);
CGContextAddRect(context, CGRectMake(x, y, width, height));
CGContextClip(context);
CGContextDrawLinearGradient (context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);

and everything 2 works fine. Thanks to Brad Larson for the 1 crucial hint.

Score: 8

You can use Core Animation layers. You can 22 use a CAShaperLayer for your line by settings its path 21 property and then you can use a CAGradientLayer as a layer 20 mask to your shape layer that will cause 19 the line to fade.

Replace your CGContext... calls 18 with calls to CGPath... calls to create 17 the line path. Set the path field on the 16 layer using that path. Then in your gradient 15 layer, specify the colors you want to use 14 (probably black to white) and then set the 13 mask to be the line layer like this:

 [gradientLayer setMask:lineLayer];

What's 12 cool about the gradient layer is that is 11 allows you to specify a list of locations 10 where the gradient will stop, so you can 9 fade in and fade out. It only supports linear 8 gradients, but it sounds like that may fit 7 your needs.

Let me know if you need clarification.

EDIT: Now 6 that I think of it, just create a single 5 CAGradientLayer that is the width/height 4 of the line you desire. Specify the gradient 3 colors (black to white or black to clear 2 color) and the startPoint and endtPoints 1 and it should give you what you need.

Score: 8

After you draw the line, you can call

CGContextClip(context);

to 11 clip further drawing to your line area. If 10 you draw the gradient, it should now be 9 contained within the line area. Note that 8 you will need to use a clear color for your 7 line if you just want the gradient to show, and 6 not the line underneath it.

There is the 5 possibility that a line will be too thin 4 for your gradient to show up, in which case 3 you can use CGContextAddRect() to define a thicker area.

I 2 present a more elaborate example of using 1 this context clipping in my answer here.

Score: 1

I created a Swift version of Benjohn's answer.

class MyView: UIView {
    override func draw(_ rect: CGRect) {
    super.draw(rect)

    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }

    context.addPath(createPath().cgPath)
    context.setLineWidth(15)
    context.replacePathWithStrokedPath()
    context.clip()

    let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor] as CFArray, locations: [0, 0.4, 1.0])!
    let radius:CGFloat = 200
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
    context.drawRadialGradient(gradient, startCenter: center, startRadius: 10, endCenter: center, endRadius: radius, options: .drawsBeforeStartLocation)
    }
}

If 3 the createPath() method creates a triangle, you 2 get something like this:

enter image description here

Hope this helps 1 anyone!

Score: 1

Swift 5 Version

This code worked for me.

override func draw(_ rect: CGRect) {
    super.draw(rect)
    
    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    
    drawGradientBorder(rect, context: context)
}

fileprivate 
func drawGradientBorder(_ rect: CGRect, context: CGContext) {
    context.saveGState()
    let path = ribbonPath()
    context.setLineWidth(5.0)
    context.addPath(path.cgPath)
    context.replacePathWithStrokedPath()
    context.clip()
    
    let baseSpace = CGColorSpaceCreateDeviceRGB()
    let gradient = CGGradient(colorsSpace: baseSpace, colors: [start.cgColor, end.cgColor] as CFArray, locations: [0.0 ,1.0])!
    context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: rect.height/2), end: CGPoint(x: rect.width, y: rect.height/2), options: [])
    context.restoreGState()
}

fileprivate 
func ribbonPath() -> UIBezierPath {
    let path = UIBezierPath()
    path.move(to: CGPoint.zero)
    path.addLine(to: CGPoint(x: bounds.maxX - gradientWidth, y: 0))
    path.addLine(to: CGPoint(x: bounds.maxX - gradientWidth - ribbonGap, y: bounds.maxY))
    path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
    path.close()
    return path
}

0

Score: 0
CGContextMoveToPoint(context, frame.size.width-200, frame.origin.y+10);
CGContextAddLineToPoint(context, frame.size.width-200, 100-10);
CGFloat colors[16] = { 0,0, 0, 0,
    0, 0, 0, .8,
    0, 0, 0, .8,
    0, 0,0 ,0 };
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 4);

CGContextSaveGState(context);
CGContextAddRect(context, CGRectMake(frame.size.width-200,10, 1, 80));
CGContextClip(context);
CGContextDrawLinearGradient (context, gradient, CGPointMake(frame.size.width-200, 10), CGPointMake(frame.size.width-200,80), 0);
CGContextRestoreGState(context);

its work for me.

0

Score: 0

Based on @LinJin's answer, here's something 3 that draws a linear rainbow gradient border 2 around a rectangle... I guess the start 1 of a rectangular UIColorWell sort of thing.

Swift 5

import UIKit
import QuartzCore

class RectangularColorWell : UIControl {
    
    var lineWidth : CGFloat = 3.0 {
        didSet {
            self.setNeedsDisplay()
        }
    }
      
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(CustomToggleControl.controlTapped(_:)))
        tapGestureRecognizer.numberOfTapsRequired = 1;
        tapGestureRecognizer.numberOfTouchesRequired = 1;
        self.addGestureRecognizer(tapGestureRecognizer)
        self.setNeedsDisplay()
        
    }
    override func draw(_ rect: CGRect) {
        guard let ctx = UIGraphicsGetCurrentContext() else { return }
        ctx.setLineWidth(lineWidth)
        ctx.setFillColor(backgroundColor!.cgColor)
        ctx.fill(rect)
        drawGradientBorder(rect, context: ctx)
    }
    
    
    func drawGradientBorder(_ rect: CGRect, context: CGContext) {
        context.saveGState()
        let path = UIBezierPath(rect: rect)
        context.addPath(path.cgPath)
        context.replacePathWithStrokedPath()
        context.clip()
        
        let rainbowColors : [UIColor] = [ .red, .orange, .yellow, .green, .blue, .purple ]
        let colorDistribution: [CGFloat] = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0 ]
        let gradient = CGGradient(
                 colorsSpace: CGColorSpaceCreateDeviceRGB(),
                 colors: rainbowColors.map { $0.cgColor } as CFArray,
                 locations: colorDistribution)!
        context.drawLinearGradient(gradient, 
                 start: CGPoint(x: 0, y: 0), 
                 end: CGPoint(x: rect.width, y: rect.height), options: [])
        context.restoreGState()
    }

    
    @objc func controlTapped(_ gestureRecognizer :UIGestureRecognizer) {
        UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
            self.alpha = 0.5
        }, completion: { (finished: Bool) -> Void in
            UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseOut, animations: {
                self.alpha = 1.0
            }, completion: { (finished: Bool) -> Void in
            })
        })

        setNeedsDisplay()
        sendActions(for: .touchDown)
    }
}

More Related questions