一个涵盖 10 种核心方法 的 Flutter 自定义组件完整示例项目,从简单到复杂,全面展示 Flutter 自定义组件的实现思路与技术细节。
- 方式总览
- 1. Widget 组合 (Composition)
- 2. StatefulWidget
- 3. CustomPainter
- 4. RenderObject/RenderBox
- 5. CustomLayout
- 6. Sliver
- 7. CustomClipper
- 8. Shader
- 9. PlatformView
- 10. ImplicitlyAnimatedWidget
- 项目结构
- 快速开始
- 参考资源
| 方式 | 难度 | 性能 | 适用场景 |
|---|---|---|---|
| Widget 组合 | 简单 | 高 | 一般 UI 组件、卡片、列表项 |
| StatefulWidget | 简单 | 高 | 有状态组件、交互组件 |
| CustomPainter | 中等 | 高 | 图表、特效、自绑图形 |
| RenderObject/RenderBox | 复杂 | 最高 | 完全自定义布局、高性能场景 |
| CustomLayout | 中等 | 高 | 自定义布局规则 |
| Sliver | 中等 | 高 | 高级滚动效果、复杂头部 |
| CustomClipper | 简单 | 高 | 非矩形裁剪、装饰效果 |
| Shader | 复杂 | 中等 | 视觉特效、渐变效果 |
| PlatformView | 复杂 | 中等 | 嵌入原生组件 |
| ImplicitlyAnimatedWidget | 中等 | 高 | 简单动画组件 |
最简单、最常用的自定义组件方式。通过组合 Flutter 提供的基础 Widget(Container、Row、Column、Stack 等)来构建复杂的 UI 组件,无需继承或自定义渲染逻辑。
| 场景 | 示例 | 组合的 Widget |
|---|---|---|
| 产品卡片 | 电商 App 的商品展示卡 | Image + Stack + Column + Text + Icon |
| 用户资料卡 | 社交 App 的用户信息卡 | CircleAvatar + Row + Column + Text |
| 功能入口 | 设置页的功能瓦片 | Container + Icon + Text |
| 信息横幅 | 提示、警告、成功提示条 | Container + Row + Icon + Text |
1. 分析 UI 设计 → 拆分为基础 Widget
2. 继承 StatelessWidget
3. 定义构造函数参数(可配置项)
4. 在 build() 方法中组合各个 Widget
class StatelessWidget {
Widget build(BuildContext context); // 构建 Widget 树
}常用基础 Widget:
- 布局:
Container,Row,Column,Stack,Wrap,Flex - 装饰:
DecoratedBox,ClipRRect,Padding,SizedBox - 文本:
Text,RichText - 图片:
Image,Icon,CircleAvatar
/// 产品展示卡片 - Widget 组合示例
class ProductCard extends StatelessWidget {
final String title;
final String description;
final double price;
final String imageUrl;
final double rating;
final bool isNew;
const ProductCard({
super.key,
required this.title,
required this.description,
required this.price,
required this.imageUrl,
this.rating = 0.0,
this.isNew = false,
});
@override
Widget build(BuildContext context) {
return Container(
width: 280,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 产品图片区域(使用 Stack 实现标签叠加)
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: Image.network(imageUrl, height: 180, fit: BoxFit.cover),
),
if (isNew)
Positioned(
top: 12,
left: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(20),
),
child: const Text('NEW', style: TextStyle(color: Colors.white)),
),
),
],
),
// 产品信息区域
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(description, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('¥${price.toStringAsFixed(2)}'),
Row(children: [
Icon(Icons.star, size: 18, color: Colors.amber[700]),
Text(rating.toStringAsFixed(1)),
]),
],
),
],
),
),
],
),
);
}
}用于创建拥有可变状态的组件。通过 setState() 触发 UI 重建,管理组件的生命周期和内部状态。
| 场景 | 示例 | 状态类型 |
|---|---|---|
| 计数器 | 购物车数量选择器 | 数值状态 |
| 颜色选择器 | 主题颜色配置 | 选中颜色状态 |
| 动画组件 | 加载动画、过渡动画 | AnimationController |
| 表单输入 | 搜索框、输入验证 | TextEditingController |
1. 创建 StatefulWidget 类 → 实现 createState()
2. 创建对应的 State<T> 类
3. initState() → 初始化状态、创建控制器
4. build() → 构建 UI,使用状态变量
5. setState() → 触发 UI 更新
6. dispose() → 清理资源、释放控制器
abstract class StatefulWidget {
State createState(); // 创建 State 对象
}
abstract class State<T extends StatefulWidget> {
T get widget; // 获取 Widget 配置
void initState(); // 初始化(只调用一次)
Widget build(BuildContext context); // 构建 UI
void didUpdateWidget(T oldWidget); // Widget 配置更新时调用
void dispose(); // 销毁时调用
void setState(VoidCallback fn); // 触发重建
}常用控制器:
AnimationController- 动画控制TextEditingController- 文本输入ScrollController- 滚动控制FocusNode- 焦点管理
/// 计数器组件 - StatefulWidget 示例
class CounterWidget extends StatefulWidget {
final int initialValue;
final Color? accentColor;
const CounterWidget({
super.key,
this.initialValue = 0,
this.accentColor,
});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
late int _counter;
@override
void initState() {
super.initState();
_counter = widget.initialValue; // 初始化状态
debugPrint('CounterWidget initState: counter = $_counter');
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 当父组件传入新的 initialValue 时,重新设置计数器
if (oldWidget.initialValue != widget.initialValue) {
_counter = widget.initialValue;
}
}
@override
void dispose() {
debugPrint('CounterWidget dispose');
super.dispose();
}
void _increment() {
setState(() {
_counter++; // 修改状态并触发重建
});
}
void _decrement() {
setState(() {
_counter--;
});
}
@override
Widget build(BuildContext context) {
final color = widget.accentColor ?? Theme.of(context).colorScheme.primary;
return Column(
children: [
Text('$_counter', style: TextStyle(fontSize: 48, color: color)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton.filled(onPressed: _decrement, icon: const Icon(Icons.remove)),
const SizedBox(width: 16),
IconButton.filled(onPressed: _increment, icon: const Icon(Icons.add)),
],
),
],
);
}
}使用 Canvas API 直接绑制图形,适合绑制图表、特效、复杂图案等标准 Widget 难以实现的效果。
| 场景 | 示例 | 核心 API |
|---|---|---|
| 圆形进度条 | 下载进度、技能等级 | drawArc + drawCircle |
| 星形/多边形 | 评分星星、徽章图案 | Path + drawPath |
| 波浪动画 | 水波效果、音频可视化 | Path + Animation |
| 数据图表 | 折线图、饼图、柱状图 | drawLine + drawRect |
| 渐变背景 | 复杂渐变效果 | Shader + drawRect |
1. 继承 CustomPainter 类
2. 实现 paint(Canvas canvas, Size size) 方法
3. 使用 Canvas API 绑制图形
4. 实现 shouldRepaint() 控制重绑时机
5. 使用 CustomPaint Widget 挂载 painter
abstract class CustomPainter {
void paint(Canvas canvas, Size size); // 绑制逻辑
bool shouldRepaint(covariant CustomPainter oldDelegate); // 是否需要重绑
}
// CustomPaint Widget
CustomPaint(
painter: MyPainter(), // 背景 painter
foregroundPainter: null, // 前景 painter
size: Size.zero, // 首选大小
child: null, // 子组件
)Canvas 常用方法:
drawRect()- 矩形drawCircle()- 圆形drawArc()- 圆弧drawPath()- 路径drawLine()- 直线drawImage()- 图片
Paint 常用属性:
color- 颜色style- PaintingStyle.fill / strokestrokeWidth- 线宽strokeCap- 线端样式shader- 着色器(渐变)
/// 圆形进度条绑制器 - CustomPainter 示例
class ProgressPainter extends CustomPainter {
final double progress; // 进度 0.0 - 1.0
final Color backgroundColor;
final Color progressColor;
final double strokeWidth;
ProgressPainter({
required this.progress,
required this.backgroundColor,
required this.progressColor,
this.strokeWidth = 10.0,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2 - strokeWidth / 2;
// 1. 绑制背景圆环
final backgroundPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, backgroundPaint);
// 2. 绑制进度圆弧(带渐变)
final progressPaint = Paint()
..shader = SweepGradient(
startAngle: -pi / 2,
endAngle: -pi / 2 + 2 * pi * progress,
colors: [progressColor, progressColor.withValues(alpha: 0.6)],
).createShader(Rect.fromCircle(center: center, radius: radius))
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
final rect = Rect.fromCircle(center: center, radius: radius);
const startAngle = -pi / 2; // 从顶部开始
final sweepAngle = 2 * pi * progress;
canvas.drawArc(rect, startAngle, sweepAngle, false, progressPaint);
// 3. 绘制中心文字
final textPainter = TextPainter(
text: TextSpan(
text: '${(progress * 100).toInt()}%',
style: TextStyle(color: progressColor, fontSize: 32, fontWeight: FontWeight.bold),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, Offset(
center.dx - textPainter.width / 2,
center.dy - textPainter.height / 2,
));
}
@override
bool shouldRepaint(ProgressPainter oldDelegate) {
return oldDelegate.progress != progress ||
oldDelegate.progressColor != progressColor;
}
}
// 使用方式
CustomPaint(
size: const Size(200, 200),
painter: ProgressPainter(
progress: 0.75,
backgroundColor: Colors.grey.shade300,
progressColor: Colors.blue,
),
)Flutter 渲染层的最底层定制方式,拥有对布局和绑制的完全控制权。适合实现标准布局 Widget 无法实现的复杂布局逻辑。
| 场景 | 示例 | 特点 |
|---|---|---|
| 圆形布局 | 环形菜单、时钟刻度 | 子组件排列成圆形 |
| 螺旋布局 | 相册展示、装饰效果 | 阿基米德螺线排列 |
| 对角线布局 | 创意卡片、图片墙 | 斜向排列 |
| 波浪布局 | 音频可视化、装饰元素 | 正弦波排列 |
| 瀑布流 | Pinterest 风格布局 | 高性能不等高列表 |
1. Widget 层:继承 SingleChildRenderObjectWidget / MultiChildRenderObjectWidget
2. 实现 createRenderObject() 创建 RenderBox
3. 实现 updateRenderObject() 更新属性
4. RenderBox 层:继承 RenderBox
5. 实现 setupParentData() 设置子组件布局信息
6. 实现 performLayout() 核心布局逻辑
7. 实现 paint() 绑制子组件
8. 实现 hitTestChildren() 点击测试
// Widget 层
abstract class MultiChildRenderObjectWidget {
RenderObject createRenderObject(BuildContext context);
void updateRenderObject(BuildContext context, RenderObject renderObject);
}
// RenderBox 层
abstract class RenderBox {
void setupParentData(RenderBox child); // 设置子组件的 ParentData
void performLayout(); // 布局逻辑
void paint(PaintingContext context, Offset offset); // 绑制
bool hitTestChildren(BoxHitTestResult result, {required Offset position});
}
// 多子组件 Mixin
mixin ContainerRenderObjectMixin<ChildType, ParentDataType> {
ChildType? get firstChild; // 第一个子组件
int get childCount; // 子组件数量
}/// 圆形布局 - RenderBox 示例
/// Widget 层:定义接口
class CircularLayout extends MultiChildRenderObjectWidget {
final double radius;
final double startAngle;
const CircularLayout({
super.key,
required this.radius,
this.startAngle = 0.0,
required super.children,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCircularLayout(radius: radius, startAngle: startAngle);
}
@override
void updateRenderObject(BuildContext context, RenderCircularLayout renderObject) {
renderObject
..radius = radius
..startAngle = startAngle;
}
}
/// RenderBox 层:实现布局逻辑
class RenderCircularLayout extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
double _radius;
double _startAngle;
RenderCircularLayout({required double radius, required double startAngle})
: _radius = radius, _startAngle = startAngle;
double get radius => _radius;
set radius(double value) {
if (_radius == value) return;
_radius = value;
markNeedsLayout(); // 标记需要重新布局
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MultiChildLayoutParentData) {
child.parentData = MultiChildLayoutParentData();
}
}
@override
void performLayout() {
double maxChildSize = 0;
// 1. 布局所有子组件,获取它们的大小
RenderBox? child = firstChild;
while (child != null) {
child.layout(BoxConstraints.loose(constraints.biggest), parentUsesSize: true);
maxChildSize = max(maxChildSize, max(child.size.width, child.size.height));
child = (child.parentData as MultiChildLayoutParentData).nextSibling;
}
// 2. 设置自身大小
final totalSize = _radius * 2 + maxChildSize;
size = constraints.constrain(Size(totalSize, totalSize));
// 3. 计算子组件位置(圆形排列)
final center = Offset(size.width / 2, size.height / 2);
final angleStep = (2 * pi) / childCount;
child = firstChild;
int index = 0;
while (child != null) {
final angle = _startAngle + angleStep * index;
final x = center.dx + _radius * cos(angle) - child.size.width / 2;
final y = center.dy + _radius * sin(angle) - child.size.height / 2;
(child.parentData as MultiChildLayoutParentData).offset = Offset(x, y);
child = (child.parentData as MultiChildLayoutParentData).nextSibling;
index++;
}
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset); // 绘制所有子组件
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
// 使用方式
CircularLayout(
radius: 100,
startAngle: -pi / 2,
children: [
for (int i = 0; i < 6; i++)
Container(width: 50, height: 50, color: Colors.primaries[i]),
],
)比 RenderBox 更高层、更简单的自定义布局方式。只关注布局逻辑,不需要处理绑制和点击测试。
| 场景 | 示例 | Delegate 类型 |
|---|---|---|
| 径向菜单 | 环形展开的操作菜单 | MultiChildLayoutDelegate |
| 仪表板 | 多个面板的复杂布局 | MultiChildLayoutDelegate |
| 居中偏移 | 带偏移的居中布局 | SingleChildLayoutDelegate |
| 跟随指针 | 组件跟随手指位置 | SingleChildLayoutDelegate |
1. 选择 CustomSingleChildLayout 或 CustomMultiChildLayout
2. 继承对应的 Delegate 类
3. 实现 performLayout(Size size) 方法
4. 使用 layoutChild() 测量子组件大小
5. 使用 positionChild() 定位子组件
6. 实现 shouldRelayout() 优化性能
// 单子组件布局
abstract class SingleChildLayoutDelegate {
Size getSize(BoxConstraints constraints);
BoxConstraints getConstraintsForChild(BoxConstraints constraints);
Offset getPositionForChild(Size size, Size childSize);
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate);
}
// 多子组件布局
abstract class MultiChildLayoutDelegate {
void performLayout(Size size);
Size layoutChild(Object childId, BoxConstraints constraints); // 测量子组件
void positionChild(Object childId, Offset offset); // 定位子组件
bool hasChild(Object childId); // 检查子组件是否存在
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
}/// 径向菜单布局代理 - MultiChildLayoutDelegate 示例
class RadialMenuLayoutDelegate extends MultiChildLayoutDelegate {
final List<Object> itemIds;
final double radius;
final double startAngle;
RadialMenuLayoutDelegate({
required this.itemIds,
this.radius = 120,
this.startAngle = 0,
});
@override
void performLayout(Size size) {
final center = Offset(size.width / 2, size.height / 2);
final itemCount = itemIds.length;
if (itemCount == 0) return;
final angleStep = (2 * pi) / itemCount;
for (int i = 0; i < itemCount; i++) {
final itemId = itemIds[i];
if (hasChild(itemId)) {
// 测量子组件
final childSize = layoutChild(itemId, BoxConstraints.loose(size));
// 计算位置
final angle = startAngle + angleStep * i;
final x = center.dx + radius * cos(angle) - childSize.width / 2;
final y = center.dy + radius * sin(angle) - childSize.height / 2;
// 定位子组件
positionChild(itemId, Offset(x, y));
}
}
}
@override
bool shouldRelayout(RadialMenuLayoutDelegate oldDelegate) {
return oldDelegate.radius != radius || oldDelegate.startAngle != startAngle;
}
}
// 使用方式
CustomMultiChildLayout(
delegate: RadialMenuLayoutDelegate(
itemIds: ['item1', 'item2', 'item3', 'item4'],
radius: 100,
),
children: [
LayoutId(id: 'item1', child: const Icon(Icons.home)),
LayoutId(id: 'item2', child: const Icon(Icons.search)),
LayoutId(id: 'item3', child: const Icon(Icons.settings)),
LayoutId(id: 'item4', child: const Icon(Icons.person)),
],
)用于构建高性能滚动效果的组件,如可伸缩头部、固定头部、懒加载列表等。Sliver 系列组件是 CustomScrollView 的核心组成部分。
| 场景 | 示例 | 核心组件 |
|---|---|---|
| 可伸缩头部 | 个人主页、详情页头部 | SliverPersistentHeader |
| 固定头部 | 分组列表的组头固定 | SliverPersistentHeader (pinned) |
| 视差滚动 | 背景图片视差效果 | SliverAppBar |
| 懒加载列表 | 长列表性能优化 | SliverList + SliverChildBuilderDelegate |
1. 继承 SliverPersistentHeaderDelegate
2. 实现 build(context, shrinkOffset, overlapsContent) 方法
3. 根据 shrinkOffset 计算收缩进度
4. 定义 minExtent 和 maxExtent
5. 实现 shouldRebuild()
6. 使用 SliverPersistentHeader 包装
7. 放入 CustomScrollView 中使用
abstract class SliverPersistentHeaderDelegate {
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
double get minExtent; // 最小高度
double get maxExtent; // 最大高度
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
}
// 使用方式
SliverPersistentHeader(
delegate: MyDelegate(),
pinned: true, // 是否固定在顶部
floating: false, // 是否浮动
)/// 可伸缩头部代理 - SliverPersistentHeaderDelegate 示例
class StretchyHeaderDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
final String title;
final String backgroundImage;
StretchyHeaderDelegate({
required this.minHeight,
required this.maxHeight,
required this.title,
required this.backgroundImage,
});
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
// 计算收缩进度 (0.0 = 完全展开, 1.0 = 完全收缩)
final progress = (shrinkOffset / (maxHeight - minHeight)).clamp(0.0, 1.0);
// 根据进度调整效果
final opacity = 1.0 - progress;
final titleSize = 32.0 - (progress * 12.0); // 从 32 缩小到 20
return Stack(
fit: StackFit.expand,
children: [
// 背景图片(随滚动淡出)
Opacity(
opacity: opacity,
child: Image.network(backgroundImage, fit: BoxFit.cover),
),
// 渐变遮罩
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.3 * opacity),
Colors.black.withValues(alpha: 0.7 * opacity),
],
),
),
),
// 收缩后的纯色背景
Opacity(
opacity: progress,
child: Container(color: Theme.of(context).colorScheme.primary),
),
// 标题(随滚动调整大小和位置)
Positioned(
left: 16,
bottom: 16 + (progress * 20),
child: Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: titleSize,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(StretchyHeaderDelegate oldDelegate) {
return oldDelegate.title != title || oldDelegate.backgroundImage != backgroundImage;
}
}
// 使用方式
CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: StretchyHeaderDelegate(
minHeight: 80,
maxHeight: 300,
title: '个人主页',
backgroundImage: 'https://example.com/cover.jpg',
),
pinned: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 50,
),
),
],
)用于创建非矩形的裁剪区域,实现波浪形、心形、星形等特殊形状的裁剪效果。
| 场景 | 示例 | 形状 |
|---|---|---|
| 波浪裁剪 | 页面底部装饰、卡片边缘 | 贝塞尔曲线波浪 |
| 心形裁剪 | 头像框、点赞动画 | 心形路径 |
| 星形裁剪 | 评分显示、徽章 | 多角星路径 |
| 票券裁剪 | 优惠券、电影票 | 锯齿边缘 |
| 对角线裁剪 | 创意卡片、图片展示 | 斜线裁剪 |
1. 继承 CustomClipper<Path>
2. 实现 getClip(Size size) 返回裁剪路径
3. 使用 Path API 构建形状
4. 实现 shouldReclip() 优化性能
5. 使用 ClipPath 应用裁剪
abstract class CustomClipper<T> {
T getClip(Size size); // 返回裁剪区域
bool shouldReclip(covariant CustomClipper<T> oldClipper);
}
// 使用方式
ClipPath(
clipper: MyClipper(),
child: widget,
)Path 常用方法:
moveTo(x, y)- 移动到起点lineTo(x, y)- 直线quadraticBezierTo(x1, y1, x2, y2)- 二次贝塞尔曲线cubicTo(x1, y1, x2, y2, x3, y3)- 三次贝塞尔曲线arcTo()- 圆弧close()- 闭合路径
/// 波浪裁剪器 - CustomClipper 示例
class WaveClipper extends CustomClipper<Path> {
final double waveHeight;
final int waveCount;
WaveClipper({
this.waveHeight = 20.0,
this.waveCount = 2,
});
@override
Path getClip(Size size) {
final path = Path();
// 从左上角开始
path.lineTo(0, size.height - waveHeight);
// 绘制底部波浪
final waveLength = size.width / waveCount;
for (int i = 0; i < waveCount; i++) {
final x1 = i * waveLength;
final x2 = x1 + waveLength / 2;
final x3 = x1 + waveLength;
// 使用二次贝塞尔曲线绘制波浪
path.quadraticBezierTo(
x2, size.height, // 控制点(波谷)
x3, size.height - waveHeight, // 终点(回到基线)
);
}
// 完成路径
path.lineTo(size.width, 0);
path.close();
return path;
}
@override
bool shouldReclip(WaveClipper oldClipper) {
return oldClipper.waveHeight != waveHeight || oldClipper.waveCount != waveCount;
}
}
// 使用方式
ClipPath(
clipper: WaveClipper(waveHeight: 30, waveCount: 3),
child: Container(
height: 200,
color: Colors.blue,
child: const Center(child: Text('Wave Clipped')),
),
)使用着色器实现高级视觉效果,包括渐变、模糊、自定义 GLSL 着色器等。Flutter 3.10+ 支持自定义 Fragment Shader。
| 场景 | 示例 | 技术 |
|---|---|---|
| 渐变效果 | 彩虹背景、金属质感 | LinearGradient / RadialGradient |
| 文字渐变 | 彩色标题、霓虹效果 | ShaderMask |
| 扫描渐变 | 雷达扫描、仪表盘 | SweepGradient |
| 自定义特效 | 水波纹、故障艺术 | FragmentProgram (GLSL) |
1. 创建 Gradient 对象(Linear/Radial/Sweep)
2. 调用 createShader(Rect) 生成 Shader
3. 在 Paint.shader 中使用
4. 或使用 ShaderMask Widget 应用到子组件
// 渐变类
LinearGradient(colors: [...], begin: Alignment.topLeft, end: Alignment.bottomRight)
RadialGradient(colors: [...], center: Alignment.center, radius: 0.5)
SweepGradient(colors: [...], startAngle: 0, endAngle: 2 * pi)
// 生成 Shader
Shader shader = gradient.createShader(Rect.fromLTWH(0, 0, width, height));
// 在 Paint 中使用
final paint = Paint()..shader = shader;
// ShaderMask Widget
ShaderMask(
shaderCallback: (Rect bounds) => gradient.createShader(bounds),
blendMode: BlendMode.srcIn,
child: widget,
)
// 自定义 GLSL 着色器 (Flutter 3.10+)
final program = await FragmentProgram.fromAsset('shaders/my_shader.frag');
final shader = program.fragmentShader();/// 渐变着色器示例
class ShaderDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// 1. 线性渐变背景
Container(
width: 200,
height: 100,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple, Colors.pink],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
// 2. 文字渐变效果(使用 ShaderMask)
ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
colors: [Colors.red, Colors.orange, Colors.yellow],
).createShader(bounds);
},
blendMode: BlendMode.srcIn,
child: const Text(
'彩虹文字',
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
),
// 3. 扫描渐变(雷达效果)
CustomPaint(
size: const Size(200, 200),
painter: _RadarPainter(),
),
],
);
}
}
class _RadarPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final paint = Paint()
..shader = SweepGradient(
colors: [
Colors.green.withValues(alpha: 0.0),
Colors.green.withValues(alpha: 0.8),
Colors.green.withValues(alpha: 0.0),
],
stops: const [0.0, 0.5, 1.0],
).createShader(Rect.fromCircle(center: center, radius: radius));
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}将原生平台的 View 嵌入到 Flutter 应用中,用于使用原生组件(如地图、视频播放器、WebView)。
| 场景 | 示例 | 原生组件 |
|---|---|---|
| 地图 | 高德地图、Google Maps | MKMapView / MapView |
| 视频 | 视频播放器 | AVPlayer / ExoPlayer |
| WebView | 内嵌网页 | WKWebView / WebView |
| 广告 | 原生广告 | GADBannerView / AdView |
Flutter 侧:
1. 使用 UiKitView (iOS) 或 AndroidView (Android)
2. 指定 viewType 标识符
3. 传递创建参数
iOS 侧:
1. 实现 FlutterPlatformViewFactory
2. 实现 FlutterPlatformView 协议
3. 在 AppDelegate 中注册
Android 侧:
1. 实现 PlatformViewFactory
2. 实现 PlatformView 接口
3. 在 FlutterActivity 中注册
// Flutter 侧
UiKitView(
viewType: 'my_native_view',
creationParams: {'param': 'value'},
creationParamsCodec: const StandardMessageCodec(),
)
AndroidView(
viewType: 'my_native_view',
creationParams: {'param': 'value'},
creationParamsCodec: const StandardMessageCodec(),
)
// 推荐方式(跨平台)
PlatformViewLink(
viewType: 'my_native_view',
surfaceFactory: (context, controller) => ...,
onCreatePlatformView: (params) => ...,
)/// PlatformView 示例 - 嵌入原生组件
class NativeMapView extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 根据平台选择不同的实现
if (Platform.isIOS) {
return UiKitView(
viewType: 'native_map_view',
creationParams: {
'initialLatitude': 39.9042,
'initialLongitude': 116.4074,
'zoomLevel': 15,
},
creationParamsCodec: const StandardMessageCodec(),
);
} else if (Platform.isAndroid) {
return AndroidView(
viewType: 'native_map_view',
creationParams: {
'initialLatitude': 39.9042,
'initialLongitude': 116.4074,
'zoomLevel': 15,
},
creationParamsCodec: const StandardMessageCodec(),
);
}
return const Center(child: Text('Platform not supported'));
}
}创建隐式动画组件,当属性值变化时自动执行动画过渡。无需手动管理 AnimationController,代码更简洁。
| 场景 | 示例 | 动画属性 |
|---|---|---|
| 颜色过渡 | 主题切换、状态变化 | Color |
| 尺寸动画 | 展开/收起、缩放效果 | double (size) |
| 位置动画 | 列表排序、拖拽反馈 | Offset / Alignment |
| 圆角动画 | 卡片展开、按钮变形 | BorderRadius |
| 渐变动画 | 背景渐变变化 | Gradient |
1. 继承 ImplicitlyAnimatedWidget
2. 定义需要动画的属性
3. 创建 State 类,继承 AnimatedWidgetBaseState
4. 实现 forEachTween() 为每个属性创建 Tween
5. 在 build() 中使用 tween.evaluate(animation) 获取当前值
// Widget 层
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
final Duration duration;
final Curve curve;
}
// State 层
abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> {
Animation<double> get animation; // 动画进度 0.0 - 1.0
void forEachTween(TweenVisitor<dynamic> visitor); // 声明所有 Tween
}
// TweenVisitor 签名
typedef TweenVisitor<T> = Tween<T>? Function(
Tween<T>? tween, // 当前 Tween
T targetValue, // 目标值
TweenConstructor<T> constructor, // Tween 构造器
);常用 Tween 类型:
Tween<double>- 数值ColorTween- 颜色SizeTween- 尺寸AlignmentTween- 对齐BorderRadiusTween- 圆角DecorationTween- 装饰
/// 自定义隐式动画组件 - 颜色和尺寸动画
class AnimatedColorBox extends ImplicitlyAnimatedWidget {
final double size;
final Color color;
final BorderRadius? borderRadius;
const AnimatedColorBox({
super.key,
required this.size,
required this.color,
this.borderRadius,
super.duration = const Duration(milliseconds: 300),
super.curve = Curves.easeInOut,
});
@override
AnimatedColorBoxState createState() => AnimatedColorBoxState();
}
class AnimatedColorBoxState extends AnimatedWidgetBaseState<AnimatedColorBox> {
Tween<double>? _sizeTween;
ColorTween? _colorTween;
BorderRadiusTween? _borderRadiusTween;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
// 为每个需要动画的属性创建 Tween
_sizeTween = visitor(
_sizeTween,
widget.size,
(dynamic value) => Tween<double>(begin: value as double),
) as Tween<double>?;
_colorTween = visitor(
_colorTween,
widget.color,
(dynamic value) => ColorTween(begin: value as Color),
) as ColorTween?;
_borderRadiusTween = visitor(
_borderRadiusTween,
widget.borderRadius ?? BorderRadius.zero,
(dynamic value) => BorderRadiusTween(begin: value as BorderRadius),
) as BorderRadiusTween?;
}
@override
Widget build(BuildContext context) {
// 使用 tween.evaluate(animation) 获取当前动画值
return Container(
width: _sizeTween?.evaluate(animation),
height: _sizeTween?.evaluate(animation),
decoration: BoxDecoration(
color: _colorTween?.evaluate(animation),
borderRadius: _borderRadiusTween?.evaluate(animation),
),
);
}
}
// 使用方式
AnimatedColorBox(
size: isExpanded ? 200 : 100,
color: isActive ? Colors.blue : Colors.grey,
borderRadius: isExpanded ? BorderRadius.circular(20) : BorderRadius.circular(8),
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
)lib/
├── main.dart # 应用入口
├── models/
│ └── widget_demo.dart # Demo 数据模型
├── screens/
│ └── home_screen.dart # 主页导航
└── widgets/
├── 01_composition/ # 方式 1: Widget 组合
│ ├── composition_demo.dart
│ └── examples/
│ ├── feature_tile.dart
│ ├── info_banner.dart
│ ├── product_card.dart
│ └── user_card.dart
├── 02_stateful/ # 方式 2: StatefulWidget
│ ├── stateful_demo.dart
│ └── examples/
│ ├── animated_box.dart
│ ├── color_picker_widget.dart
│ ├── counter_widget.dart
│ └── text_input_widget.dart
├── 03_custom_painter/ # 方式 3: CustomPainter
│ ├── custom_painter_demo.dart
│ └── painters/
│ ├── animated_wave_widget.dart
│ ├── gradient_painter.dart
│ ├── progress_painter.dart
│ ├── shape_painter.dart
│ ├── star_painter.dart
│ └── wave_painter.dart
├── 04_render_object/ # 方式 4: RenderObject
│ ├── render_object_demo.dart
│ └── render_boxes/
│ ├── circular_layout.dart
│ ├── diagonal_layout.dart
│ ├── spiral_layout.dart
│ └── wave_layout.dart
├── 05_custom_layout/ # 方式 5: CustomLayout
│ ├── custom_layout_demo.dart
│ └── delegates/
│ ├── center_with_offset_delegate.dart
│ ├── dashboard_delegate.dart
│ ├── follow_pointer_delegate.dart
│ └── radial_menu_delegate.dart
├── 06_sliver/ # 方式 6: Sliver
│ ├── sliver_demo.dart
│ └── custom_slivers/
│ ├── pinned_header_delegate.dart
│ └── stretchy_header_delegate.dart
├── 07_custom_clipper/ # 方式 7: CustomClipper
│ ├── custom_clipper_demo.dart
│ └── clippers/
│ ├── diagonal_clipper.dart
│ ├── heart_clipper.dart
│ ├── star_clipper.dart
│ ├── ticket_clipper.dart
│ └── wave_clipper.dart
├── 08_shader/ # 方式 8: Shader
│ ├── shader_demo.dart
│ └── shaders/
├── 09_platform_view/ # 方式 9: PlatformView
│ ├── platform_view_demo.dart
│ └── platform_widgets/
└── 10_implicit_animation/ # 方式 10: ImplicitlyAnimatedWidget
├── implicit_animation_demo.dart
└── animated_widgets/
├── animated_color_box.dart
├── animated_gradient_container.dart
└── animated_position_box.dart
- Flutter SDK: ^3.9.2
- Dart SDK: ^3.9.2
# 克隆项目
git clone <repository-url>
cd custom_widget
# 获取依赖
flutter pub get
# 运行项目
flutter run- 入门阶段:从 Widget 组合 和 StatefulWidget 开始
- 进阶阶段:学习 CustomPainter 和 CustomClipper
- 高级阶段:深入 RenderObject 和 Sliver
- 专项学习:根据需求学习 Shader 和 PlatformView
- Flutter 官方文档
- Flutter Widget Catalog
- CustomPainter API
- RenderObject 深入解析
- Sliver 官方指南
- formwork.md - 10 种方法的核心 API 概述
License: MIT