下文主要针对各种加圆角的情况进行实践,看看会不会出现离屏渲染的情况。

测试条件

  • 测试平台:iPhone 12,iOS 16.3.1,Xcode 14.2。
  • 测试方法:运行项目后,通过 Xcode 设置 “Debug -> View Debugging -> Rendering -> Color Off-screen Rendered” 来打开离屏渲染检测。
  • 主要测试点:
    • 是否开启 masksToBounds
    • 是否设置图片
    • 是否设置背景色”
    • 父视图是否具有透明度

结论

这里先把结论放到最前面,后面您可以跳过实践过程,直接切到解决方案一节。

✅ 代表不会触发离屏渲染;❌ 代表会触发离屏渲染。

UIView UIImageView UIButton
(只有文字)
UIButton
(图片/背景图)
直接设置
背景色
(非透明)
父视图具有透明度
alpha

下面重点说一下 UIImageViewUIButton 的测试过程:

UIImageView

iOS 9 对 UIImageView 进行了一系列优化,相比较过去而言,触发离屏渲染的场景要小了不少

直接设置

lazy var testView = UIImageView().then {
    $0.image = UIImage(named: "Forms/share-cover")
    $0.layer.masksToBounds = true
    $0.layer.cornerRadius = 30
}

一个设置了图片的 UIImageView。示例中同时设置了 masksToBoundscornerRadius。从图片上可以看出来,并不会触发离屏渲染:

UIImageView 不设置 masksToBounds,只设置 cornerRadius 的话不会显示圆角。

添加背景色

那如果我们手贱再给它添加一个背景色呢?

lazy var testView = UIImageView().then {
    $0.image = UIImage(named: "Forms/share-cover")
    $0.layer.masksToBounds = true
    $0.layer.cornerRadius = 30
    $0.backgroundColor = .white // 添加一个白色的背景色
}

果然手贱是没有好处的,设置了背景色后则会触发离屏渲染。

UIButton

UIButton 默认也是没有背景色的,需要同时考虑文字和图片两种情况。

文字

如果不设置背景色的话,只有文字的 UIButton 对象是显示不出来圆角的,不过我也是分别测了一下。

lazy var testView = UIButton(type: .custom).then {
    $0.setTitle("这是一个标题", for: .normal)
    $0.layer.cornerRadius = 30
}

结论是:不论设置不设置背景色,只有文字的 UIButton 对象添加圆角后都不会触发离屏渲染

同时只需要设置 cornerRadius,不需要开启 masksToBounds 就可以显示出来圆角。

图片 & 背景图

因为现象一致,所以图片和背景图在这里归位一类进行讨论。

带图片的 UIButton 是重点测试对象,分下面几种情况:

直接设置

lazy var testView = UIButton(type: .custom).then {
    $0.setTitle("这是一个标题", for: .normal)
    $0.layer.masksToBounds = true
    $0.layer.cornerRadius = 30
    $0.setImage(UIImage(named: "Forms/share-cover"), for: .normal)
}

如果 UIButton 只设置 cornerRadius 而不开启 masksToBounds,那么图片是不会显示出圆角的。

从图上看,是会触发离屏渲染。

尝试解决离屏渲染问题

实践了哪些情况会触发离屏渲染,接下来就想办法尝试解决这些问题。

用父视图进行包裹

有的文章会提到用一个父视图包裹需要添加圆角的视图,然后将圆角添加到父视图上。

实践发现该方法并不能解决离屏渲染问题。

避免开启 masksToBounds

UIImageView 视图可以在不开启 masksToBounds,仅设置 cornerRadius 的情况下显示圆角,只包含文字的 UIButton 对象也一样。

所以在这种情况下,不开启 masksToBounds 也可以避免触发离屏渲染。

为图片添加圆角

直接为图片添加圆角是比较常用的避免离屏渲染的方法。只不过现在比较常用的 UIImageView 视图已经不会触发离屏渲染了,但是 UIButton 依然可以这么做。

下面提几点实际需求里可能会遇到的问题:

“拼接图片”

例如微信的群聊头像,可能涉及到多个图片在一个视图控件中进行展示。

这个时候最好将多张图片拼到一起,然后对这张图片整体设置圆角。而不是在一个 UIView 中尝试添加多个 UIImageVIew,再对 UIView 设置圆角。

视图尺寸不固定

一般 UIButton 会遇到 “尺寸不固定 + 需要圆角 + 背景色” 的情况。

例如页面某个位置有一个 “距离屏幕两侧 10px,可用状态背景色为蓝色,不可用状态下为灰蓝色” 的按钮。

此时可以考虑找 UI 切一个带圆角的纯色图片直接用作底色,但是我们开发也可以自己生成这样一张图片,然后通过拉伸来达到相同的效果。

首先我们先找喵神借用一个非常好用的枚举,来表示圆角:

extension UIImage {
    enum Radius {
        /// 圆角半径应该按照图片**宽度**的比例计算。
        /// 通常关联的值应该在0和0.5之间,其中0表示没有圆角,0.5表示使用图片宽度的一半作为圆角半径。
        case widthFraction(CGFloat)
        
        /// 圆角半径应该按照图片**高度**的比例计算。
        /// 通常关联的值应该在0和0.5之间,其中0表示没有圆角,0.5表示使用图片高度的一半作为圆角半径。
        case heightFraction(CGFloat)
        
        /// 使用一个固定的点值作为圆角半径。
        case point(CGFloat)
        
        func compute(with size: CGSize) -> CGFloat {
            let cornerRadius: CGFloat
            switch self {
            case .point(let point):
                cornerRadius = point
            case .widthFraction(let widthFraction):
                cornerRadius = size.width * widthFraction
            case .heightFraction(let heightFraction):
                cornerRadius = size.height * heightFraction
            }
            return cornerRadius
        }
    }
}

之后生成纯色图片:

extension UIImage {
    static func color(
        _ color: UIColor,
        size: CGSize = .init(width: 1, height: 1),
        radius: Radius? = nil
    ) -> UIImage {
        let cornerRadius = radius?.compute(with: size)
        
        let renderer = UIGraphicsImageRenderer(size: size)
        let image = renderer.image { _ in
            let rect = CGRect(origin: .zero, size: size)
            let path: UIBezierPath
            
            if let cornerRadius = cornerRadius {
                path = .init(roundedRect: rect, cornerRadius: cornerRadius)
            } else {
                path = .init(rect: rect)
            }
            
            color.setFill()
            path.fill()
        }
        
        guard let cornerRadius = cornerRadius else { return image }
        
        let insets = { UIEdgeInsets(top: $0, left: $0, bottom: $0, right: $0) }(cornerRadius)
        return image.resizableImage(withCapInsets: insets, resizingMode: .stretch)
    }
}

你还可以扩展这个方法,配置需要圆角的位置,而不是为四个边都添加圆角,比较简单,这里就不再赘述了。