写点什么

flutter 系列之: 做一个会飞的菜单

作者:程序那些事
  • 2023-06-06
    广东
  • 本文字数:3258 字

    阅读完需:约 11 分钟

简介

flutter 中自带了 drawer 组件,可以实现通用的菜单功能,那么有没有一种可能,我们可以通过自定义动画来实现一个别样的菜单呢?


答案是肯定的,一起来看看吧。

定义一个菜单项目

因为这里的主要目的是实现菜单的动画,所以这里的菜单比较简单,我们的 menu 是一个 StatefulWidget,里面就是一个 Column 组件,column 中有四行诗:


  static const _menuTitles = [    '迟日江山丽',    '春风花草香',    '泥融飞燕子',    '沙暖睡鸳鸯',  ];
Widget build(BuildContext context) { return Container( color: Colors.white, child:_buildContent() ); }

Widget _buildContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), ..._buildListItems() ], ); }
List<Widget> _buildListItems() { final listItems = <Widget>[]; for (var i = 0; i < _menuTitles.length; ++i) { listItems.add( Padding( padding: const EdgeInsets.symmetric(horizontal: 36.0, vertical: 16), child: Text( _menuTitles[i], textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w500, ), ), ) ); } return listItems; }
复制代码

让 menu 动起来

怎么让 menu 动起来呢?我们需要给最外层的 AnimateMenuApp 添加一个 AnimationController,所以需要在_AnimateMenuAppState 添加 SingleTickerProviderStateMixin 的 mixin,如下所示:


class _AnimateMenuAppState extends State<AnimateMenuApp>    with SingleTickerProviderStateMixin {  late AnimationController _drawerSlideController;
复制代码


然后在 initState 中对_drawerSlideController 进行初始化:


  void initState() {    super.initState();
_drawerSlideController = AnimationController( vsync: this, duration: const Duration(milliseconds: 150), ); }
复制代码


在让 menu 动起来之前,我们需要设计一下动画的样式。假如我们的动画是让 menu 从右向左飞出。那么我们可以使用 FractionalTranslation 来进行 offset 进行位置变换。


并且当菜单没有开启的时候,我们需要显示一个空的组件,这里用 SizedBox 来替代。


当菜单开启的时候,就执行这个 FractionalTranslation 的动画,所以我们的 build 方法需要这样写:


  Widget _buildDrawer() {    return AnimatedBuilder(      animation: _drawerSlideController,      builder: (context, child) {        return FractionalTranslation(          translation: Offset(1.0 - _drawerSlideController.value, 0.0),          child: _isDrawerClosed() ? const SizedBox() : const Menu(),        );      },    );  }
复制代码


FractionalTranslation 中的 Offset 是根据_drawerSlideController 的 value 来进行变化的。


那么_drawerSlideController 的 value 怎么变化呢?


我们定义一个_toggleDrawer 方法,在点击菜单按钮的时候来触发这个方法,从而实现_drawerSlideController 的 value 变化:


  void _toggleDrawer() {    if (_isDrawerOpen() || _isDrawerOpening()) {      _drawerSlideController.reverse();    } else {      _drawerSlideController.forward();    }  }
复制代码


同时,我们定义下面几个判断菜单状态的方法:


  bool _isDrawerOpen() {    return _drawerSlideController.value == 1.0;  }
bool _isDrawerOpening() { return _drawerSlideController.status == AnimationStatus.forward; }
bool _isDrawerClosed() { return _drawerSlideController.value == 0.0; }
复制代码


因为菜单图标需要根据菜单状态来发生改变,菜单的状态又是依赖于_drawerSlideController,所以,我们把 IconButton 放到一个 AnimatedBuilder 里面,从而实现动态变化的效果:


  PreferredSizeWidget _buildAppBar() {    return AppBar(      title: const Text(        '动画菜单',        style: TextStyle(          color: Colors.black,        ),      ),      backgroundColor: Colors.transparent,      elevation: 0.0,      automaticallyImplyLeading: false,      actions: [        AnimatedBuilder(          animation: _drawerSlideController,          builder: (context, child) {            return IconButton(              onPressed: _toggleDrawer,              icon: _isDrawerOpen() || _isDrawerOpening()                  ? const Icon(                Icons.clear,                color: Colors.black,              )                  : const Icon(                Icons.menu,                color: Colors.black,              ),            );          },        ),      ],    );  }
复制代码


最后实现的效果如下:


添加菜单内部的动画

上面的例子中整个菜单是作为一个整体来动画的,有没有可能菜单里面的每一个 item 也有自己的动画呢?


答案当然是肯定的。


我们只需要在上面的基础上将 menu 组件添加动画支持即可:


class _MenuState extends State<Menu> with SingleTickerProviderStateMixin
复制代码


动画中的位移我们选择使用 Transform.translate,同时还添加了淡入淡出的效果,也就是把上面例子中的 Padding 用 AnimatedBuilder 包裹起来,如下所示:


  List<Widget> _buildListItems() {    final listItems = <Widget>[];    for (var i = 0; i < _menuTitles.length; ++i) {      listItems.add(        AnimatedBuilder(          animation: _itemController,          builder: (context, child) {            final animationPercent = Curves.easeOut.transform(              _itemSlideIntervals[i].transform(_itemController.value),            );            final opacity = animationPercent;            final slideDistance = (1.0 - animationPercent) * 150;
return Opacity( opacity: opacity, child: Transform.translate( offset: Offset(slideDistance, 0), child: child, ), ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 36.0, vertical: 16), child: Text( _menuTitles[i], textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w500, ), ), ), ), ); } return listItems; }
复制代码


AnimatedBuilder 中的 builder 返回的是一个 Opacity 对象,里面包含了 opacity 和 child 两个属性。其中最终要的一个变化值是 animationPercent,这个值是根据_itemController 的 value 和初始设置的各个 item 的变化时间来决定的。


每个 item 的值是不一样的:


  void _createAnimationIntervals() {    for (var i = 0; i < _menuTitles.length; ++i) {      final startTime = _initialDelayTime + (_staggerTime * i);      final endTime = startTime + _itemSlideTime;      _itemSlideIntervals.add(        Interval(          startTime.inMilliseconds / _animationDuration.inMilliseconds,          endTime.inMilliseconds / _animationDuration.inMilliseconds,        ),      );    }  }
复制代码


最后运行结果如下:


总结

在 flutter 中一切皆可动画,我们只需要掌握动画创作的诀窍即可。


本文的例子:https://github.com/ddean2009/learn-flutter.git

发布于: 刚刚阅读数: 3
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020-06-07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
flutter系列之:做一个会飞的菜单_flutter_程序那些事_InfoQ写作社区