反转简单的 UIView 掩码(切孔而不是剪辑到圆圈)

Invert simple UIView mask (cut hole instead of clip to circle)

我试图避免 CAShapeLayer,因为我需要弄乱 CABasicAnimation,而不是 UIView.animate。因此,我只是使用 UIView 的 mask 属性 来屏蔽视图。这是我目前的代码:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 300))
        imageView.image = UIImage(named: "TestImage")
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        view.addSubview(imageView)
        
        let maskView = UIView(frame: CGRect(x: 100, y: 100, width: 80, height: 80))
        maskView.backgroundColor = UIColor.blue /// ensure opaque
        maskView.layer.cornerRadius = 10
        
        imageView.mask = maskView /// set the mask
    }
}
Without imageView.mask = maskView With imageView.mask = maskView

它使图像视图的一部分可见。然而,这就是我想要的:

我怎样才能在其中切一个洞而不是使部分图像视图可见?

您可以创建图像视图并将其设置为遮罩。请注意,这不适用于动画。如果您想将蒙版动画化为不同的形状,您应该向视图的 CALayer 添加一个蒙版并使用 CALayerAnimation,如您所提到的。还不错。

下面我概述了如何生成具有透明部分(孔)的图像,您可以将其用作图像视图中的遮罩。但是,如果您的目标是为孔的大小、形状或位置设置动画,那么这将不起作用。您必须为每一帧重新生成蒙版图像,这真的很慢。

以下是使用图像视图作为遮罩获得静态视图所需效果的方法:

使用 UIGraphicsBeginImageContextWithOptions() UIGraphicsImageRenderer 创建一个对大部分图像不透明的图像,并在您想要的位置有一个透明的“孔”孔.

然后将该图像安装到您的图像视图中,并使该图像视图成为您的遮罩。

创建带有透明圆角矩形“孔”的大部分不透明图像的代码可能如下所示:

/**
 Function to create a UIImage that is mostly opaque, with a transparent rounded rect "knockout" in it. Such an image might be used ask a mask
 for another view, where the transparent "knockout" appears as a hole in the view that is being masked.
    - Parameter size:  The size of the image to create
    - Parameter transparentRect: The (rounded )rectangle to make transparent in the middle of the image.
    - Parameter cornerRadius: The corner radius ot use in the transparent rectangle. Pass 0 to make the rectangle square-cornered.
 */
func imageWithTransparentRoundedRect(size: CGSize, transparentRect: CGRect, cornerRadius: CGFloat) -> UIImage? {
    let renderer = UIGraphicsImageRenderer(size: size)
    let image = renderer.image { (context) in
        let frame = CGRect(origin: .zero, size: size)
        UIColor.white.setFill()
        context.fill(frame)
        let roundedRect = UIBezierPath(roundedRect: transparentRect, cornerRadius: cornerRadius)
        context.cgContext.setFillColor(UIColor.clear.cgColor)
        context.cgContext.setBlendMode(.clear)

        roundedRect.fill()
    }
    return image
}

还有一个 viewDidLoad 安装 UIImageView 的方法,它带有一个有孔的蒙版图像视图,可能如下所示:

override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .cyan
        let size = CGSize(width: 200, height: 300)
        let origin = CGPoint(x: 50, y: 50)
        let frame =  CGRect(origin: origin, size: size)
        let imageView = UIImageView(frame: frame)
        imageView.image = UIImage(named: "TestImage")
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        view.addSubview(imageView)
        imageView.layer.borderWidth = 2

        //Create a mask image view the same size as the (image) view we will be masking
        let maskView = UIImageView(frame: imageView.bounds)

        //Build an opaque UIImage with a transparent "knockout" rounded rect inside it.
        let transparentRect = CGRect(x: 100, y: 100, width: 80, height: 80)
        let maskImage = imageWithTransparentRoundedRect(size: size, transparentRect: transparentRect, cornerRadius: 20)

        //Install the image with the "hole" into the mask image view
        maskView.image = maskImage

        //Make the maskView the ImageView's mask
        imageView.mask = maskView /// set the mask
    }
}

我使用上面的代码创建了一个示例项目。您可以从 Github 此处下载:

https://github.com/DuncanMC/UIImageMask.git

我刚刚更新了项目,以展示如何使用 CAShapeLayer 作为图像视图层上的遮罩来做同样的事情。这样做,就可以对遮罩层路径的变化进行动画处理。

新版本有一个分段控件,让您可以选择是使用视图遮罩中的 UIImage 遮罩图像视图 属性,还是通过用作图像视图图层遮罩的 CAShapeLayer。

对于 CAShapeLayer 版本,遮罩层的路径是整个图像视图大小的矩形,其中绘制了第二个较小的圆角矩形。然后将形状图层上的缠绕规则设置为“even/odd”规则,这意味着如果必须穿过偶数个形状边界才能到达某个点,则认为它在形状之外。这使您能够创建我们在这里需要的空心形状。

当您 select 图层蒙版选项时,它会启用一个动画按钮,该按钮会随机更改蒙版中的“剪切”透明矩形。

创建掩码路径的函数如下所示:

func maskPath(transparentRect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
    let fullRect = UIBezierPath(rect: maskLayer.frame)
    let roundedRect = UIBezierPath(roundedRect: transparentRect, cornerRadius: cornerRadius)
    fullRect.append(roundedRect)
    return fullRect
}

制作动画的函数如下所示:

@IBAction func handleAnimateButton(_ sender: Any) {

    //Create a CABasicAnimation that will change the path of our maskLayer
    //Use the keypath "path". That tells the animation object what property we are animating
    let animation = CABasicAnimation(keyPath: "path")

    animation.autoreverses = true //Make the animation reverse back to the oringinal position once it's done

    //Use ease-in, ease-out timing, which looks smooth
    animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)

    animation.duration = 0.3 //Make each step in the animation last 0.3 seconds.


    let transparentRect: CGRect

    //Randomly either animate the transparent rect to a different shape or shift it
    if Bool.random() {
        //Make the transparent rect taller and skinnier
        transparentRect = self.transparentRect.inset(by: UIEdgeInsets(top: -20, left: 20, bottom: -20, right: 20))
    } else {
        //Shift the transparent rect to by a random amount that still says inside the image view's bounds.
        transparentRect = self.transparentRect.offsetBy(dx: CGFloat.random(in: -100...20), dy: CGFloat.random(in: -100...100))
    }

    let cornerRadius: CGFloat = CGFloat.random(in: 0...30)
    //install the new path as the animation's `toValue`. If we dont specify a `fromValue` the animation will start from the current path.
    animation.toValue = maskPath(transparentRect: transparentRect, cornerRadius: cornerRadius).cgPath

    //add the animation to the maskLayer. Since the animation's `keyPath` is "path",
    //it will animate the layer's "path" property to the "toValue"
    maskLayer.add(animation, forKey: nil)

    //Since we don't actually change the path on the mask layer, the mask will revert to it's original path once the animation completes.
}

结果(使用我自己的示例图像)如下所示:

基于 CALayer 的遮罩动画示例如下所示: