使用 UILabel 子层切割覆盖图像的角
Using a UILabel Sublayer to Cut Off Corners Overlaying an Image
我在编写用于切断 UILabel(或者,实际上,您可以添加子层的任何 UIView 派生对象)的角的代码时遇到了问题 -- 我必须感谢 Kurt Revis 对 Use a CALayer to add a diagonal banner/badge to the corner of a UITableViewCell 的回答指出了我的方向。
如果角覆盖纯色,我没有问题 - 很简单,可以使切角与该颜色匹配。但是如果这个角覆盖了一张图片,你会怎么让图片透出来呢?
我已经搜索过任何与此问题类似的问题,但大多数答案都与表格中的单元格有关,而我在这里所做的只是在屏幕视图上放置一个标签。
这是我使用的代码:
-(void)returnChoppedCorners:(UIView *)viewObject
{
NSLog(@"Object Width = %f", viewObject.layer.frame.size.width);
NSLog(@"Object Height = %f", viewObject.layer.frame.size.height);
CALayer* bannerLeftTop = [CALayer layer];
bannerLeftTop.backgroundColor = [UIColor blackColor].CGColor;
// or whatever color the background is
bannerLeftTop.bounds = CGRectMake(0, 0, 25, 25);
bannerLeftTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerLeftTop.position = CGPointMake(10, 10);
bannerLeftTop.affineTransform = CGAffineTransformMakeRotation(-45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerLeftTop];
CALayer* bannerRightTop = [CALayer layer];
bannerRightTop.backgroundColor = [UIColor blackColor].CGColor;
bannerRightTop.bounds = CGRectMake(0, 0, 25, 25);
bannerRightTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerRightTop.position = CGPointMake(viewObject.layer.frame.size.width - 10.0, 10.0);
bannerRightTop.affineTransform = CGAffineTransformMakeRotation(45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerRightTop];
}
我将添加类似的代码来处理 BottomLeft 和 BottomRight 角,但是现在,这些角是覆盖图像的角。 bannerLeftTop 和 bannerRightTop 实际上是在黑色背景上旋转过角的正方形。让它们变得清晰只会让底层的 UILabel 背景颜色出现,而不是图像。与使用 z 属性 相同。掩盖答案吗?哦,我应该改用底层图像吗?
我还遇到了传递给此方法的高度和宽度的问题 -- 它们与对象的受限高度和宽度不匹配。但我们会把它留到另一个问题。
您需要做的不是在标签上绘制不透明的角三角形,而是遮盖标签,这样它的角就不会绘制到屏幕上。
因为iOS 8.0,UIView
有一个maskView
属性,所以我们实际上不需要降到Core Animation级别来做这个。我们可以绘制一个图像用作蒙版,并剪掉适当的角。然后我们将创建一个图像视图来保存蒙版图像,并将其设置为标签的 maskView
(或其他)。
唯一的问题是(在我的测试中)UIKit 不会自动调整遮罩视图的大小,无论是使用约束还是自动调整大小。如果调整遮罩视图的大小,我们必须“手动”更新遮罩视图的框架。
我知道你的问题被标记为 objective-c,但为了方便起见,我在 Swift 游乐场制定了我的答案。将其翻译成 Objective-C 应该不难。我没有做任何特别的事情“Swifty”。
所以...这是一个函数,它接受一个角数组(指定为 UIViewContentMode
个案例,因为该枚举包括角的案例)、一个视图和一个“深度”,这就是每个角三角形应沿其正方形边测量许多点:
func maskCorners(corners: [UIViewContentMode], ofView view: UIView, toDepth depth: CGFloat) {
在 Objective-C 中,对于 corners
参数,您可以使用位掩码(例如 (1 << UIViewContentModeTopLeft) | (1 << UIViewContentModeBottomRight)
),或者您可以使用 [=34= 的 NSArray
]s(例如 @[ @(UIViewContentModeTopLeft), @(UIViewContentModeBottomRight) ]
)。
无论如何,我要创建一个正方形的 9 切片可调整大小的图像。图像将需要在中间有一个点用于拉伸,并且由于每个角可能需要被裁剪,因此角需要 depth
乘 depth
点。因此图像的边长为 1 + 2 * depth
点:
let s = 1 + 2 * depth
现在我要创建一条勾勒出蒙版轮廓的路径,边角被剪掉。
let path = UIBezierPath()
因此,如果左上角被剪裁,我需要路径避开正方形的左上点(位于 0, 0)。否则,路径包括正方形的左上角。
if corners.contains(.TopLeft) {
path.moveToPoint(CGPoint(x: 0, y: 0 + depth))
path.addLineToPoint(CGPoint(x: 0 + depth, y: 0))
} else {
path.moveToPoint(CGPoint(x: 0, y: 0))
}
依次对每个角做同样的事情,绕着广场走:
if corners.contains(.TopRight) {
path.addLineToPoint(CGPoint(x: s - depth, y: 0))
path.addLineToPoint(CGPoint(x: s, y: 0 + depth))
} else {
path.addLineToPoint(CGPoint(x: s, y: 0))
}
if corners.contains(.BottomRight) {
path.addLineToPoint(CGPoint(x: s, y: s - depth))
path.addLineToPoint(CGPoint(x: s - depth, y: s))
} else {
path.addLineToPoint(CGPoint(x: s, y: s))
}
if corners.contains(.BottomLeft) {
path.addLineToPoint(CGPoint(x: 0 + depth, y: s))
path.addLineToPoint(CGPoint(x: 0, y: s - depth))
} else {
path.addLineToPoint(CGPoint(x: 0, y: s))
}
最后,关闭路径以便我填充它:
path.closePath()
现在我需要创建遮罩图像。我将使用仅 alpha 位图来执行此操作:
let colorSpace = CGColorSpaceCreateDeviceGray()
let scale = UIScreen.mainScreen().scale
let gc = CGBitmapContextCreate(nil, Int(s * scale), Int(s * scale), 8, 0, colorSpace, CGImageAlphaInfo.Only.rawValue)!
我需要调整上下文的坐标系以匹配 UIKit:
CGContextScaleCTM(gc, scale, -scale)
CGContextTranslateCTM(gc, 0, -s)
现在我可以在上下文中填写路径了。在这里使用白色是任意的;任何 alpha 为 1.0 的颜色都可以:
CGContextSetFillColorWithColor(gc, UIColor.whiteColor().CGColor)
CGContextAddPath(gc, path.CGPath)
CGContextFillPath(gc)
接下来我从位图中创建一个 UIImage
:
let image = UIImage(CGImage: CGBitmapContextCreateImage(gc)!, scale: scale, orientation: .Up)
如果这是在 Objective-C 中,您此时想释放位图上下文,使用 CGContextRelease(gc)
,但 Swift 会帮我处理它。
无论如何,我将不可调整大小的图像转换为 9 切片可调整大小的图像:
let maskImage = image.resizableImageWithCapInsets(UIEdgeInsets(top: depth, left: depth, bottom: depth, right: depth))
最后,我设置了遮罩视图。我可能已经有了蒙版视图,因为您可能已经用不同的设置裁剪了视图,所以如果它是图像视图,我将重复使用现有的蒙版视图:
let maskView = view.maskView as? UIImageView ?? UIImageView()
maskView.image = maskImage
最后,如果我必须创建蒙版视图,我需要将其设置为 view.maskView
并设置其框架:
if view.maskView != maskView {
view.maskView = maskView
maskView.frame = view.bounds
}
}
好的,我该如何使用这个功能?为了演示,我将制作一个紫色背景视图,并在其上放一张图片:
let view = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
view.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
let backgroundView = UIView(frame: view.frame)
backgroundView.backgroundColor = UIColor.purpleColor()
backgroundView.addSubview(view)
XCPlaygroundPage.currentPage.liveView = backgroundView
然后我将遮盖图像视图的一些角。大概你会这样做,比如说,viewDidLoad
:
maskCorners([.TopLeft, .BottomRight], ofView: view, toDepth: 50)
结果如下:
您可以看到通过剪角显示的紫色背景。
如果我要调整视图大小,我需要更新蒙版视图的框架。例如,我可能会在我的视图控制器中这样做:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.cornerClippedView.maskView?.frame = self.cornerClippedView.bounds
}
Here's a gist of all the code,因此您可以将其复制并粘贴到 playground 中进行试用。您必须提供自己的可爱测试图像。
更新
以下代码用于创建白色背景的标签,并将其叠加(每边插入 20 点)在背景图像上:
let backgroundView = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
let label = UILabel(frame: backgroundView.bounds.insetBy(dx: 20, dy: 20))
label.backgroundColor = UIColor.whiteColor()
label.font = UIFont.systemFontOfSize(50)
label.text = "This is the label"
label.lineBreakMode = .ByWordWrapping
label.numberOfLines = 0
label.textAlignment = .Center
label.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
backgroundView.addSubview(label)
XCPlaygroundPage.currentPage.liveView = backgroundView
maskCorners([.TopLeft, .BottomRight], ofView: label, toDepth: 50)
结果:
我在编写用于切断 UILabel(或者,实际上,您可以添加子层的任何 UIView 派生对象)的角的代码时遇到了问题 -- 我必须感谢 Kurt Revis 对 Use a CALayer to add a diagonal banner/badge to the corner of a UITableViewCell 的回答指出了我的方向。
如果角覆盖纯色,我没有问题 - 很简单,可以使切角与该颜色匹配。但是如果这个角覆盖了一张图片,你会怎么让图片透出来呢?
我已经搜索过任何与此问题类似的问题,但大多数答案都与表格中的单元格有关,而我在这里所做的只是在屏幕视图上放置一个标签。
这是我使用的代码:
-(void)returnChoppedCorners:(UIView *)viewObject
{
NSLog(@"Object Width = %f", viewObject.layer.frame.size.width);
NSLog(@"Object Height = %f", viewObject.layer.frame.size.height);
CALayer* bannerLeftTop = [CALayer layer];
bannerLeftTop.backgroundColor = [UIColor blackColor].CGColor;
// or whatever color the background is
bannerLeftTop.bounds = CGRectMake(0, 0, 25, 25);
bannerLeftTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerLeftTop.position = CGPointMake(10, 10);
bannerLeftTop.affineTransform = CGAffineTransformMakeRotation(-45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerLeftTop];
CALayer* bannerRightTop = [CALayer layer];
bannerRightTop.backgroundColor = [UIColor blackColor].CGColor;
bannerRightTop.bounds = CGRectMake(0, 0, 25, 25);
bannerRightTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerRightTop.position = CGPointMake(viewObject.layer.frame.size.width - 10.0, 10.0);
bannerRightTop.affineTransform = CGAffineTransformMakeRotation(45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerRightTop];
}
我将添加类似的代码来处理 BottomLeft 和 BottomRight 角,但是现在,这些角是覆盖图像的角。 bannerLeftTop 和 bannerRightTop 实际上是在黑色背景上旋转过角的正方形。让它们变得清晰只会让底层的 UILabel 背景颜色出现,而不是图像。与使用 z 属性 相同。掩盖答案吗?哦,我应该改用底层图像吗?
我还遇到了传递给此方法的高度和宽度的问题 -- 它们与对象的受限高度和宽度不匹配。但我们会把它留到另一个问题。
您需要做的不是在标签上绘制不透明的角三角形,而是遮盖标签,这样它的角就不会绘制到屏幕上。
因为iOS 8.0,UIView
有一个maskView
属性,所以我们实际上不需要降到Core Animation级别来做这个。我们可以绘制一个图像用作蒙版,并剪掉适当的角。然后我们将创建一个图像视图来保存蒙版图像,并将其设置为标签的 maskView
(或其他)。
唯一的问题是(在我的测试中)UIKit 不会自动调整遮罩视图的大小,无论是使用约束还是自动调整大小。如果调整遮罩视图的大小,我们必须“手动”更新遮罩视图的框架。
我知道你的问题被标记为 objective-c,但为了方便起见,我在 Swift 游乐场制定了我的答案。将其翻译成 Objective-C 应该不难。我没有做任何特别的事情“Swifty”。
所以...这是一个函数,它接受一个角数组(指定为 UIViewContentMode
个案例,因为该枚举包括角的案例)、一个视图和一个“深度”,这就是每个角三角形应沿其正方形边测量许多点:
func maskCorners(corners: [UIViewContentMode], ofView view: UIView, toDepth depth: CGFloat) {
在 Objective-C 中,对于 corners
参数,您可以使用位掩码(例如 (1 << UIViewContentModeTopLeft) | (1 << UIViewContentModeBottomRight)
),或者您可以使用 [=34= 的 NSArray
]s(例如 @[ @(UIViewContentModeTopLeft), @(UIViewContentModeBottomRight) ]
)。
无论如何,我要创建一个正方形的 9 切片可调整大小的图像。图像将需要在中间有一个点用于拉伸,并且由于每个角可能需要被裁剪,因此角需要 depth
乘 depth
点。因此图像的边长为 1 + 2 * depth
点:
let s = 1 + 2 * depth
现在我要创建一条勾勒出蒙版轮廓的路径,边角被剪掉。
let path = UIBezierPath()
因此,如果左上角被剪裁,我需要路径避开正方形的左上点(位于 0, 0)。否则,路径包括正方形的左上角。
if corners.contains(.TopLeft) {
path.moveToPoint(CGPoint(x: 0, y: 0 + depth))
path.addLineToPoint(CGPoint(x: 0 + depth, y: 0))
} else {
path.moveToPoint(CGPoint(x: 0, y: 0))
}
依次对每个角做同样的事情,绕着广场走:
if corners.contains(.TopRight) {
path.addLineToPoint(CGPoint(x: s - depth, y: 0))
path.addLineToPoint(CGPoint(x: s, y: 0 + depth))
} else {
path.addLineToPoint(CGPoint(x: s, y: 0))
}
if corners.contains(.BottomRight) {
path.addLineToPoint(CGPoint(x: s, y: s - depth))
path.addLineToPoint(CGPoint(x: s - depth, y: s))
} else {
path.addLineToPoint(CGPoint(x: s, y: s))
}
if corners.contains(.BottomLeft) {
path.addLineToPoint(CGPoint(x: 0 + depth, y: s))
path.addLineToPoint(CGPoint(x: 0, y: s - depth))
} else {
path.addLineToPoint(CGPoint(x: 0, y: s))
}
最后,关闭路径以便我填充它:
path.closePath()
现在我需要创建遮罩图像。我将使用仅 alpha 位图来执行此操作:
let colorSpace = CGColorSpaceCreateDeviceGray()
let scale = UIScreen.mainScreen().scale
let gc = CGBitmapContextCreate(nil, Int(s * scale), Int(s * scale), 8, 0, colorSpace, CGImageAlphaInfo.Only.rawValue)!
我需要调整上下文的坐标系以匹配 UIKit:
CGContextScaleCTM(gc, scale, -scale)
CGContextTranslateCTM(gc, 0, -s)
现在我可以在上下文中填写路径了。在这里使用白色是任意的;任何 alpha 为 1.0 的颜色都可以:
CGContextSetFillColorWithColor(gc, UIColor.whiteColor().CGColor)
CGContextAddPath(gc, path.CGPath)
CGContextFillPath(gc)
接下来我从位图中创建一个 UIImage
:
let image = UIImage(CGImage: CGBitmapContextCreateImage(gc)!, scale: scale, orientation: .Up)
如果这是在 Objective-C 中,您此时想释放位图上下文,使用 CGContextRelease(gc)
,但 Swift 会帮我处理它。
无论如何,我将不可调整大小的图像转换为 9 切片可调整大小的图像:
let maskImage = image.resizableImageWithCapInsets(UIEdgeInsets(top: depth, left: depth, bottom: depth, right: depth))
最后,我设置了遮罩视图。我可能已经有了蒙版视图,因为您可能已经用不同的设置裁剪了视图,所以如果它是图像视图,我将重复使用现有的蒙版视图:
let maskView = view.maskView as? UIImageView ?? UIImageView()
maskView.image = maskImage
最后,如果我必须创建蒙版视图,我需要将其设置为 view.maskView
并设置其框架:
if view.maskView != maskView {
view.maskView = maskView
maskView.frame = view.bounds
}
}
好的,我该如何使用这个功能?为了演示,我将制作一个紫色背景视图,并在其上放一张图片:
let view = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
view.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
let backgroundView = UIView(frame: view.frame)
backgroundView.backgroundColor = UIColor.purpleColor()
backgroundView.addSubview(view)
XCPlaygroundPage.currentPage.liveView = backgroundView
然后我将遮盖图像视图的一些角。大概你会这样做,比如说,viewDidLoad
:
maskCorners([.TopLeft, .BottomRight], ofView: view, toDepth: 50)
结果如下:
您可以看到通过剪角显示的紫色背景。
如果我要调整视图大小,我需要更新蒙版视图的框架。例如,我可能会在我的视图控制器中这样做:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.cornerClippedView.maskView?.frame = self.cornerClippedView.bounds
}
Here's a gist of all the code,因此您可以将其复制并粘贴到 playground 中进行试用。您必须提供自己的可爱测试图像。
更新
以下代码用于创建白色背景的标签,并将其叠加(每边插入 20 点)在背景图像上:
let backgroundView = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
let label = UILabel(frame: backgroundView.bounds.insetBy(dx: 20, dy: 20))
label.backgroundColor = UIColor.whiteColor()
label.font = UIFont.systemFontOfSize(50)
label.text = "This is the label"
label.lineBreakMode = .ByWordWrapping
label.numberOfLines = 0
label.textAlignment = .Center
label.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
backgroundView.addSubview(label)
XCPlaygroundPage.currentPage.liveView = backgroundView
maskCorners([.TopLeft, .BottomRight], ofView: label, toDepth: 50)
结果: