Skip to content

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

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

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages