iOS绘制半圆UIView:详解CAShapeLayer与draw方法
2025-05-02 18:32:49
画个半圆 UIView?其实没那么难
遇到个问题,想整个半圆形的 UIView
,还得是蓝色填充的。试了用 clipsToBounds
配合父视图来裁剪一个圆形 View,结果不灵。这事儿咋整呢?
别急,这问题挺常见的。直接用 clipsToBounds
在目标 View 上通常搞不定这事,咱们得换个思路。
为啥 clipsToBounds
不好使?
简单说,clipsToBounds
这属性,是决定一个视图 超出它自己边界的子视图 是否要被裁剪掉。它并不能改变视图本身的形状。
比如你有个正方形的 View A,里面放了个比它大的圆形 View B。
- 如果 A 的
clipsToBounds
是false
(默认值),那 B 超出 A 边界的部分 会显示 出来。 - 如果 A 的
clipsToBounds
是true
,那 B 超出 A 边界的部分 就被裁剪掉 ,看不见了。
你看,clipsToBounds
控制的是“孩子”别跑出“家”的范围,而不是把“家”(视图本身)从方的变成圆的或半圆的。你想改变视图本身呈现的样子,得从绘制层面入手。
可行的路子
想画出特定形状的 View,主流的方法有两种:
- 利用
CAShapeLayer
和UIBezierPath
: 这是比较推荐的方式,性能好,灵活性高。 - 重写
draw(_:)
方法 : 使用 Core Graphics 自行绘制,自由度最大,但需要注意性能。
下面咱们挨个儿细说。
方法一:CAShapeLayer
+ UIBezierPath
这是目前在 iOS 开发中绘制自定义形状视图的首选方案。
原理和作用
CAShapeLayer
是CALayer
的一个子类,专门用来高效绘制基于矢量路径(CGPath
)的形状。它使用 GPU 进行渲染,性能通常比 CPU 绘制要好,特别是在形状复杂或需要动画时。UIBezierPath
是 UIKit 提供的方便创建和管理矢量路径的类。我们可以用它来定义各种线条、曲线,当然也包括我们需要的半圆弧线。
思路就是:用 UIBezierPath
画出半圆的路径,然后创建一个 CAShapeLayer
,把这个路径交给它 (shapeLayer.path = path.cgPath
),再设置好填充颜色 (fillColor
),最后把这个 shapeLayer
作为我们 UIView
的 mask
(遮罩) 或者直接添加为子图层来显示。对于一个纯色的半圆 View,直接添加为子图层或者设置为主 Layer 更常见。
操作步骤与代码示例 (Swift)
-
创建一个
UIView
的子类 ,比如叫HalfCircleView
。 -
在
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 // 避免默认背景色干扰
}
}
-
使用
HalfCircleView
:let myHalfCircle = HalfCircleView(frame: CGRect(x: 50, y: 100, width: 200, height: 100)) myHalfCircle.fillColor = .systemBlue // 设置成你想要的蓝色 view.addSubview(myHalfCircle)
(此处应有一张效果图占位符,实际博客需替换为真实截图)
说明:
- 上面代码绘制的是一个 上半圆 ,圆心在视图底部边界的中点。
path.close()
很关键,它会自动从弧线的结束点画一条直线回到弧线的起始点,这样才构成一个封闭的半圆形区域,才能正确填充颜色。不加close()
的话,fillColor
可能只会填充一个扇形区域,或者行为不确定。- 半径
radius
的计算考虑了视图宽高比,防止半圆绘制不全。 - 角度用的是 弧度 (Radians) 。
CGFloat.pi
代表 180 度,0
代表 0 度。 UIKit 的坐标系 Y 轴向下,0 度在右边,pi
在左边,pi/2
在下边,3*pi/2
在上边。所以从pi
顺时针true
到0
正好是上半圆。 - 如果想画 下半圆 ,可以调整
arcCenter
(比如到bounds.minY
),或者调整startAngle
和endAngle
(比如从0
顺时针到pi
)。画 左半圆 或 右半圆 同理,调整圆心和角度即可。
安全建议
- 这个方法本身没有特别的安全风险。性能方面,
CAShapeLayer
已经是比较优化的方案了。
进阶使用技巧
- 动画:
CAShapeLayer
的path
,fillColor
,strokeColor
,lineWidth
等属性都是支持隐式动画或显式动画的 (CABasicAnimation
,CAKeyframeAnimation
)。你可以让半圆变形、变色。 - 边框: 给
shapeLayer
设置strokeColor
和lineWidth
就可以画出半圆的边框。 - 渐变填充: 可以用
CAGradientLayer
作为CAShapeLayer
的mask
,或者反过来,用CAShapeLayer
作为CAGradientLayer
的mask
,来实现渐变色的半圆。 - 精确点击判断: 默认情况下,视图的点击区域是其矩形
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)
-
创建
UIView
子类 ,比如HalfCircleDrawView
。 -
重写
draw(_:)
方法 。 -
重要: 通常需要设置
backgroundColor
为.clear
并且isOpaque
为false
,否则视图会有一个默认的矩形背景,遮挡住你的半圆效果,或者导致透明区域渲染不正确。
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() // 绘制边框
}
}
- 使用
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(_:)
的绘制结果缓存到CALayer
的contents
属性中(生成UIImage
或CGImage
),这样就不需要每次都重新执行绘制代码了。但这会增加内存开销。 - 混合绘制: 你可以在
draw(_:)
中绘制一部分内容,同时 View 还拥有子视图或子 Layer,它们会按层级叠加显示。
选哪个?
- 对于画简单形状 (如半圆、圆角矩形)、需要动画 或者对性能 比较敏感的场景,优先推荐使用
CAShapeLayer
。它是苹果设计的专门用于此目的的类,更现代,性能更好。 - 如果你需要进行非常复杂 、像素级 的自定义绘制,或者要集成一些老的基于 Core Graphics 的绘图代码,那么重写
draw(_:)
提供了最大的灵活性。
对于“画个蓝色填充的半圆 UIView”这个需求,CAShapeLayer
的方法显得更简洁且高效。希望这两种方法能帮你解决问题!