Is it possible to make 2 subviews with same z index?

156 Views Asked by At

I am looking to create a water fill effect into a shape and am using WaveViewAnimation project.

I want to allow the user to press on different buttons to create different colour waves to fill the shape. And when user presses on second colour while the first colour is

waveRed = WaveAnimationView(frame: CGRect(origin: .zero, size: lapView.bounds.size), color: UIColor.red.withAlphaComponent(0.5))
    lapView.addSubview(waveRed)

waveBlue = WaveAnimationView(frame: CGRect(origin: .zero, size: lapView.bounds.size), color: UIColor.green.withAlphaComponent(0.5))
lapView.addSubview(waveBlue)

waveRed.layer.zPosition = 1
waveBlue.layer.zPosition = 1
waveRed.startAnimation()
waveBlue.startAnimation()

The output is something like this

I want the output something like the combination of 2 colours blended, something like the If red wave is filled and green was is pressed. The wave overlap area needs to be in yellow colour.

Could someone please guide me/advice me how to achieve this.

1

There are 1 best solutions below

0
DonMag On

You may be able to get your desired result by using the .compositingFilter property of CALayer.

There are various "blend" filters... this one:

topLayer.compositingFilter = "screenBlendMode"

is probably what you want to play with.

Apple's docs on it are vague -

Discussion

This results in colors that are at least as light as either of the two contributing sample colors. The formula used to create this filter is described in the PDF specification, which is available online from the Adobe Developer Center. See PDF Reference and Adobe Extensions to the PDF Specification.

Of course, the link doesn't help... but I did find this from searching:

Screen

Multiplies the complements of the backdrop and source color values, then complements the result. The result color is always at least as light as either of the two constituent colors. Screening any color with white produces white; screening with black leaves the original color unchanged. The effect is similar to projecting multiple photographic slides simultaneously onto a single screen.

So, if we have a red circle overlapping a green circle...

We get this with NO filter (or the default "normalBlendMode"):

enter image description here

If we give the red circle layer .compositingFilter = "screenBlendMode" we get this:

enter image description here

As we see, "screen blending" 1, 0, 0, 1 (red) with 0, 1, 0, 1 (green) gives us 1, 1, 0, 1 -- yellow.

Notice that we lose the "top" of the red circle. That's because any color screen-blended with white results in white.

If we add another "bottom" layer matching the top red layer, like this:

enter image description here

We'll get this result:

enter image description here

because, as we'd expect, any color screen-blended with itself results in itself.

Here's the sample code I used to produce these images that you can play with:

class ViewController: UIViewController {

    let theView: UIView = UIView()
    
    let topLayer = CAShapeLayer()
    let bottomLayer = CAShapeLayer()
    let middleLayer = CAShapeLayer()
    
    let infoLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        v.textAlignment = .center
        v.font = .systemFont(ofSize: 18.0, weight: .light)
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        theView.backgroundColor = .white
        infoLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        theView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(theView)
        
        infoLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(infoLabel)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            // add the view 20-points inset from the sides
            //  height 1.2 times the width
            //  centered vertically
            theView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            theView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            theView.heightAnchor.constraint(equalTo: theView.widthAnchor, multiplier: 1.2),
            theView.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: -40.0),
            
            infoLabel.topAnchor.constraint(equalTo: theView.bottomAnchor, constant: 20.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            infoLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0),
        ])

        // bottom layer is red
        bottomLayer.fillColor = UIColor.red.cgColor
        // middle layer is green
        middleLayer.fillColor = UIColor.green.cgColor
        // top layer same color as bottom layer
        topLayer.fillColor = UIColor.red.cgColor

        theView.layer.addSublayer(bottomLayer)
        theView.layer.addSublayer(middleLayer)
        theView.layer.addSublayer(topLayer)
        
        nextStep()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // set layer shapes to overlapping ovals
        var b = theView.bounds.insetBy(dx: 20.0, dy: 20.0)
        b.size.height *= 0.75
        
        var pth = UIBezierPath(ovalIn: b)

        // bottom and top layers get the same path
        //  so they exactly overlay each other
        bottomLayer.path = pth.cgPath
        topLayer.path = pth.cgPath
        
        // shift the oval rect down for the "middle" layer
        b.origin.y += theView.bounds.height * 0.25 - 10.0
        
        pth = UIBezierPath(ovalIn: b)
        middleLayer.path = pth.cgPath
        
    }
    
    var counter: Int = -1
    
    func nextStep() {
        
        counter += 1
        
        switch counter % 3 {
        case 1:
            // screen blend
            //  hide bottom layer
            topLayer.compositingFilter = "screenBlendMode"
            bottomLayer.opacity = 0
            infoLabel.text = "Screen Blend - with bottom layer hidden"
        case 2:
            // screen blend
            //  show bottom layer
            topLayer.compositingFilter = "screenBlendMode"
            bottomLayer.opacity = 1
            infoLabel.text = "Screen Blend - with bottom layer visible"
        default:
            // normal blend
            //  bottom layer opacity doesn't matter, since it will be covered by top layer
            topLayer.compositingFilter = "normalBlendMode"
            infoLabel.text = "Default - no Blend Effect"
        }
        
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        nextStep()
    }
    
}

Each tap anywhere on the screen will step through the different blend-modes and "bottom layer" visibility.