アニメーションは、よりシンプル、フラッタ簡単なアニメーションのチュートリアルを達成するためにしてみましょう!

著者:ディディエBoelens
説明リンク:www.didierboelens.com/2018/06/ani ...
翻訳:HCC

パワフルで使いやすいフラッターアニメーション。具体的な例に続いて、あなたはフラッターアニメーションについてのすべてを学びます。

難易度:中級

今日、我々はあなたが別のページに1ページからジャンプしたときに任意のアニメーションを持っている、または(例えばインク入れのような)ボタンをクリックしないモバイルアプリケーションを想像することはできません...映画があるでしょう。どこにでもアニメーション。

フラッターは、アニメーションが非常に簡単に実装することであることを確認してください。

要するに、この記事では、たとえ唯一の専門家が前に話ができ、記事の外観をより魅力的にするために、私はヴィタリーRubtsovをモデル化し、それに挑戦する、このトピックを議論することですドリブル(みじん切り「ギロチンメニューをアップロードヘッドメニュー)「フラッターとアニメーションがステップによってこの効果ステップを達成しています。

image.png

主要な概念や理論的な知識をご紹介します。この記事の最初の部分は、第二部は、上記のアニメーションを実現することになります。

3つのコアアニメーション

アニメーション効果を達成するために、次の3つの要素を提供する必要があります。

  • ティッカー
  • アニメーション
  • AnimationController

ここでは詳細な説明後に、簡単な紹介をすることが、これらの要素のいくつかあります。

ティッカー

簡単に言えば、このクラスのティッカーが信号を送信するために、一定の時間間隔(毎秒約60回)、となります、あなたは毎秒ターンを刻み、この時計を考える必要があります。

ティッカーが起動すると、ダニ意志ティッカーコールバックのそれぞれに最初の目盛りの到着、コールバックメソッドの初めから。

重要
すべてのティッカーは、異なる時間に開始されるかもしれないが、彼らは常にいくつかの同期のアニメーションのために非常に便利です同期して、で実行しますが。

アニメーション

アニメーションは、実際には何も特別ですが、アニメーション時間の価値の変動とライフサイクルの変更(特定の種類)、できるだけアニメーション値は、方法が変更されたリニア(例えば1、2することができ3,4,5 ...)、それはより複雑になり得る(「曲線カーブ」の下)を参照してください。

AnimationController

AnimationControllerは、一つは、1つ以上のアニメーション(開始、終了、繰り返し)コントローラを制御することが可能です。換言すれば、最大最小値からの速度変化に応じて、指定された時間内に前記値より上のアニメーションを可能にします。

AnimationControllerクラスプレゼンテーション

このようなアニメーションを制御することができます。後、私たちには、いくつかの異なるアニメーションは同じコントローラによって制御することができます表示されますので、より正確には、私はむしろ、「コントロールシーンを」と言うでしょう......

したがって、このAnimationControllerクラスは、私たちのことができます。

  • 前方、子アニメーションを開始するか、逆再生
  • 子のアニメーションを停止します
  • アニメーション値の特定サブ組
  • 境界定義アニメーション値

次の擬似コードは、初期化パラメータとは異なることができるクラスを示し

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
);
复制代码

ほとんどの場合、それはAnimationController初期値、下界、は、UpperBoundとdebugLabelに設計していません。

AnimationControllerは、ティッカーにバインドする方法

アニメーション作品を作るために、AnimationControllerは、ティッカーにバインドする必要があります。

通常の状況下では、インスタンス上StatefulWidgetにバインドされたティッカーを生成することができます。

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();
    }

    ...
}
复制代码
  • 2行目この行はフラッターを伝え、あなたは、単一のティッカー、ティッカーはMyStateWidgetインスタンスにこれをリンクします。

  • 8-10行

コントローラを初期化します。シーン(子アニメーション)の合計時間は1000ミリ秒に設定されており、ティッカー(:このVSYNC)に結合しています。

暗黙のパラメータ:下界= 0.0とは、UpperBound = 1.0

  • 16行

非常に重要な、このページのインスタンスMyStateWidget破壊は、あなたはコントローラを解放する必要がある場合。

TickerProviderStateMixin还是SingleTickerProviderStateMixin?

あなたが状況下で、いくつかのアニメーションコントローラを持っている場合は、別のティッカーを持ちたい、だけSingleTickerProviderStateMixinがTickerProviderStateMixinで交換する必要があります。

まあ、私はティッカーにコントローラをバインドする必要がありますが、それは動作しますか?

ティッカーは、線形目盛りの値は最小値と最大値との間に発生するに従って、目盛り当たり60について生成される所定の時間をAnimationControllerであろうからです。

この1000ミリ秒に生成された値の例は次のとおりです。

image.png

我々は、値が1.0〜0.0(下界)(は、UpperBound)1000ミリ秒まで変化参照します。51個の異なる値を生成します。

私たちはそれを使用する方法を参照するにはコードを拡張してみましょう。

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%'),
                ),
            ),
        );
    }
}

复制代码
  • このラインは、コントローラ12行、その値が変化するが、我々は(SETSTATE(よる))ウィジェットを再構築する必要があるたびに指示します

  • 15行目

ウィジェットの初期化が完了した後、我々はカウントコントローラが開始さを伝える(フォワード() - >は、UpperBoundに下界から)

  • 26行目

我々は、コントローラ(_controller.value)の値を取得し、この例では、この値の範囲は0.0〜1.0である(すなわち、0%〜100%)が、我々は、ページに表示され、このパーセンテージ整数表現を得ますセンター。

アニメーションの概念

これまで見てきたように、コントローラは、直線状に互いに異なる小さな値を返すことができます。

時々、私たちのような他のニーズを持っていることがあります。

  • 値の他のタイプを使用して、例えばオフセット、int型...
  • 0.0から1.0までの範囲ではありません
  • 直線的な変化以外に考慮すべき変化の他のタイプのいくつかの効果

他の値の型を使用します

テンプレートを使用して他の値型、Animationクラスを使用できるようにするには。

言い換えれば、あなたが定義することができます。

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

別の数値範囲を使用します

時々、私たちはむしろ0.0と1.0よりも、別の範囲を使用します。

そのようなAの範囲を定義するために、我々は、Tweenクラスを使用します。

この点を説明するために、私たちはあなたが0からπ/ 2の変化への状況の視点をしたい状況を考えてみましょう。

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

复制代码

タイプの変更

如前所述,将默认值从 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上找到。

结论

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

私は正常にフラッタアニメーションを説明するために、この記事は長いことを願っています。

もちろん、あなたがより多くのフラッターを勉強したい、あなたは私に従うことができます

おすすめ

転載: blog.51cto.com/14606040/2462244