0

I have the following code:

public override void ViewWillAppear(bool animated)
{
    base.ViewWillAppear(animated);

    var gradient = new CAGradientLayer();
    gradient.Frame = view.Bounds;
    gradient.Colors = colors;

    var mask = new CALayer();
    mask.Frame = view.Bounds;
    mask.Contents = view.Image.CGImage;
    mask.ContentsGravity = CALayer.GravityCenter;

    gradient.Mask = mask;
    view.Layer.AddSublayer(gradient);
}

which produces the following result: image

"view" is a UIImageView which is centered perfectly.

The number "1" is exactly in the middle. Here you can see that the mask is not centered perfectly.

The image here is the heart icon from the standard resources, it's not a custom image. (If e.g. used in a button, then it's centered perfectly).

Before posting I've tried all possible combinations when it comes to setting Frame and Bounds, the mask is always off-center by a few pixels.

Someone else reported a similar issue here, but there was no solution: Gradient Color over Template Image in Swift 4

This makes me believe that there's a bug with how masks are applied. Could anyone confirm?

ison
  • 173
  • 2
  • 9
  • 1
    “there's a bug with how masks are applied” There isn’t. Trust me on this one. – matt Mar 20 '21 at 18:42
  • @matt How comes a simple code like this produces incorrect results then? It looks like masks are not centered, even if CALayer.GravityCenter is used. Or is there some edge-case here with how Frames should be set? – ison Mar 20 '21 at 18:47
  • You have not shown enough code to reproduce the issue, nor have you even explained what you’re trying to do, so no further assistance is possible. I’m just saying you won’t figure this out unless you start by assuming that _you_ are making a mistake. – matt Mar 20 '21 at 18:53
  • @matt The only other thing is a storyboard with a centered UIImageView and a centered label. I'm not sure if I can upload it somewhere here. – ison Mar 20 '21 at 18:55
  • For example maybe you are doing this in the equivalent of viewDidLoad. But that is too soon, no frames are known. As I say, without sufficient code and clear goal, question is pointless. – matt Mar 20 '21 at 18:55
  • @matt It's in ViewWillAppear. I've updated the code. If you think there's anything else that'd be good to post then let me know. – ison Mar 20 '21 at 18:57
  • I still do not know what you are wishing to _do_. Is it a secret? Surely the goal is not a weird red blob. – matt Mar 20 '21 at 19:10
  • @matt I'd like the image to be centered, as per title: "Why is CALayer mask not centered and off by a few pixels?" – ison Mar 20 '21 at 19:12
  • But an image is not a mask. What image and what does it have to do with gradients and centering and masking? – matt Mar 20 '21 at 19:13
  • @matt An image can be used as a mask, like above. It's a gradient layer with an image used as a mask, which produces a red shape on the screen which is not centered. There are no other widgets involved here, only the ones mentioned in the code above. – ison Mar 20 '21 at 19:15
  • So the question is: place a gradient layer on the entire screen, and mask it exactly in the center with an image? – matt Mar 20 '21 at 19:37
  • @matt Not sure what you mean, but yes, then the masked image is off-center by a few pixels. You can see a similar result here: https://stackoverflow.com/questions/47122957/gradient-color-over-template-image-in-swift-4 (there's an image). I found somewhere that masks actually use a different coord space, so maybe this is the reason, but I don't know how to translate Frame to these mask coords. – ison Mar 20 '21 at 19:38

1 Answers1

1

There’s no bug with masking. This is crucial stuff, basic to all drawing. If there were a bug the whole interface would be broken. The bug is in your code.

It's all basically just a matter of timing. Do not talk about any frames or bounds until you know what they actually are.

To demonstrate, I'm going to work backwards: first I'll show you the result of my code, then I'll show you the code. Here's the result. I start with a storyboard that has a square centered by autolayout, so you know I'm not cheating:

enter image description here

Now I run the app and superimpose over the entire screen a gradient layer with a centered heart image mask:

enter image description here

As you can see, the heart is in exactly the right place. It is centered in precisely the same sense as the yellow square, and that is why it appears centered in the square.

Now here's the code. Notice that the assembly work is done just once, in viewDidLoad, but all the measurement work is done when we have actual measurements to work with, namely, in viewDidLayoutSubviews. This is Swift but you should have no difficulty translating (C# or whatever):

class ViewController: UIViewController {
    
    let gradient : CAGradientLayer = {
        let g = CAGradientLayer()
        g.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
        return g
    }()
    
    let mask = CALayer()
    
    let image : UIImage = {
        let im = UIImage(systemName: "heart.fill")!
        return im
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.layer.addSublayer(gradient)
        mask.contents = image.cgImage
        mask.contentsGravity = .center
        gradient.mask = mask
    }
    
    override func viewDidLayoutSubviews() {
        gradient.frame = view.bounds
        mask.frame = gradient.bounds
    }
}

Go ye and do likewise.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • So I started a new project and it indeed worked! I tried to figure out why it'd produce different results than in my case, and it turned out to be because I did it in ViewWillAppear which apparently was too soon. I moved my code to ViewDidAppear and it worked. I didn't realize ViewWillAppear could be too soon. And the image was off only by a few pixels. Thanks! – ison Mar 22 '21 at 11:52
  • 1
    In my opinion viewDidAppear is likely to be too _late_ — the user can see the drawing jump. That is why I use viewDidLayoutSubviews. – matt Mar 22 '21 at 19:19