返回

iOS绘制半圆UIView:详解CAShapeLayer与draw方法

IOS

画个半圆 UIView?其实没那么难

遇到个问题,想整个半圆形的 UIView,还得是蓝色填充的。试了用 clipsToBounds 配合父视图来裁剪一个圆形 View,结果不灵。这事儿咋整呢?

别急,这问题挺常见的。直接用 clipsToBounds 在目标 View 上通常搞不定这事,咱们得换个思路。

为啥 clipsToBounds 不好使?

简单说,clipsToBounds 这属性,是决定一个视图 超出它自己边界的子视图 是否要被裁剪掉。它并不能改变视图本身的形状。

比如你有个正方形的 View A,里面放了个比它大的圆形 View B。

  • 如果 A 的 clipsToBoundsfalse (默认值),那 B 超出 A 边界的部分 会显示 出来。
  • 如果 A 的 clipsToBoundstrue,那 B 超出 A 边界的部分 就被裁剪掉 ,看不见了。

你看,clipsToBounds 控制的是“孩子”别跑出“家”的范围,而不是把“家”(视图本身)从方的变成圆的或半圆的。你想改变视图本身呈现的样子,得从绘制层面入手。

可行的路子

想画出特定形状的 View,主流的方法有两种:

  1. 利用 CAShapeLayerUIBezierPath : 这是比较推荐的方式,性能好,灵活性高。
  2. 重写 draw(_:) 方法 : 使用 Core Graphics 自行绘制,自由度最大,但需要注意性能。

下面咱们挨个儿细说。

方法一:CAShapeLayer + UIBezierPath

这是目前在 iOS 开发中绘制自定义形状视图的首选方案。

原理和作用

  • CAShapeLayerCALayer 的一个子类,专门用来高效绘制基于矢量路径(CGPath)的形状。它使用 GPU 进行渲染,性能通常比 CPU 绘制要好,特别是在形状复杂或需要动画时。
  • UIBezierPath 是 UIKit 提供的方便创建和管理矢量路径的类。我们可以用它来定义各种线条、曲线,当然也包括我们需要的半圆弧线。

思路就是:用 UIBezierPath 画出半圆的路径,然后创建一个 CAShapeLayer,把这个路径交给它 (shapeLayer.path = path.cgPath),再设置好填充颜色 (fillColor),最后把这个 shapeLayer 作为我们 UIViewmask (遮罩) 或者直接添加为子图层来显示。对于一个纯色的半圆 View,直接添加为子图层或者设置为主 Layer 更常见。

操作步骤与代码示例 (Swift)

  1. 创建一个 UIView 的子类 ,比如叫 HalfCircleView

  2. layoutSubviews 方法中创建和更新 CAShapeLayer 。为啥是 layoutSubviews?因为视图的尺寸在这个时候才最终确定,我们画半圆需要依赖视图的最终宽高。

import UIKit

class HalfCircleView: UIView {

    // 可以外部设置填充颜色
    var fillColor: UIColor = .blue {
        didSet {
            shapeLayer.fillColor = fillColor.cgColor
        }
    }

    // 持有 shapeLayer 方便更新
    private let shapeLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayer()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayer()
    }

    private func setupLayer() {
        // 将 shapeLayer 添加到视图的 layer 层级
        layer.addSublayer(shapeLayer)
        // 设置初始填充色
        shapeLayer.fillColor = fillColor.cgColor
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // 视图尺寸变化时,更新 shapeLayer 的路径和位置
        updatePath()
    }

    private func updatePath() {
        let arcCenter = CGPoint(x: bounds.midX, y: bounds.maxY) // 圆心定在底部中点
        let radius = min(bounds.width / 2, bounds.height)      // 半径取宽度一半和高度的较小值,确保能放下
        let startAngle = CGFloat.pi                           // 起始角度:左侧 (180度)
        let endAngle = 0                                      // 结束角度:右侧 (0度)
        let clockwise = true                                  // 顺时针绘制

        // 创建贝塞尔路径
        let path = UIBezierPath()
        // 画上半圆弧线
        path.addArc(withCenter: arcCenter,
                    radius: radius,
                    startAngle: startAngle,
                    endAngle: endAngle,
                    clockwise: clockwise)
        // 封闭路径,形成一个"D"形(从弧线终点画回起点)
        path.close()

        // 更新 shapeLayer 的路径
        shapeLayer.path = path.cgPath

        // 如果你的视图背景色不是透明的,需要设置一下
        // self.backgroundColor = .clear // 避免默认背景色干扰
    }
}
  1. 使用 HalfCircleView :

    let myHalfCircle = HalfCircleView(frame: CGRect(x: 50, y: 100, width: 200, height: 100))
    myHalfCircle.fillColor = .systemBlue // 设置成你想要的蓝色
    view.addSubview(myHalfCircle)
    

    Half Circle Example (此处应有一张效果图占位符,实际博客需替换为真实截图)

说明:

  • 上面代码绘制的是一个 上半圆 ,圆心在视图底部边界的中点。
  • path.close() 很关键,它会自动从弧线的结束点画一条直线回到弧线的起始点,这样才构成一个封闭的半圆形区域,才能正确填充颜色。不加 close() 的话,fillColor 可能只会填充一个扇形区域,或者行为不确定。
  • 半径 radius 的计算考虑了视图宽高比,防止半圆绘制不全。
  • 角度用的是 弧度 (Radians)CGFloat.pi 代表 180 度,0 代表 0 度。 UIKit 的坐标系 Y 轴向下,0 度在右边,pi 在左边,pi/2 在下边,3*pi/2 在上边。所以从 pi 顺时针 true0 正好是上半圆。
  • 如果想画 下半圆 ,可以调整 arcCenter (比如到 bounds.minY),或者调整 startAngleendAngle (比如从 0 顺时针到 pi)。画 左半圆右半圆 同理,调整圆心和角度即可。

安全建议

  • 这个方法本身没有特别的安全风险。性能方面,CAShapeLayer 已经是比较优化的方案了。

进阶使用技巧

  • 动画: CAShapeLayerpath, fillColor, strokeColor, lineWidth 等属性都是支持隐式动画或显式动画的 (CABasicAnimation, CAKeyframeAnimation)。你可以让半圆变形、变色。
  • 边框:shapeLayer 设置 strokeColorlineWidth 就可以画出半圆的边框。
  • 渐变填充: 可以用 CAGradientLayer 作为 CAShapeLayermask,或者反过来,用 CAShapeLayer 作为 CAGradientLayermask,来实现渐变色的半圆。
  • 精确点击判断: 默认情况下,视图的点击区域是其矩形 bounds。如果你希望只有半圆区域内响应点击,需要重写 point(inside:with:) 方法,并使用 shapeLayer.path.contains(point) 来判断。
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    // 检查点是否在 shapeLayer 的路径定义的区域内
    return shapeLayer.path?.contains(point) ?? false
}

方法二:重写 draw(_:) 方法

这是更“底层”一点的方式,直接在视图的绘制周期里用 Core Graphics(通过 UIKit 封装的 UIBezierPath 很方便)来画图。

原理和作用

  • 每个 UIView 都有一个 draw(_ rect: CGRect) 方法(早期是 drawRect:)。当视图需要被绘制或重绘时(比如第一次显示,或者调用了 setNeedsDisplay()),系统会调用这个方法。
  • 你可以在这个方法里获取当前的图形上下文 (CGContext),然后使用 Core Graphics 的 C API 或者 UIBezierPath 的方法来进行绘制操作,比如画线、画圆、填充颜色等。

操作步骤与代码示例 (Swift)

  1. 创建 UIView 子类 ,比如 HalfCircleDrawView

  2. 重写 draw(_:) 方法

  3. 重要: 通常需要设置 backgroundColor.clear 并且 isOpaquefalse,否则视图会有一个默认的矩形背景,遮挡住你的半圆效果,或者导致透明区域渲染不正确。

import UIKit

class HalfCircleDrawView: UIView {

    var fillColor: UIColor = .blue {
        didSet {
            // 当颜色改变时,需要告诉系统重绘视图
            setNeedsDisplay()
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }

    private func setupView() {
        // 必须设置!否则会有默认背景或透明度问题
        self.isOpaque = false
        self.backgroundColor = .clear
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect) // 虽然通常为空,但调用一下是好习惯

        // 使用 UIBezierPath 定义半圆路径 (逻辑同 CAShapeLayer)
        let arcCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
        let radius = min(bounds.width / 2, bounds.height)
        let startAngle = CGFloat.pi
        let endAngle = 0
        let clockwise = true

        let path = UIBezierPath()
        path.addArc(withCenter: arcCenter,
                    radius: radius,
                    startAngle: startAngle,
                    endAngle: endAngle,
                    clockwise: clockwise)
        path.close() // 封闭路径形成半圆

        // 设置填充色
        fillColor.setFill()

        // 执行填充
        path.fill()

        // 如果需要边框,可以这样设置:
        // UIColor.black.setStroke() // 设置边框颜色
        // path.lineWidth = 2         // 设置线宽
        // path.stroke()              // 绘制边框
    }
}
  1. 使用 HalfCircleDrawView :
    let myHalfCircleDraw = HalfCircleDrawView(frame: CGRect(x: 50, y: 250, width: 200, height: 100))
    myHalfCircleDraw.fillColor = UIColor(red: 0.2, green: 0.6, blue: 0.9, alpha: 1.0) // 换个蓝色
    view.addSubview(myHalfCircleDraw)
    

说明:

  • 路径创建的逻辑和 CAShapeLayer 方法里是一样的。
  • draw(_:) 里,我们用 fillColor.setFill() 来设置接下来要用的填充色,然后 path.fill() 执行填充操作。画边框是类似的 *.setStroke()path.stroke()
  • 每次需要更新视图外观(比如颜色变了、尺寸变了需要重画路径),必须手动调用 setNeedsDisplay() 来触发系统在下一个绘制周期调用 draw(_:)。相比之下 CAShapeLayer 在其属性变化时(如 fillColor, path)通常会自动重绘。

安全建议

  • 无特殊安全问题。主要是性能考量。

进阶使用技巧

  • 性能优化: draw(_:) 是 CPU 密集型操作。对于复杂的绘图或者需要频繁重绘的场景,性能可能不如 CAShapeLayer。避免在 draw(_:) 里执行耗时操作。可以优化传入 draw(_:)rect 参数,只重绘变化的部分(需要更复杂的逻辑判断)。
  • 绘图缓存: 对于不经常变化的内容,可以将 draw(_:) 的绘制结果缓存到 CALayercontents 属性中(生成 UIImageCGImage),这样就不需要每次都重新执行绘制代码了。但这会增加内存开销。
  • 混合绘制: 你可以在 draw(_:) 中绘制一部分内容,同时 View 还拥有子视图或子 Layer,它们会按层级叠加显示。

选哪个?

  • 对于画简单形状 (如半圆、圆角矩形)、需要动画 或者对性能 比较敏感的场景,优先推荐使用 CAShapeLayer 。它是苹果设计的专门用于此目的的类,更现代,性能更好。
  • 如果你需要进行非常复杂像素级 的自定义绘制,或者要集成一些老的基于 Core Graphics 的绘图代码,那么重写 draw(_:) 提供了最大的灵活性。

对于“画个蓝色填充的半圆 UIView”这个需求,CAShapeLayer 的方法显得更简洁且高效。希望这两种方法能帮你解决问题!