Alexey Savchenko
Alexey Savchenko

Reputation: 889

Masking CAGradientLayer over CALayers

In my scene I have 2 views: first holds CALayer instances (bars), another hold CAGradientLayer and placed over first one. Picture below describes current state.

Image

But I need this gradient to be applied only to bars (CALayer) of the first view.

enter image description here

I haven't found any relevant information to my problem. Any help appreciated.

Upvotes: 1

Views: 1512

Answers (1)

rob mayoff
rob mayoff

Reputation: 386068

You have to apply a mask to the gradient. There are various ways you could approach this problem.

You could create a CAShapeLayer, set the shape layer's path to the shape of the bars, and set the gradient layer's mask to that shape layer.

Or you could get rid of the bar layer and instead use two gradient layers, one for the orange bars and the other for the gray bars. Put both gradient layers in a subview, side-by-side, and set the superview's layer mask to the shape layer. Here's how to do that.

You'll need two gradient layers and a shape layer:

@IBDesignable
class BarGraphView : UIView {

    private let orangeGradientLayer = CAGradientLayer()
    private let grayGradientLayer = CAGradientLayer()
    private let maskLayer = CAShapeLayer()

You'll also need the bar width:

    private let barWidth = CGFloat(9)

At initialization time, set up the gradients and add all the sublayers:

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    private func commonInit() {
        backgroundColor = .black

        initGradientLayer(orangeGradientLayer, with: .orange)
        initGradientLayer(grayGradientLayer, with: .gray)

        maskLayer.strokeColor = nil
        maskLayer.fillColor = UIColor.white.cgColor
        layer.mask = maskLayer
    }

    private func initGradientLayer(_ gradientLayer: CAGradientLayer, with color: UIColor) {
        gradientLayer.colors = [ color, color, color.withAlphaComponent(0.6), color ].map({ $0.cgColor })
        gradientLayer.locations = [ 0.0, 0.5, 0.5, 1.0 ]
        layer.addSublayer(gradientLayer)
    }

At layout time, set the frames of the gradient layers and set the mask layer's path. This requires a little work because you don't want a bar to be half orange and half gray.

    override func layoutSubviews() {
        super.layoutSubviews()

        let barCount = ceil(bounds.size.width / barWidth)
        let orangeBarCount = floor(barCount / 2)
        let grayBarCount = barCount - orangeBarCount

        var grayFrame = bounds
        grayFrame.size.width = grayBarCount * barWidth
        grayFrame.origin.x = frame.maxX - grayFrame.size.width
        grayGradientLayer.frame = grayFrame

        var orangeFrame = bounds
        orangeFrame.size.width -= grayFrame.size.width
        orangeGradientLayer.frame = orangeFrame

        maskLayer.frame = bounds
        maskLayer.path = barPath()
    }

    private func barPath() -> CGPath {
        var columnBounds = self.bounds
        columnBounds.origin.x = columnBounds.maxX
        columnBounds.size.width = barWidth
        let path = CGMutablePath()
        for datum in barData.reversed() {
            columnBounds.origin.x -= barWidth
            let barHeight = CGFloat(datum) * columnBounds.size.height
            let barRect = columnBounds.insetBy(dx: 1, dy: (columnBounds.size.height - barHeight) / 2)
            path.addRoundedRect(in: barRect, cornerWidth: 2, cornerHeight: 2)
        }
        return path
    }

    let barData: [Double] = {
        let count = 100
        return (0 ..< count).map({ 0.5 + (1 + sin(8.0 * .pi * Double($0) / Double(count))) / 4 })
    }()

}

Result:

gradient bars

The BarGraphView is transparent wherever there are no bars. If you want it on a dark background, put a dark view behind it, or make it a subview of a dark view:

gradient bars on dark blue

Upvotes: 8

Related Questions