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:高度 + 透明度

在上一版基础上,中间包了一层 AnimatedOpacityopacityvisible 在 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:同时 FadeTransitionSizeTransitionsizeFactor 使用同一 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 即可对比三种效果(参数一致:childvisibleduration 可选)。

如何选择

组件 动画内容 建议场景
FadeInOutWidget 仅高度 默认首选,性能最好
FadeInOutWidget2 高度 + 透明度 需要淡入淡出
FadeInOutWidget3 Fade + Size(Switcher) 需要切换感、或自定义过渡