Skip to content

Latest commit

 

History

History
1446 lines (1199 loc) · 40.1 KB

File metadata and controls

1446 lines (1199 loc) · 40.1 KB

Flutter 自定义组件实现方式大全

一个涵盖 10 种核心方法 的 Flutter 自定义组件完整示例项目,从简单到复杂,全面展示 Flutter 自定义组件的实现思路与技术细节。

目录


方式总览

方式 难度 性能 适用场景
Widget 组合 简单 一般 UI 组件、卡片、列表项
StatefulWidget 简单 有状态组件、交互组件
CustomPainter 中等 图表、特效、自绑图形
RenderObject/RenderBox 复杂 最高 完全自定义布局、高性能场景
CustomLayout 中等 自定义布局规则
Sliver 中等 高级滚动效果、复杂头部
CustomClipper 简单 非矩形裁剪、装饰效果
Shader 复杂 中等 视觉特效、渐变效果
PlatformView 复杂 中等 嵌入原生组件
ImplicitlyAnimatedWidget 中等 简单动画组件

1. Widget 组合 (Composition)

概述

最简单、最常用的自定义组件方式。通过组合 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

核心 API

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)),
                    ]),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

2. StatefulWidget

概述

用于创建拥有可变状态的组件。通过 setState() 触发 UI 重建,管理组件的生命周期和内部状态。

使用场景

场景 示例 状态类型
计数器 购物车数量选择器 数值状态
颜色选择器 主题颜色配置 选中颜色状态
动画组件 加载动画、过渡动画 AnimationController
表单输入 搜索框、输入验证 TextEditingController

实现流程

1. 创建 StatefulWidget 类 → 实现 createState()
2. 创建对应的 State<T> 类
3. initState() → 初始化状态、创建控制器
4. build() → 构建 UI,使用状态变量
5. setState() → 触发 UI 更新
6. dispose() → 清理资源、释放控制器

核心 API

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)),
          ],
        ),
      ],
    );
  }
}

3. CustomPainter

概述

使用 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

核心 API

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 / stroke
  • strokeWidth - 线宽
  • 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,
  ),
)

4. RenderObject/RenderBox

概述

Flutter 渲染层的最底层定制方式,拥有对布局和绑制的完全控制权。适合实现标准布局 Widget 无法实现的复杂布局逻辑。

使用场景

场景 示例 特点
圆形布局 环形菜单、时钟刻度 子组件排列成圆形
螺旋布局 相册展示、装饰效果 阿基米德螺线排列
对角线布局 创意卡片、图片墙 斜向排列
波浪布局 音频可视化、装饰元素 正弦波排列
瀑布流 Pinterest 风格布局 高性能不等高列表

实现流程

1. Widget 层:继承 SingleChildRenderObjectWidget / MultiChildRenderObjectWidget
2. 实现 createRenderObject() 创建 RenderBox
3. 实现 updateRenderObject() 更新属性
4. RenderBox 层:继承 RenderBox
5. 实现 setupParentData() 设置子组件布局信息
6. 实现 performLayout() 核心布局逻辑
7. 实现 paint() 绑制子组件
8. 实现 hitTestChildren() 点击测试

核心 API

// 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]),
  ],
)

5. CustomLayout

概述

比 RenderBox 更高层、更简单的自定义布局方式。只关注布局逻辑,不需要处理绑制和点击测试。

使用场景

场景 示例 Delegate 类型
径向菜单 环形展开的操作菜单 MultiChildLayoutDelegate
仪表板 多个面板的复杂布局 MultiChildLayoutDelegate
居中偏移 带偏移的居中布局 SingleChildLayoutDelegate
跟随指针 组件跟随手指位置 SingleChildLayoutDelegate

实现流程

1. 选择 CustomSingleChildLayout 或 CustomMultiChildLayout
2. 继承对应的 Delegate 类
3. 实现 performLayout(Size size) 方法
4. 使用 layoutChild() 测量子组件大小
5. 使用 positionChild() 定位子组件
6. 实现 shouldRelayout() 优化性能

核心 API

// 单子组件布局
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)),
  ],
)

6. Sliver

概述

用于构建高性能滚动效果的组件,如可伸缩头部、固定头部、懒加载列表等。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 中使用

核心 API

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,
      ),
    ),
  ],
)

7. CustomClipper

概述

用于创建非矩形的裁剪区域,实现波浪形、心形、星形等特殊形状的裁剪效果。

使用场景

场景 示例 形状
波浪裁剪 页面底部装饰、卡片边缘 贝塞尔曲线波浪
心形裁剪 头像框、点赞动画 心形路径
星形裁剪 评分显示、徽章 多角星路径
票券裁剪 优惠券、电影票 锯齿边缘
对角线裁剪 创意卡片、图片展示 斜线裁剪

实现流程

1. 继承 CustomClipper<Path>
2. 实现 getClip(Size size) 返回裁剪路径
3. 使用 Path API 构建形状
4. 实现 shouldReclip() 优化性能
5. 使用 ClipPath 应用裁剪

核心 API

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')),
  ),
)

8. Shader

概述

使用着色器实现高级视觉效果,包括渐变、模糊、自定义 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 应用到子组件

核心 API

// 渐变类
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;
}

9. PlatformView

概述

将原生平台的 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 中注册

核心 API

// 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'));
  }
}

10. ImplicitlyAnimatedWidget

概述

创建隐式动画组件,当属性值变化时自动执行动画过渡。无需手动管理 AnimationController,代码更简洁。

使用场景

场景 示例 动画属性
颜色过渡 主题切换、状态变化 Color
尺寸动画 展开/收起、缩放效果 double (size)
位置动画 列表排序、拖拽反馈 Offset / Alignment
圆角动画 卡片展开、按钮变形 BorderRadius
渐变动画 背景渐变变化 Gradient

实现流程

1. 继承 ImplicitlyAnimatedWidget
2. 定义需要动画的属性
3. 创建 State 类,继承 AnimatedWidgetBaseState
4. 实现 forEachTween() 为每个属性创建 Tween
5. 在 build() 中使用 tween.evaluate(animation) 获取当前值

核心 API

// 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

学习建议

  1. 入门阶段:从 Widget 组合 和 StatefulWidget 开始
  2. 进阶阶段:学习 CustomPainter 和 CustomClipper
  3. 高级阶段:深入 RenderObject 和 Sliver
  4. 专项学习:根据需求学习 Shader 和 PlatformView

参考资源


License: MIT