自定义视图是任何移动应用独特界面的基础。移动应用中的自定义视图允许您创建标准组件中不可用的自有图形、动画和元素。根据Apple Developer,2025,drawRect仍然是iOS移动开发中自定义绘制的主要方法。在Android上,onDraw与Canvas扮演着相同角色,而在Flutter中——CustomPainter。
要点
自定义视图是用户定义的界面组件,它覆盖标准的绘制和测量方法以创建独特的图形。图表、动画、示意图、自定义聊天元素和AR面具 — 这些是移动应用中自定义视图的典型使用场景。标准UI组件(Button、TextView、Label)覆盖了80%的任务,但剩余20%需要在移动开发中完全控制绘制。移动开发中的绘制是一项区分初级开发者和高级开发者的技能。
移动应用中的自定义视图对于独特的2D图形、复杂动画、图表和数据可视化是必需的。如果标准UIKit不提供所需元素 — 则创建自定义视图。在移动应用中,自定义视图用于进度条、自定义按钮、渐变填充、动画图标和手指绘制画布。重要的是不要过度使用 — 对于简单样式,通过外观API配置标准组件就足够了。
在iOS上,自定义视图通过继承UIView并覆盖drawRect方法来创建。drawRect在首次渲染时以及每次setNeedsDisplay()调用后被调用。在drawRect内部,开发者通过UIGraphicsGetCurrentContext()获取CGContext — 用于线条、形状、渐变和文本的画布。Core Graphics(Quartz 2D)是一个低级别的基于CPU的渲染引擎,提供对每个像素的完全控制。UIBezierPath是一个方便的包装器,用于以简洁的语法创建路径。
class CircleView: UIView {
private var fillColor: UIColor = .systemBlue
override func drawRect(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.setFillColor(fillColor.cgColor)
ctx.addEllipse(in: bounds.insetBy(dx: 4, dy: 4))
ctx.drawPath(using: .fill)
}
func updateColor(_ newColor: UIColor) {
fillColor = newColor
setNeedsDisplay()
}
}
Core Graphics提供CGContext — 用于所有绘制操作的画布。CGGradient创建线性和径向渐变,支持CGColorSpace(sRGB、Display P3)。变换通过CGAffineTransform执行 — 用于旋转、缩放和平移的3x3矩阵。CGContextTranslateCTM、CGContextRotateCTM和CGContextScaleCTM修改坐标系。Core Graphics在CPU上运行,因此复杂的图形可能会加载处理器 — 使用Metal进行GPU加速。
UIBezierPath是CGPath上的面向对象包装器。addArc(withCenter:radius:startAngle:endAngle:clockwise:)方法创建圆弧和圆,addCurve创建贝塞尔曲线。路径通过addClip支持联合、交集和减集操作。UIBezierPath适用于自定义遮罩(maskView)、裁剪路径和复杂形状 — 星形、心形、自定义图标。
在Android上,自定义视图通过覆盖onDraw(Canvas)来扩展View。Canvas包含移动开发中的所有绘制方法:drawCircle、drawRect、drawLine、drawText、drawBitmap、drawPath和drawRoundRect。Paint配置参数 — 颜色、线条粗细、样式(FILL、STROKE、FILL_AND_STROKE)、抗锯齿(isAntiAlias)和文本大小。onDraw在每次invalidate()时被调用,不应包含重型操作 — 所有计算都被缓存。
class CustomCircleView(context: Context, attrs: AttributeSet?)
: View(context, attrs) {
private val paint = Paint().apply {
style = Paint.Style.FILL
color = Color.BLUE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 4f
canvas.drawCircle(cx, cy, radius, paint)
}
}
Android上的Path等同于UIBezierPath:addCircle、addRect、addOval、lineTo、cubicTo和quadTo。PathMeasure计算路径长度和任意点的坐标 — 用于沿着路径动画化对象移动。对于动画,使用ValueAnimator并在回调中调用invalidate()。动画器可以更改颜色、半径、位置 — 任何影响绘制的属性。Android上的自定义视图还支持StateListDrawable和animated-vector以通过XML进行动画。
Android上的渐变通过Shader设置 — LinearGradient、RadialGradient和SweepGradient。Shader通过paint.setShader(shader)分配给Paint。LinearGradient在两点之间生成线性渐变,带有可选的颜色位置。RadialGradient — 从中心到边缘的径向。SweepGradient — 围绕中心点的圆锥形。Shader.TileMode(CLAMP、REPEAT、MIRROR)定义超出区域边界的行为。
Flutter使用CustomPainter — 一个具有paint(Canvas canvas, Size size)和shouldRepaint方法的类。Flutter中的Canvas提供与Android相同的方法:drawCircle、drawRect、drawLine、drawPath、drawParagraph和drawImage。Flutter中的Paint配置颜色、粗细、样式和抗锯齿。Flutter不使用drawRect或onDraw — 所有绘制通过嵌入在CustomPaint小部件中的CustomPainter进行。
class CirclePainter extends CustomPainter {
final Color color;
CirclePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final radius = size.shortestSide / 2 - 4;
canvas.drawCircle(size.center(Offset.zero), radius, paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) =>
oldDelegate.color != color;
}
shouldRepaint确定是否需要重新绘制CustomPainter。仅在影响渲染的数据更改时返回true — 颜色、大小、图表数据。RepaintBoundary将重绘隔离到单独的图层中 — 用于很少更改的小部件。Flutter没有setNeedsDisplay或invalidate — 重绘通过setState或监听更改的ValueNotifier触发。
Flutter支持LinearGradient、RadialGradient和SweepGradient — 与Android中相同的类型。Gradient通过createShader(Rect bounds)为Paint创建Shader。变换通过canvas.rotate、canvas.scale、canvas.translate和canvas.skew执行。Flutter为Transform小部件使用4x4变换矩阵(Matrix4)。Flutter中的绘制动画使用AnimationController与setState结合来重新绘制CustomPainter。
自定义视图的正确测量是可预测界面行为的基础。在iOS上,视图的大小通过sizeThatFits和intrinsicContentSize确定。sizeThatFits返回给定约束下的最佳大小。intrinsicContentSize — 没有外部约束时内容的自然大小。在Android上,测量在onMeasure中通过MeasureSpec参数(UNSPECIFIED、EXACTLY、AT_MOST)实现。在Flutter中,大小传递给paint(Canvas, Size)并可以通过computeDryLayout或LayoutBuilder计算。
intrinsicContentSize返回CGSize — 基于其内容的视图的自然大小。setContentHuggingPriority和setContentCompressionResistancePriority控制拉伸和压缩时的行为。sizeThatFits由系统调用以在Auto Layout中计算大小。对于自定义视图,如果您的组件具有自然大小,请覆盖intrinsicContentSize — 例如,圆形应始终为100x100。
onMeasure接受widthMeasureSpec和heightMeasureSpec。MeasureSpec包含模式和大小:UNSPECIFIED(无约束,视图自行选择大小)、EXACTLY(来自父级的精确大小)、AT_MOST(最大大小)。setMeasuredDimension(width, height)保存测量结果。当数据更改时,调用requestLayout()重新测量。嵌套的ViewGroup需要覆盖onLayout以排列子元素。
| 平台 | 测量方法 | 更改时调用 | 返回 |
|---|---|---|---|
| iOS | sizeThatFits / intrinsicContentSize | setNeedsLayout / invalidateIntrinsicContentSize | CGSize |
| Android | onMeasure / setMeasuredDimension | requestLayout / invalidate | void(保存大小) |
| Flutter | computeDryLayout | setState / LayoutBuilder | Size |
computeDryLayout是一种RenderObject方法,用于在没有完整布局周期的情况下计算大小。LayoutBuilder.getSize — 在build方法中获取可用空间的替代方法。在Flutter中,画布大小作为Size传递给paint,不需要单独测量 — CustomPainter在传递的边界内绘制。但是,对于自定义RenderObject,您需要覆盖performLayout和computeDryLayout。
常见问题
drawRect是通过Core Graphics进行自定义绘制的UIView方法。它在视图首次渲染时以及每次setNeedsDisplay()调用后被调用。在drawRect内使用CGContext绘制线条、形状和文本。
创建一个扩展View的类,覆盖onDraw(Canvas canvas)并使用Canvas API:drawCircle、drawRect、drawPath。Paint配置颜色、粗细和样式。要更新,请调用invalidate()。
CustomPainter是Flutter中用于在Canvas上进行自定义绘制的类。实现paint(Canvas, Size)和shouldRepaint方法。它通过CustomPaint小部件放置。Flutter没有drawRect — 所有绘制通过CustomPainter进行。
在iOS上使用sizeThatFits和intrinsicContentSize。在Android上 — 使用MeasureSpec的onMeasure。在Flutter中,大小传递给paint(Canvas, Size)或通过computeDryLayout计算。每个平台使用自己的测量方法。
自定义视图对于无法使用标准UIKit或View实现的独特图形、动画、图表和元素是必需的。对于简单样式,配置标准组件就足够了 — 不要不必要地复杂化架构。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。