反转简单的 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 的遮罩动画示例如下所示:
我试图避免 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 的遮罩动画示例如下所示: