Flutter 展开收起:FadeInOutWidgets 的三种实现
在 Flutter 里做「显示 / 隐藏一块内容」时,常见需求是:收起时高度变为 0,展开时恢复自然高度;有时还希望 淡入淡出。本仓库 lib/ui/comview/FadeInOutWidgets.dart 里提供了三个无状态组件,按复杂度递增,可按场景选用。
设计要点
- 外层统一用
ClipRect,避免子树在高度动画过程中在可视区域外绘制出多余内容。 visible == false时子节点用SizedBox.shrink()占位,配合动画组件把高度收到 0。- 默认
duration多为 280ms(第三版为 250ms),曲线多为Curves.easeInOut。
一、FadeInOutWidget:只做高度动画(优先使用)
注释写明:基础版,只做高度动画(从 0 到 child 自然高度),优先使用,性能最好。
实现上是 AnimatedSize + SizedBox(width: double.infinity),子节点在 visible 为真时放 child,否则 SizedBox.shrink()。没有透明度动画,因此 重绘范围相对更小,列表或频繁切换时更省。
适合:只需要「滑开 / 收起」占位高度,不需要淡入淡出。
import 'package:flutter/material.dart';
/// 基础版:只做高度动画(从 0 到 child 自然高度)
/// 优先使用这个,性能最好
class FadeInOutWidget extends StatelessWidget {
final Widget child;
final bool visible;
final Duration duration;
const FadeInOutWidget({
super.key,
required this.child,
required this.visible,
this.duration = const Duration(milliseconds: 280),
});
@override
Widget build(BuildContext context) {
return ClipRect(
child: AnimatedSize(
duration: duration,
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: SizedBox(
width: double.infinity,
child: visible ? child : const SizedBox.shrink(),
),
),
);
}
}
二、FadeInOutWidget2:高度 + 透明度
在上一版基础上,中间包了一层 AnimatedOpacity:opacity 随 visible 在 0 与 1 之间切换,展开和收起都会有过渡。
适合:希望隐藏时不仅高度收起,还有明显的淡出;展示时也有淡入。代价是比纯 AnimatedSize 多一层透明度动画。
/// 高度 + 透明度:展开/收起都有淡入淡出效果
class FadeInOutWidget2 extends StatelessWidget {
final Widget child;
final bool visible;
final Duration duration;
const FadeInOutWidget2({
super.key,
required this.child,
required this.visible,
this.duration = const Duration(milliseconds: 280),
});
@override
Widget build(BuildContext context) {
return ClipRect(
child: AnimatedSize(
duration: duration,
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: SizedBox(
width: double.infinity,
child: AnimatedOpacity(
duration: duration,
curve: Curves.easeInOut,
opacity: visible ? 1 : 0,
child: visible ? child : const SizedBox.shrink(),
),
),
),
);
}
}
三、FadeInOutWidget3:AnimatedSwitcher 的「组件切换」感
这一版改用 AnimatedSwitcher,自定义 transitionBuilder:同时 FadeTransition 与 SizeTransition(sizeFactor 使用同一 Animation<double>),axisAlignment: -1.0 表示 从上往下展开。
child 在显示时用 KeyedSubtree + ValueKey('ql_fade_in_out_show'),隐藏时用带 ValueKey('ql_fade_in_out_hide') 的 SizedBox.shrink(),保证 每次 show/hide 切换都会触发动画。
适合:更强调「两个状态之间的切换动画」,而不是单纯同一棵子树的高度变化;交互上更接近「整块内容被换掉」的感觉。
/// 使用 AnimatedSwitcher:更像“组件切换”的进入/退出动画
class FadeInOutWidget3 extends StatelessWidget {
final Widget child;
final bool visible;
final Duration duration;
const FadeInOutWidget3({
super.key,
required this.child,
required this.visible,
this.duration = const Duration(milliseconds: 250),
});
@override
Widget build(BuildContext context) {
return ClipRect(
child: AnimatedSwitcher(
duration: duration,
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0, // 从上往下展开
child: child,
),
);
},
child: visible
? KeyedSubtree(
// 确保每次 show 切换都会触发动画
key: const ValueKey('ql_fade_in_out_show'),
child: child,
)
: const SizedBox.shrink(
key: ValueKey('ql_fade_in_out_hide'),
),
),
);
}
}
调用示例
FadeInOutWidget(
visible: _expanded,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('展开后可见的内容'),
),
)
将 FadeInOutWidget 换成 FadeInOutWidget2 / FadeInOutWidget3 即可对比三种效果(参数一致:child、visible、duration 可选)。
如何选择
| 组件 | 动画内容 | 建议场景 |
|---|---|---|
FadeInOutWidget |
仅高度 | 默认首选,性能最好 |
FadeInOutWidget2 |
高度 + 透明度 | 需要淡入淡出 |
FadeInOutWidget3 |
Fade + Size(Switcher) | 需要切换感、或自定义过渡 |