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

简介

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理