📚 Flutter 自定义 View 权威指引
一、核心理念:理解 Flutter 的"绘制三棵树"
在 Flutter 中实现自定义 View,首先要理解其架构核心:
-
Widget - 配置描述
- 不可变,只描述UI应该长什么样
- 轻量级,频繁创建和销毁
-
Element - 生命周期管理
- 连接 Widget 和 RenderObject 的桥梁
- 管理更新和重建
-
RenderObject - 布局与绘制
- 重量级对象,负责实际测量、布局和绘制
- 持久存在,避免频繁重建
关键认知 :Flutter 中没有 Android 或 iOS 中传统的"View"概念,自定义绘制的核心是操作 RenderObject
或在 CustomPainter
中使用 Canvas
绘制。
二、两种主流的自定义绘制方案
方案一:使用 CustomPainter(推荐入门和简单场景)
这是最常用的 2D 自定义绘制方案,适合大多数UI定制需求。
dart
class MyCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill
..strokeWidth = 2.0;
// 绘制矩形
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
// 绘制圆形
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
min(size.width, size.height) / 4,
paint..color = Colors.red,
);
// 绘制路径
final path = Path()
..moveTo(0, size.height)
..lineTo(size.width / 2, 0)
..lineTo(size.width, size.height)
..close();
canvas.drawPath(path, paint..color = Colors.green);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// 重要:优化性能,只在必要时重绘
return true; // 实际情况中应根据具体条件判断
}
}
// 使用
CustomPaint(
painter: MyCustomPainter(),
size: Size(200, 200), // 指定尺寸
)
方案二:继承 RenderObject(高级复杂场景)
当需要完全控制布局和绘制逻辑时,直接使用 RenderObject。
dart
class MyCustomRenderBox extends RenderBox {
@override
void performLayout() {
// 1. 确定自身尺寸
size = constraints.constrain(Size(200, 200));
}
@override
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
// 绘制逻辑
final paint = Paint()..color = Colors.blue;
canvas.drawRect(Offset.zero & size, paint);
canvas.restore();
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
// 处理点击测试
if (size.contains(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
}
// 包装成 Widget
class MyCustomWidget extends LeafRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) {
return MyCustomRenderBox();
}
}
三、完整的自定义 View 开发流程
步骤1:需求分析与技术选型
- 简单静态图形 → CustomPainter
- 需要复杂布局逻辑 → RenderBox
- 需要处理复杂手势 → 结合 GestureDetector
步骤2:实现绘制逻辑
dart
class AdvancedCustomPainter extends CustomPainter {
final double progress;
final Color primaryColor;
AdvancedCustomPainter({
required this.progress,
required this.primaryColor,
});
@override
void paint(Canvas canvas, Size size) {
_drawBackground(canvas, size);
_drawProgress(canvas, size);
_drawText(canvas, size);
}
void _drawBackground(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey[300]!
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(10),
),
paint,
);
}
void _drawProgress(Canvas canvas, Size size) {
final gradient = LinearGradient(
colors: [primaryColor, primaryColor.withOpacity(0.7)],
);
final paint = Paint()
..shader = gradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height))
..style = PaintingStyle.fill;
final progressWidth = size.width * progress;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, progressWidth, size.height),
Radius.circular(10),
),
paint,
);
}
void _drawText(Canvas canvas, Size size) {
final textPainter = TextPainter(
text: TextSpan(
text: '${(progress * 100).toInt()}%',
style: TextStyle(color: Colors.white, fontSize: 14),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
(size.width - textPainter.width) / 2,
(size.height - textPainter.height) / 2,
),
);
}
@override
bool shouldRepaint(AdvancedCustomPainter oldDelegate) {
return progress != oldDelegate.progress ||
primaryColor != oldDelegate.primaryColor;
}
}
步骤3:添加交互支持
dart
class InteractiveCustomView extends StatefulWidget {
@override
_InteractiveCustomViewState createState() => _InteractiveCustomViewState();
}
class _InteractiveCustomViewState extends State<InteractiveCustomView> {
double _progress = 0.5;
Offset? _lastOffset;
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
_progress = (_progress + details.delta.dx / 300).clamp(0.0, 1.0);
});
},
onTapDown: (details) {
final box = context.findRenderObject() as RenderBox;
final localOffset = box.globalToLocal(details.globalPosition);
setState(() {
_progress = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
});
},
child: CustomPaint(
painter: AdvancedCustomPainter(
progress: _progress,
primaryColor: Colors.blue,
),
size: Size(300, 50),
),
);
}
}
四、性能优化深度指南
1. 重绘优化策略
dart
class OptimizedPainter extends CustomPainter {
final double value;
final List<Path> _cachedPaths = [];
@override
bool shouldRepaint(OptimizedPainter oldDelegate) {
// 精确控制重绘条件
return (value - oldDelegate.value).abs() > 0.01;
}
@override
bool shouldRebuildSemantics(OptimizedPainter oldDelegate) {
return false; // 语义化信息不需要重建时返回false
}
}
2. 复杂路径缓存
dart
class PathCachePainter extends CustomPainter {
static Path? _cachedComplexPath;
Path get _complexPath {
_cachedComplexPath ??= _createComplexPath();
return _cachedComplexPath!;
}
Path _createComplexPath() {
final path = Path();
// 复杂的路径创建逻辑
for (int i = 0; i < 100; i++) {
path.lineTo(i * 2, sin(i * 0.1) * 50);
}
return path;
}
}
3. 图片资源优化
dart
class ImagePainter extends CustomPainter {
final ui.Image? image;
Future<void> precacheImage() async {
final ByteData data = await rootBundle.load('assets/image.png');
final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
final frame = await codec.getNextFrame();
image = frame.image;
}
@override
void paint(Canvas canvas, Size size) {
if (image != null) {
canvas.drawImageRect(
image!,
Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.height.toDouble()),
Rect.fromLTWH(0, 0, size.width, size.height),
Paint(),
);
}
}
}
五、实战案例:完整的圆形进度条
dart
class CircularProgressView extends StatefulWidget {
final double progress;
final double strokeWidth;
final Color backgroundColor;
final Color progressColor;
const CircularProgressView({
Key? key,
required this.progress,
this.strokeWidth = 10,
this.backgroundColor = Colors.grey,
this.progressColor = Colors.blue,
}) : super(key: key);
@override
_CircularProgressViewState createState() => _CircularProgressViewState();
}
class _CircularProgressViewState extends State<CircularProgressView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
)..forward();
}
@override
void didUpdateWidget(CircularProgressView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.progress != oldWidget.progress) {
_controller.forward(from: 0);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: _CircularProgressPainter(
progress: widget.progress * _controller.value,
strokeWidth: widget.strokeWidth,
backgroundColor: widget.backgroundColor,
progressColor: widget.progressColor,
),
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class _CircularProgressPainter extends CustomPainter {
final double progress;
final double strokeWidth;
final Color backgroundColor;
final Color progressColor;
_CircularProgressPainter({
required this.progress,
required this.strokeWidth,
required this.backgroundColor,
required this.progressColor,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (min(size.width, size.height) - strokeWidth) / 2;
// 绘制背景圆
final backgroundPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, backgroundPaint);
// 绘制进度弧
final progressPaint = Paint()
..color = progressColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
final sweepAngle = 2 * pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2,
sweepAngle,
false,
progressPaint,
);
// 绘制进度文本
final textPainter = TextPainter(
text: TextSpan(
text: '${(progress * 100).toInt()}%',
style: TextStyle(
fontSize: radius * 0.4,
color: progressColor,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
center - Offset(textPainter.width / 2, textPainter.height / 2),
);
}
@override
bool shouldRepaint(_CircularProgressPainter oldDelegate) {
return progress != oldDelegate.progress ||
strokeWidth != oldDelegate.strokeWidth ||
backgroundColor != oldDelegate.backgroundColor ||
progressColor != oldDelegate.progressColor;
}
}
六、调试与性能监控
dart
// 在 MaterialApp 中启用性能覆盖层
MaterialApp(
home: Scaffold(
body: Stack(
children: [
YourCustomView(),
PerformanceOverlay.allEnabled(), // 显示性能数据
],
),
),
);
// 检查绘制性能
void checkPerformance() {
// 使用 Flutter DevTools 的 Performance 面板
// 重点关注:
// - GPU 绘制时间
// - 重绘区域(通过 debugPaintRepaintRainbowEnabled)
// - 内存使用情况
}
七、进阶技巧与最佳实践
- 使用 RepaintBoundary 隔离重绘区域
- 避免在 paint 方法中创建新对象
- 对于动画,优先使用 AnimationBuilder
- 复杂图形考虑使用 Canvas 的图层操作(saveLayer/restore)
- 测试不同设备的性能表现
这份指南涵盖了从基础到进阶的完整知识体系,希望能帮助您掌握 Flutter 自定义 View 的开发技能。在实际开发中,建议根据具体需求选择合适的方案,并始终关注性能优化。