底部导航栏滑动动画与形变效果

December 27, 2025
2 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

日期

2025-12-27

背景

我更希望底部四个界面(文件、传输、发送、设置)支持左右滑动切换,并且激活的按钮有圆形背景,圆球在切换时能滑动、变色并带有拉伸形变效果。

实现内容

1. PageView 替代 IndexedStack

原来使用 IndexedStack 只能通过点击切换,改为 PageView 支持手势滑动:

body: PageView(
  controller: _pageController,
  onPageChanged: _onPageChanged,
  children: [
    FilesPage(onGoToSettings: () => _goToPage(3)),
    const TransfersPage(),
    const SendPage(),
    const SettingsPage(),
  ],
),

2. 自定义底部导航栏

放弃系统 BottomNavigationBar,使用 Stack + Positioned 实现自定义导航栏,以便精确控制圆形指示器的位置和形变。

3. 圆球滑动与变色动画

使用 AnimationController 配合 Tween 实现:

// 位置动画
_positionAnim = Tween<double>(
  begin: _prevIndex.toDouble(),
  end: _navIndex.toDouble(),
).animate(CurvedAnimation(parent: _animController, curve: Curves.easeOut));
 
// 颜色动画
_colorAnim = ColorTween(
  begin: _navColors[_prevIndex],
  end: _navColors[_navIndex],
).animate(CurvedAnimation(parent: _animController, curve: Curves.easeOut));

4. 拉伸形变效果(Squash & Stretch)

使用 sin(progress * π) 曲线计算形变量,在动画中间达到最大拉伸:

// 计算形变量:使用 sin 曲线,在动画中间拉伸最大
final progress = animController.value;
final stretch = math.sin(progress * math.pi) * maxStretch; // maxStretch = 20
final currentWidth = circleSize + stretch; // 44 → 64 → 44
 
// 使用 borderRadius 保持圆角端点
decoration: BoxDecoration(
  color: color,
  borderRadius: BorderRadius.circular(circleSize / 2),
),

效果

  • 动画开始:圆形(44px)
  • 动画中间:最大拉伸成椭圆(64px)
  • 动画结束:恢复圆形(44px)

5. 细节修复

  • SafeArea:使用 SafeArea(top: false) 处理全面屏手势条区域
  • 图标居中:图标区域使用 EdgeInsets.only(top: 8) 与圆形背景的 top: 8 保持一致
  • 溢出修复:高度从 70 调整为 72,间距和字号微调

关键代码结构

_HomePageState (with SingleTickerProviderStateMixin)
├── PageController // 控制页面滑动
├── AnimationController // 控制圆球动画
├── Animation<double> // 位置动画
├── Animation<Color?> // 颜色动画
└── _AnimatedBottomNav // 自定义底部导航组件
├── Stack
│ ├── Positioned // 滑动圆形背景(带形变)
│ └── Row // 图标和标签
└── SafeArea // 安全区域

动画参数

  • 动画时长:250ms
  • 动画曲线:Curves.easeOut
  • 最大拉伸量:20px
  • 圆形大小:44px
  • 导航栏高度:72px

主题色

页面颜色
文件Colors.amber
传输Colors.blue[800]
发送Colors.green
设置Colors.redAccent

参考资源

涉及文件

  • lib/ui/home_page.dart - 主页面和底部导航栏实现