在 Flutter 开发中,ListView、GridView等通用滚动组件能满足 80% 的常规场景,但面对电商异形商品展示、社交 APP 个性化卡片流、数据可视化仪表盘等复杂 UI 需求时,仅靠组合现有 Widget 往往会遇到性能瓶颈或视觉效果限制。此时深入 Flutter 渲染底层,自定义RenderObject成为实现高性能、定制化布局的核心方案。
本文将从 Flutter 渲染架构的核心原理出发,手把手教你实现一个扇形滚动列表 (非通用场景、代码实现独特),并拆解自定义RenderObject的关键流程与性能优化技巧,让你掌握 Flutter 渲染层的核心能力。
一、核心铺垫:Widget-Element-RenderObject 三层架构解析
要理解自定义RenderObject,首先要理清 Flutter UI 渲染的三层核心结构,这是所有自定义布局的基础:
| 层级 | 核心作用 | 生命周期特性 |
|---|---|---|
| Widget | 纯配置类,描述 UI 的 "样子"(不可变、轻量) | 可频繁重建,仅保存配置信息 |
| Element | Widget 的实例化节点,管理 Widget 与 RenderObject 的关联(连接层) | 树结构稳定,仅在 Widget 类型变化时重建 |
| RenderObject | 真正处理布局(Layout)、绘制(Paint)、触摸事件的对象(渲染层) | 重量级,尽量减少重建 / 重计算 |
三者的关联流程:
- Widget 通过
createElement()创建对应的 Element; - Element 在
mount()阶段调用 Widget 的createRenderObject()创建 RenderObject; - RenderObject 接收 Element 传递的配置,完成布局与绘制,最终输出到屏幕。
通用组件(如Container)的 RenderObject 由 Flutter 框架封装,而自定义布局的核心,就是通过重写RenderObject的布局、绘制逻辑,实现定制化 UI。
二、实战:自定义 RenderObject 实现扇形滚动列表
2.1 需求定义
我们要实现的扇形滚动列表具备以下特性:
- 列表项围绕垂直中心轴呈扇形排列;
- 滚动时列表项随位置变化自动缩放 + 旋转;
- 越靠近视口中心的列表项越大、越清晰,边缘项越小;
- 全程保持 60fps 高性能渲染,无卡顿。
2.2 数据模型与基础准备
首先定义列表项的数据模型,包含核心展示信息与布局参数:
Dart
/// 扇形列表项数据模型
class FanListItem {
/// 展示文本
final String text;
/// 基础尺寸
final double baseSize;
/// 颜色
final Color color;
FanListItem({
required this.text,
required this.baseSize,
required this.color,
});
}
2.3 自定义 RenderBox:核心布局与绘制逻辑
RenderObject的子类中,RenderBox是处理二维布局的基础类(对应矩形区域)。我们自定义RenderFanList继承RenderBox,并重写核心方法:
Dart
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// 自定义RenderBox:实现扇形列表的布局与绘制
class RenderFanList extends RenderBox {
/// 列表数据
final List<FanListItem> items;
/// 滚动偏移量(由外部Scrollable驱动)
double _scrollOffset = 0.0;
RenderFanList({
required this.items,
double scrollOffset = 0.0,
}) : _scrollOffset = scrollOffset;
// 设置滚动偏移并标记需要重绘
set scrollOffset(double value) {
if (_scrollOffset == value) return;
_scrollOffset = value;
markNeedsPaint(); // 仅标记重绘,避免不必要的布局计算
}
// 布局约束:父节点传递的尺寸限制
@override
void performLayout() {
// 扇形列表的整体尺寸:宽度取父约束最大值,高度自适应(或固定)
size = Size(
constraints.maxWidth,
constraints.maxHeight,
);
}
// 核心绘制逻辑
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
final canvas = context.canvas;
final centerX = size.width / 2; // 扇形中心X轴
final centerY = size.height / 2; // 扇形中心Y轴
final itemCount = items.length;
final itemSpacing = 80.0; // 列表项间距
// 缓存变换矩阵,避免重复计算(性能优化)
final matrixCache = <Matrix4>[];
for (int i = 0; i < itemCount; i++) {
final item = items[i];
// 计算当前项的实际Y坐标(结合滚动偏移)
final itemY = centerY + (i - itemCount / 2) * itemSpacing - _scrollOffset;
// 计算缩放比例:距离中心越近,缩放越大(0.5~1.0)
final scale = 1.0 - (itemY - centerY).abs() / (size.height / 2) * 0.5;
// 计算旋转角度:距离中心越远,旋转角度越大(-15°~15°)
final rotation = -(itemY - centerY) / (size.height / 2) * 15 * 3.14159 / 180;
// 构建变换矩阵(平移+旋转+缩放)
final matrix = Matrix4.identity()
..translate(centerX - item.baseSize / 2, itemY - item.baseSize / 2)
..rotateZ(rotation)
..scale(scale);
matrixCache.add(matrix);
// 保存画布状态
canvas.save();
// 应用变换矩阵
canvas.transform(matrix.storage);
// 绘制列表项背景(圆角矩形)
final rect = Rect.fromLTWH(0, 0, item.baseSize, item.baseSize);
final paint = Paint()..color = item.color.withOpacity(scale);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, Radius.circular(12)),
paint,
);
// 绘制文本
final textPainter = TextPainter(
text: TextSpan(
text: item.text,
style: TextStyle(
color: Colors.white,
fontSize: 14 * scale,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
(item.baseSize - textPainter.width) / 2,
(item.baseSize - textPainter.height) / 2,
),
);
// 恢复画布状态
canvas.restore();
}
}
// 命中测试:处理触摸事件(可选,本文暂不展开)
@override
bool hitTestSelf(Offset position) => true;
}
2.4 封装 Scrollable:让自定义 RenderObject 支持滚动
自定义RenderBox本身不具备滚动能力,需要结合 Flutter 的Scrollable、Viewport等组件封装成可滚动 Widget:
Dart
/// 扇形滚动列表Widget
class FanScrollList extends StatefulWidget {
final List<FanListItem> items;
const FanScrollList({
super.key,
required this.items,
});
@override
State<FanScrollList> createState() => _FanScrollListState();
}
class _FanScrollListState extends State<FanScrollList> {
/// 滚动控制器
final ScrollController _scrollController = ScrollController();
/// 渲染对象引用
RenderFanList? _renderFanList;
@override
void initState() {
super.initState();
// 监听滚动偏移,同步到RenderObject
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_renderFanList != null) {
_renderFanList!.scrollOffset = _scrollController.offset;
}
}
@override
Widget build(BuildContext context) {
return Scrollable(
controller: _scrollController,
axisDirection: AxisDirection.down,
physics: const BouncingScrollPhysics(), // 弹性滚动物理效果
viewportBuilder: (context, offset) {
return LayoutBuilder(
builder: (context, constraints) {
return CustomPaint(
// 自定义RenderObject关联到Widget
painter: _FanListPainter(
items: widget.items,
onRenderObjectCreated: (renderObject) {
_renderFanList = renderObject;
},
),
size: Size(constraints.maxWidth, constraints.maxHeight * 3), // 滚动区域高度
);
},
);
},
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
/// 连接Widget与RenderObject的Painter
class _FanListPainter extends CustomPainter {
final List<FanListItem> items;
final Function(RenderFanList) onRenderObjectCreated;
RenderFanList? _renderObject;
_FanListPainter({
required this.items,
required this.onRenderObjectCreated,
});
@override
void paint(Canvas canvas, Size size) {
if (_renderObject == null) {
_renderObject = RenderFanList(items: items);
onRenderObjectCreated(_renderObject!);
}
// 将画布传递给RenderObject进行绘制
_renderObject!.layout(BoxConstraints.tight(size));
_renderObject!.paint(PaintingContext(canvas, Offset.zero), Offset.zero);
}
@override
bool shouldRepaint(covariant _FanListPainter oldDelegate) {
return oldDelegate.items != items;
}
}
2.5 完整使用示例
将上述组件整合,实现可运行的完整示例:
Dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter扇形滚动列表',
theme: ThemeData(primarySwatch: Colors.blue),
home: const FanListDemo(),
);
}
}
class FanListDemo extends StatelessWidget {
const FanListDemo({super.key});
// 模拟列表数据
List<FanListItem> _generateItems() {
final colors = [
Colors.redAccent,
Colors.blueAccent,
Colors.greenAccent,
Colors.orangeAccent,
Colors.purpleAccent,
Colors.tealAccent,
Colors.pinkAccent,
];
return List.generate(
10,
(index) => FanListItem(
text: 'Item $index',
baseSize: 120,
color: colors[index % colors.length],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('自定义RenderObject扇形列表')),
body: FanScrollList(items: _generateItems()),
);
}
}
三、性能优化:让自定义 RenderObject 更丝滑
自定义RenderObject若处理不当,容易出现卡顿,以下是核心优化技巧:
3.1 精准标记重绘 / 重布局
- 仅在必要时调用
markNeedsLayout()(布局参数变化时),优先使用markNeedsPaint()(仅重绘); - 本文中滚动偏移变化仅触发重绘(
markNeedsPaint()),而非重布局,减少计算开销。
3.2 缓存计算结果
- 对旋转角度、缩放比例、矩阵变换等重复计算的值进行缓存(如本文的
matrixCache),避免每次paint都重新计算。
3.3 隔离重绘区域
适用场景
自定义RenderObject并非银弹,以下场景优先使用:
拓展方向
掌握RenderObject的自定义能力,能让你突破 Flutter 通用组件的限制,真正掌控 UI 渲染的底层逻辑,应对各类复杂的定制化需求。希望本文能帮助你理解 Flutter 渲染架构的核心,写出更高效、更灵活的 Flutter 代码。
-
使用
RepaintBoundary包裹独立的绘制区域,避免单个列表项变化导致整个画布重绘:Dart// 在FanScrollList的build中添加RepaintBoundary viewportBuilder: (context, offset) { return RepaintBoundary( child: LayoutBuilder(/* ... */), ); }3.4 减少绘制对象创建
-
避免在
paint方法内创建TextPainter、Paint等对象(本文示例为简化未做,生产环境需缓存):Dart// 优化方案:将TextPainter缓存到RenderFanList中 class RenderFanList extends RenderBox { final Map<int, TextPainter> _textPainterCache = {}; @override void paint(PaintingContext context, Offset offset) { for (int i = 0; i < items.length; i++) { if (!_textPainterCache.containsKey(i)) { _textPainterCache[i] = TextPainter(/* 初始化 */); } final textPainter = _textPainterCache[i]!; // 复用textPainter进行绘制 } } }四、总结与拓展
本文通过实现扇形滚动列表 这一非通用场景,拆解了 Flutter 自定义
RenderObject的核心流程: -
定义
RenderBox子类,重写performLayout(布局)和paint(绘制); -
关联 Widget 与 RenderObject(通过
CustomPaint/CustomSingleChildLayout); -
结合
Scrollable实现滚动交互; -
针对性优化性能,保证渲染流畅。
-
通用组件无法满足的异形布局(如扇形、环形、不规则网格);
-
结合
Physics自定义滚动物理效果(如扇形列表的惯性衰减); -
增加列表项的点击 / 长按事件(重写
hitTest方法); -
实现按需加载(惰性渲染),仅绘制视口内的列表项。
- 高性能要求的大数据量列表(避免 Widget 树嵌套导致的性能损耗);
- 自定义触摸事件处理(如精准的点击 / 滑动识别)。
- https://openharmonycrossplatform.csdn.net/content
- 欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。