Let animation to achieve a more simple, Flutter simple animated tutorial!

Author: Didier Boelens
description link: www.didierboelens.com/2018/06/ani...
translation: hcc

Flutter animations powerful and easy to use. Followed by a specific example, you will learn everything about Flutter animation.

Difficulty: Intermediate

Today, we can not imagine a mobile application which does not have any animation when you jump from one page to another page, or click on a button (such as InkWell) ... there will be a movie. Animation everywhere.

Flutter make the animation is very easy to implement.

In short, this article is to discuss this topic, even though only an expert can talk about before, in order to make the article look more attractive, I will challenge it, modeled Vitaly Rubtsov in Dribble uploaded a "Guillotine Menu (chopped head menu) "is a animation with Flutter achieve this effect step by step.

image.png

The first part of this article will introduce the main concepts and theoretical knowledge, the second part will be to achieve the animation above.

The three core animation

In order to achieve animation effects, you must provide the following three elements:

  • Ticker
  • Animation
  • AnimationController

Here are several of these elements to make a simple introduction, a more detailed explanation later.

Ticker

In simple terms, Ticker in this class will be a regular time intervals (about 60 times per second), to send a signal, you have to think of this watch, ticking every second turn.

When Ticker start, since the beginning of the arrival of the first tick, callback method to each of the tick will Ticker callback.

IMPORTANT
Although all ticker may be started at different times, but they always perform in a synchronized manner, which for some synchronization animation is very useful.

Animation

Animation is actually nothing special, but is a possible animated value as a life cycle changes (specific type), with changes in the value of the animation time changes the way can be linear (eg 1, 2, 3,4,5 ...), it may be more complicated (refer to "curves curve" below).

AnimationController

AnimationController is one can control one or more animations (start, end, repeat) controller. In other words, it allows Animation above said values ​​within a specified time, in accordance with a speed change from a minimum to a maximum.

AnimationController class presentation

Such can control the animation. To be more precise, I would rather say "Control scene" because later on we will see several different animations can be controlled by the same controller ......

Therefore, this AnimationController class, we can:

  • Start a child animation, forward or reverse playback
  • Stop a child animation
  • A specific sub-set of animation values
  • Boundary definition animation values

The following pseudo-code shows the class which can be different from the initialization parameter

AnimationController controller = new AnimationController(
    value:      // the current value of the animation, usually 0.0 (= default)
    lowerBound: // the lowest value of the animation, usually 0.0 (= default)
    upperBound: // the highest value of the animation, usually 1.0 (= default)
    duration:   // the total duration of the whole animation (scene)
    vsync:      // the ticker provider
    debugLabel: // a label to be used to identify the controller
            // during debug session
);
复制代码

In most cases, it does not design to value, lowerBound, upperBound and debugLabel initialization AnimationController.

How AnimationController bound to the Ticker

To make the animation work, AnimationController must be bound to the Ticker.

Under normal circumstances, you can generate a Ticker bound to a StatefulWidget on the instance.

class _MyStateWidget extends State<MyStateWidget>
        with SingleTickerProviderStateMixin {
    AnimationController _controller;

    @override
    void initState(){
      super.initState();
      _controller = new AnimationController(
        duration: const Duration(milliseconds: 1000), 
        vsync: this,
      );
    }

    @override
    void dispose(){
      _controller.dispose();
      super.dispose();
    }

    ...
}
复制代码
  • Line 2 This line tells Flutter, you want a single Ticker, Ticker link this to the MyStateWidget instance.

  • 8-10 row

Initialize the controller. The total duration of the scene (child animation) is set to 1000 milliseconds, and bind to the Ticker (vsync: this).

Implicit parameters: lowerBound = 0.0 and upperBound = 1.0

  • 16 lines

Very important, when an instance MyStateWidget destruction of this page, you need to release the controller.

TickerProviderStateMixin 还是 SingleTickerProviderStateMixin?

If you have a few Animation Controller under the circumstances, you want to have different Ticker, only need to replace SingleTickerProviderStateMixin is TickerProviderStateMixin.

Well, I have to bind the controller to the Ticker, but does it work?

It is because TICKER, will generate about 60 per tick, will AnimationController given time, according to the value of the linear tick is generated between the minimum and maximum values.

Examples of values ​​produced in this 1000 milliseconds follows:

image.png

We see the values ​​vary from 1.0 to 1000 ms in 0.0 (lowerBound) (upperBound). Generating 51 different values.

Let us extend the code to see how to use it.

class _MyStateWidget extends State<MyStateWidget>
        with SingleTickerProviderStateMixin {
    AnimationController _controller;

    @override
    void initState(){
      super.initState();
      _controller = new AnimationController(
        duration: const Duration(milliseconds: 1000), 
        vsync: this,
      );
      _controller.addListener((){
          setState((){});
      });
      _controller.forward();
    }

    @override
    void dispose(){
      _controller.dispose();
      super.dispose();
    }

    @override
    Widget build(BuildContext context){
        final int percent = (_controller.value * 100.0).round();
        return new Scaffold(
            body: new Container(
                child: new Center(
                    child: new Text('$percent%'),
                ),
            ),
        );
    }
}

复制代码
  • This line tells the controller 12 rows, each time its value changes, we need to rebuild Widget (by setState ())

  • Line 15

After Widget initialization is complete, we tell the controller starts counting (forward () -> from lowerBound to upperBound)

  • Line 26

We retrieve the value of the controller (_controller.value), and in this example, the range of this value is 0.0 to 1.0 (i.e., 0% to 100%), we get this percentage integer expression, which is displayed on the page center of.

The concept of animation

As we have seen, controller can return small values ​​different from each other in a linear manner.

Sometimes we may have other needs such as:

  • Using other types of values, e.g. Offset, int ...
  • Not range from 0.0 to 1.0
  • Other types of changes to be considered other than a linear change some effect

Use other value types

To be able to use other value types, Animation class using templates.

In other words, you can define:

Animation<int> integerVariation;
Animation<double> decimalVariation;
Animation<Offset> offsetVariation;
复制代码

Using different numerical ranges

Sometimes, we want to use a different range, rather than the 0.0 and 1.0.

In order to define such a range, we will use the Tween class.

To illustrate this point, let us consider a situation where you want the perspective of the situation from 0 to π / 2 changes.

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2);

复制代码

Change type

如前所述,将默认值从 lowerBound 变化到 upperBound 的默认方式是线性的,controller 就是这么控制的。

如果要使角度从0到π/ 2 弧度线性变化,请将 Animation 绑定到AnimationController:

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2).animate(_controller);
复制代码

当您开始动画(通过_controller.forward())时,angleAnimation.value 将使用 _controller.value 来获取 范围[0.0; π/ 2] 中的值。

下图显示了这种线性变化(π/ 2 = 1.57)

image.png

使用Flutter预定义的曲线变化

Flutter 提供了一组预定义的 Curved 变化,如下:

image.png

要使用这些曲线效果:

Animation<double> angleAnimation = new Tween(begin: 0.0, end: pi/2).animate(
    new CurvedAnimation(
        parent: _controller,
        curve:  Curves.ease,
        reverseCurve: Curves.easeOut
    ));
复制代码

这将产生值[0; π/ 2] 之间的值:

  • 当正向播放动画,数值从 0 到 π/2 ,会使用 Curves.ease 效果
  • 当反向播放动画,数值从 π/2 到 0,会使用 Curves.easeOut 效果

控制动画

该AnimationController 类可以让你通过 API 来控制动画。(以下是最常用的API):

  • _controller.forward({两个区间的值})

要求控制器开始生成 lowerBound- > upperBound中的值

from 的可选参数可用于强制控制器从lowerBound之外的另一个值开始“ 计数 ”

  • _controller.reverse({两个区间的值})

要求控制器开始生成 upperBound- > lowerBound中的值

from的可选参数可用于强制控制器从“ upperBound ”之外的另一个值开始“ 计数 ”

  • _controller.stop({bool cancelled:true})

停止运行动画

  • _controller.reset()

将动画重置为从 LowerBound 开始

  • _controller.animateTo(double target, { Duration duration, Curve curve: Curves.linear })

将动画的当前值改变到目标值。

  • _controller.repeat({double min,double max,Duration period})

开始以正向运行动画,并在动画完成后重新启动动画。如果定义了 min 或者 max ,将限制动画的重复执行次数。

安全起见

由于动画可能会意外停止(例如关闭屏幕),因此在使用以下API之一时,添加“ .orCancel ” 更为安全:

__controller.forward().orCancel;
复制代码

这个小技巧,可以保证,在 _controller 释放之前,如果 Ticker 取消了,将不会导致异常。

场景的概念

官方文档中不存在“ 场景 ”一词,但就我个人而言,我发现它更接近现实。我来解释一下。

如我所说,一个 AnimationController 管理一个Animation。但是,我们可能将“ 动画 ” 一词理解为一系列需要依次播放或重叠播放的子动画。将子动画组合在一起,这就是我所说的“ 场景 ”。

考虑以下情况,其中动画的整个持续时间为10秒,我们希望达到的效果是:

  • 在开始的2秒内,有一个球从屏幕的左侧移动到屏幕的中间
  • 然后,同一个球需要3秒钟才能从屏幕中心移动到屏幕顶部中心
  • 最终,球需要5秒钟才能消失。 正如您最可能已经想到的那样,我们必须考虑3种不同的动画:

///
/// Definition of the _controller with a whole duration of 10 seconds
///
AnimationController _controller = new AnimationController(
    duration: const Duration(seconds: 10), 
    vsync: this
);

///
/// First animation that moves the ball from the left to the center
///
Animation<Offset> moveLeftToCenter = new Tween(
    begin: new Offset(0.0, screenHeight /2), 
    end: new Offset(screenWidth /2, screenHeight /2)
).animate(_controller);

///
/// Second animation that moves the ball from the center to the top
///
Animation<Offset> moveCenterToTop = new Tween(
    begin: new Offset(screenWidth /2, screenHeight /2), 
    end: new Offset(screenWidth /2, 0.0)
).animate(_controller);

///
/// Third animation that will be used to change the opacity of the ball to make it disappear
///
Animation<double> disappear = new Tween(
    begin: 1.0, 
    end: 0.0
).animate(_controller);
复制代码

现在的问题是,我们如何链接(或编排)子动画?

Interval

组合动画可以通过 Interval 这个类来实现。但是,那什么是 Interval?

可能和我们脑子里首先想到的不一样, Interval 和时间没有关系,而是一组值的范围。

如果考虑使用 _controller,则必须记住,它会使值从 lowerBound 到 upperBound 变化。

通常,这两个值基本定义为 lowerBound = 0.0 和 upperBound = 1.0,这使动画计算更容易,因为[0.0-> 1.0]只是从0%到100%的变化。因此,如果一个场景的总持续时间为10秒,则最有可能在5秒后,相应的_controller.value将非常接近0.5(= 50%)。

如果将3个不同的动画放在一个时间轴上,则可以获得如下示意图:

image.png

如果现在考虑值的间隔,则对于3个动画中的每个动画,我们将得到:

  • moveLeftToCenter

持续时间:2秒,从0秒开始,以2秒结束=>范围= [0; 2] =>百分比:从整个场景的0%到20%=> [0.0; 0.20]

  • moveCenterToTop

持续时间:3秒,开始于2秒,结束于5秒=>范围= [2; 5] =>百分比:从整个场景的20%到50%=> [0.20; 0.50]

  • disappear

持续时间:5秒,开始于5秒,结束于10秒=>范围= [5; 10] =>百分比:从整个场景的50%到100%=> [0.50; 1.0]

现在我们有了这些百分比,我们得到每个动画的定义,如下:


///
/// Definition of the _controller with a whole duration of 10 seconds
///
AnimationController _controller = new AnimationController(
    duration: const Duration(seconds: 10), 
    vsync: this
);

///
/// First animation that moves the ball from the left to the center
///
Animation<Offset> moveLeftToCenter = new Tween(
    begin: new Offset(0.0, screenHeight /2), 
    end: new Offset(screenWidth /2, screenHeight /2)
    ).animate(
            new CurvedAnimation(
                parent: _controller,
                curve:  new Interval(
                    0.0,
                    0.20,
                    curve: Curves.linear,
                ),
            ),
        );

///
/// Second animation that moves the ball from the center to the top
///
Animation<Offset> moveCenterToTop = new Tween(
    begin: new Offset(screenWidth /2, screenHeight /2), 
    end: new Offset(screenWidth /2, 0.0)
    ).animate(
            new CurvedAnimation(
                parent: _controller,
                curve:  new Interval(
                    0.20,
                    0.50,
                    curve: Curves.linear,
                ),
            ),
        );

///
/// Third animation that will be used to change the opacity of the ball to make it disappear
///
Animation<double> disappear = new Tween(begin: 1.0, end: 0.0)
        .animate(
            new CurvedAnimation(
                parent: _controller,
                curve:  new Interval(
                    0.50,
                    1.0,
                    curve: Curves.linear,
                ),
            ),
        );
复制代码

这就是定义场景(或一系列动画)所需的全部设置。当然,没有什么可以阻止您重叠子动画…

响应动画状态

有时,获取动画(或场景)的状态很方便。

动画可能具有4种不同的状态:

  • dismissed:动画在开始后停止(或尚未开始)
  • forward:动画从头到尾运行
  • reverse:动画反向播放
  • completed:动画在播放后停止

要获得此状态,我们需要通过以下方式监听动画状态的变化:

   myAnimation.addStatusListener((AnimationStatus status){
       switch(status){
           case AnimationStatus.dismissed:
               ...
               break;

           case AnimationStatus.forward:
               ...
               break;

           case AnimationStatus.reverse:
               ...
               break;

           case AnimationStatus.completed:
               ...
               break;
       }
   });

复制代码

状态应用的典型示例就是状态的切换。例如,动画完成后,我们要反转它,如:

  myAnimation.addStatusListener((AnimationStatus status){
      switch(status){
          ///
          /// When the animation is at the beginning, we force the animation to play
          ///
          case AnimationStatus.dismissed:
              _controller.forward();
              break;

          ///
          /// When the animation is at the end, we force the animation to reverse
          ///
          case AnimationStatus.completed:
              _controller.reverse();
              break;
      }
  });
复制代码

理论已经足够了,现在我们开始实战

我在文章开头提到了一个动画,现在我准备开始实现它,名字就叫“guillotine(断头台)”

动画分析及程序初始化

未来能够实现“斩头台”效果,我们需要考虑一下几个方面:

  • 页面内容本身
  • 当我们点击菜单图标时,菜单栏会旋转
  • 旋转时,菜单会覆盖页面内容并填充整个视口
  • 一旦菜单是完全可见,我们再次点击图标,菜单旋转出来,以便回到原来的位置和尺寸

从这些观察中,我们可以立即得出结论,我们没有使用带有AppBar的普通Scaffold(因为后者是固定的)。

我们需要使用 2 层 Stack:

  • 页面内容(下层)
  • 菜单(上层)

程序的基本框架基本出来了:

class MyPage extends StatefulWidget {
    @override
    _MyPageState createState() => new _MyPageState();
}

class _MyPageState extends State<MyPage>{
  @override
  Widget build(BuildContext context){
      return SafeArea(
        top: false,
        bottom: false,
        child: new Container(
          child: new Stack(
            alignment: Alignment.topLeft,
            children: <Widget>[
              new Page(),
              new GuillotineMenu(),
            ],
          ),
        ),
      );
  }
}

class Page extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        return new Container(
            padding: const EdgeInsets.only(top: 90.0),
            color: Color(0xff222222),
        );
    }
}

class GuillotineMenu extends StatefulWidget {
    @override
    _GuillotineMenuState createState() => new _GuillotineMenuState();
}

class _GuillotineMenuState extends State<GuillotineMenu> {

    @overrride
    Widget build(BuildContext context){
        return new Container(
            color: Color(0xff333333),
        );
    }
}

复制代码

这些代码的运行结果为黑屏,仅显示覆盖整个视口的GuillotineMenu。

菜单效果分析

如果你看了上面的示例,可以看到菜单完全打开时,它完全覆盖了视口。打开后,只有可见的AppBa。

而如果最初旋转 GuillotineMenu 并在按下菜单按钮时将其旋转π/ 2,将会怎样呢,如下图所示这样吗?

image.png

然后,我们可以按以下方式重写_GuillotineMenuState类:(这里不在解释如何布局,这不是重点)


class _GuillotineMenuState extends State<GuillotineMenu> {
   double rotationAngle = 0.0;

    @override
    Widget build(BuildContext context){
        MediaQueryData mediaQueryData = MediaQuery.of(context);
        double screenWidth = mediaQueryData.size.width;
        double screenHeight = mediaQueryData.size.height;

        return new Transform.rotate(
                angle: rotationAngle,
                origin: new Offset(24.0, 56.0),
                alignment: Alignment.topLeft,
                child: Material(
                    color: Colors.transparent,
                    child: Container(
                    width: screenWidth,
                    height: screenHeight,
                    color: Color(0xFF333333),
                    child: new Stack(
                        children: <Widget>[
                            _buildMenuTitle(),
                            _buildMenuIcon(),
                            _buildMenuContent(),
                        ],
                    ),
                ),
            ),
        );
    }

    ///
    /// Menu Title
    ///
    Widget _buildMenuTitle(){
        return new Positioned(
            top: 32.0,
            left: 40.0,
            width: screenWidth,
            height: 24.0,
            child: new Transform.rotate(
                alignment: Alignment.topLeft,
                origin: Offset.zero,
                angle: pi / 2.0,
                child: new Center(
                child: new Container(
                    width: double.infinity,
                    height: double.infinity,
                    child: new Opacity(
                    opacity: 1.0,
                    child: new Text('ACTIVITY',
                        textAlign: TextAlign.center,
                        style: new TextStyle(
                            color: Colors.white,
                            fontSize: 20.0,
                            fontWeight: FontWeight.bold,
                            letterSpacing: 2.0,
                        )),
                    ),
                ),
            )),
        );
    }

    ///
    /// Menu Icon
    /// 
    Widget _buildMenuIcon(){
        return new Positioned(
            top: 32.0,
            left: 4.0,
            child: new IconButton(
                icon: const Icon(
                    Icons.menu,
                    color: Colors.white,
                ),
                onPressed: (){},
            ),
        );
    }

    ///
    /// Menu content
    ///
    Widget _buildMenuContent(){
        final List<Map> _menus = <Map>[
            {
            "icon": Icons.person,
            "title": "profile",
            "color": Colors.white,
            },
            {
            "icon": Icons.view_agenda,
            "title": "feed",
            "color": Colors.white,
            },
            {
            "icon": Icons.swap_calls,
            "title": "activity",
            "color": Colors.cyan,
            },
            {
            "icon": Icons.settings,
            "title": "settings",
            "color": Colors.white,
            },
        ];

        return new Padding(
            padding: const EdgeInsets.only(left: 64.0, top: 96.0),
            child: new Container(
                width: double.infinity,
                height: double.infinity,
                child: new Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: _menus.map((menuItem) {
                        return new ListTile(
                            leading: new Icon(
                            menuItem["icon"],
                            color: menuItem["color"],
                            ),
                            title: new Text(
                            menuItem["title"],
                            style: new TextStyle(
                                color: menuItem["color"],
                                fontSize: 24.0),
                            ),
                        );
                    }).toList(),
                ),
            ),
        );
    }
}
复制代码
  • 10-13行

这些线定义了断头台菜单围绕旋转中心(菜单图标的位置)的旋转

现在,此代码的结果将显示一个未旋转的菜单屏幕(因为rotationAngle = 0.0),该屏幕显示了垂直的标题。

接下来使 menu 显示动画

如果更新 rotationAngle 的值(在-π/ 2和0之间),您将看到菜单旋转了相应的角度。

如前所述,我们需要

  • 一个SingleTickerProviderStateMixin,因为我们只有1个场景
  • 一个AnimationController
  • 一个动画 有一个角度变化

代码如下所示:

class _GuillotineMenuState extends State<GuillotineMenu>
    with SingleTickerProviderStateMixin {

    AnimationController animationControllerMenu;
    Animation<double> animationMenu;

    ///
    /// Menu Icon, onPress() handling
    ///
    _handleMenuOpenClose(){
        animationControllerMenu.forward();
    }

    @override
    void initState(){
        super.initState();

    ///
        /// Initialization of the animation controller
        ///
        animationControllerMenu = new AnimationController(
            duration: const Duration(milliseconds: 1000), 
            vsync: this
        )..addListener((){
            setState((){});
        });

    ///
        /// Initialization of the menu appearance animation
        ///
        _rotationAnimation = new Tween(
            begin: -pi/2.0, 
            end: 0.0
        ).animate(animationControllerMenu);
    }

    @override
    void dispose(){
        animationControllerMenu.dispose();
        super.dispose();
    }

    @override
    Widget build(BuildContext context){
        MediaQueryData mediaQueryData = MediaQuery.of(context);
        double screenWidth = mediaQueryData.size.width;
        double screenHeight = mediaQueryData.size.height;
        double angle = animationMenu.value;

        return new Transform.rotate(
            angle: angle,
            origin: new Offset(24.0, 56.0),
            alignment: Alignment.topLeft,
            child: Material(
                color: Colors.transparent,
                child: Container(
                    width: screenWidth,
                    height: screenHeight,
                    color: Color(0xFF333333),
                    child: new Stack(
                        children: <Widget>[
                            _buildMenuTitle(),
                            _buildMenuIcon(),
                            _buildMenuContent(),
                        ],
                    ),
                ),
            ),
        );
    }

    ...
    ///
    /// Menu Icon
    /// 
    Widget _buildMenuIcon(){
        return new Positioned(
            top: 32.0,
            left: 4.0,
            child: new IconButton(
                icon: const Icon(
                    Icons.menu,
                    color: Colors.white,
                ),
                onPressed: _handleMenuOpenClose,
            ),
        );
    }
    ...
}
复制代码

现在,当我们按下菜单按钮时,菜单会打开,但再次按下按钮时菜单不会关闭。这是 AnimationStatus 要完成的事情。

让我们添加一个监听器,并基于 AnimationStatus 决定是向前还是向后运行动画。


///
/// Menu animation status
///
enum _GuillotineAnimationStatus { closed, open, animating }

class _GuillotineMenuState extends State<GuillotineMenu>
    with SingleTickerProviderStateMixin {
    AnimationController animationControllerMenu;
    Animation<double> animationMenu;
    _GuillotineAnimationStatus menuAnimationStatus = _GuillotineAnimationStatus.closed;

    _handleMenuOpenClose(){
        if (menuAnimationStatus == _GuillotineAnimationStatus.closed){
            animationControllerMenu.forward().orCancel;
        } else if (menuAnimationStatus == _GuillotineAnimationStatus.open) {
            animationControllerMenu.reverse().orCancel;
        }
    }

    @override
    void initState(){
        super.initState();

    ///
        /// Initialization of the animation controller
        ///
        animationControllerMenu = new AnimationController(
            duration: const Duration(milliseconds: 1000), 
            vsync: this
        )..addListener((){
            setState((){});
        })..addStatusListener((AnimationStatus status) {
            if (status == AnimationStatus.completed) {
        ///
        /// When the animation is at the end, the menu is open
        ///
              menuAnimationStatus = _GuillotineAnimationStatus.open;
            } else if (status == AnimationStatus.dismissed) {
        ///
        /// When the animation is at the beginning, the menu is closed
        ///
              menuAnimationStatus = _GuillotineAnimationStatus.closed;
            } else {
        ///
        /// Otherwise the animation is running
        ///
              menuAnimationStatus = _GuillotineAnimationStatus.animating;
            }
          });

    ...
    }
...
}
复制代码

现在菜单可以按预期方式打开或关闭,但是前面的演示向我们展示了一个打开/关闭的动画,该懂哈不是线性的,看起来有一个反复的回弹效果。接下来让我们添加此效果。

为此,我将选择以下2种效果:

  • 菜单打开时用 bounceOut
  • 菜单关闭时用 bouncIn

image.png

image.png


class _GuillotineMenuState extends State<GuillotineMenu>
    with SingleTickerProviderStateMixin {
...
    @override
    void initState(){
    ...
    ///
    /// Initialization of the menu appearance animation
    /// 
    animationMenu = new Tween(
        begin: -pi / 2.0, 
        end: 0.0
    ).animate(new CurvedAnimation(
        parent: animationControllerMenu,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn,
    ));
    }
...
}
复制代码

在此实现中仍有一些细节没有实现:打开菜单时标题消失,而关闭菜单时显示标题。这是一个面朝上/朝外的效果,也要作为动画处理。让我们添加它。


class _GuillotineMenuState extends State<GuillotineMenu>
    with SingleTickerProviderStateMixin {
  AnimationController animationControllerMenu;
  Animation<double> animationMenu;
  Animation<double> animationTitleFadeInOut;
  _GuillotineAnimationStatus menuAnimationStatus;

...
  @override
  void initState(){
    ...
    ///
    /// Initialization of the menu title fade out/in animation
    /// 
    animationTitleFadeInOut = new Tween(
        begin: 1.0, 
        end: 0.0
    ).animate(new CurvedAnimation(
        parent: animationControllerMenu,
        curve: new Interval(
            0.0,
            0.5,
            curve: Curves.ease,
        ),
    ));
  }
...
  ///
  /// Menu Title
  ///
  Widget _buildMenuTitle(){
    return new Positioned(
      top: 32.0,
      left: 40.0,
      width: screenWidth,
      height: 24.0,
      child: new Transform.rotate(
        alignment: Alignment.topLeft,
        origin: Offset.zero,
        angle: pi / 2.0,
        child: new Center(
          child: new Container(
            width: double.infinity,
            height: double.infinity,
              child: new Opacity(
                opacity: animationTitleFadeInOut.value,
                child: new Text('ACTIVITY',
                    textAlign: TextAlign.center,
                    style: new TextStyle(
                        color: Colors.white,
                        fontSize: 20.0,
                        fontWeight: FontWeight.bold,
                        letterSpacing: 2.0,
                    )),
                ),
            ),
        )),
    );
  }
...
}
复制代码

最终的效果基本如下:

image.png

本文的完整源代码可在GitHub上找到。

结论

如您所见,构建动画非常简单,甚至复杂的动画也是如此。

I hope this article longer able to successfully explain Flutter animation.

Of course, you want to learn more flutter, you can follow me

Guess you like

Origin blog.51cto.com/14606040/2462244