如何在 Flutter 中链接动画和非动画功能?

How to chain animation and non-animation functions in Flutter?

我开发了一个变位词解决游戏,它向用户展示了一个变位词,他们必须按照以下方式解决。

布局是一个 table,具有一系列 table 行,每行包含 table 个单元格。每个 table 单元格都有一个带有文本小部件的容器,该小部件可以为空或包含一个字符。字符本身保存在一个列表中,该列表的长度与屏幕上 table 单元格的数量相同。 table 单元格中的每个文本小部件都从列表中的相应项目中提取其文本值。

如果玩家想以不同的顺序显示角色,他们可以随机播放角色。我已将动画添加到文本小部件,以便它们淡出视图,底层列表随机打乱,然后字符在新位置淡出视图。基本工作流程是:

  1. 用户按随机播放
  2. 程序遍历字符列表并触发淡出动画 任何具有文本值的文本小部件淡出动画持续 800 毫秒
  3. 然后程序将文本列表中的目标词打乱顺序程序迭代 再次列出字符并触发具有的任何文本小部件的淡入动画 文本值

我的问题是动画并不总是按计划执行。有时角色会消失然后淡入。有时它们会淡出并保持隐藏状态。有时他们会按上面的计划工作。我假设这是因为动画的时间和我的代码。目前我有一系列代码 class 可以一次性执行上面的活动,按照下面的伪代码

For each table cell {
  if (table cell Text widget has a value) then {
    trigger the Text widget fade-out animation;
  }
}

Shuffle the text List;

For each table cell {
  if (table cell Text widget has a value) then {
    trigger the Text widget fade-in animation;
  }
}

我认为以这种方式执行代码会导致问题,因为这意味着我的淡出动画将被触发,底层文本列表将被随机播放,而这些动画仍在 运行ning 并且淡出动画也会在淡出动画结束前触发。

我的问题是,控制动画和随机播放功能的执行时间的正确设计模式是什么,以便它们按顺序执行而不重叠?

我研究过创建一种堆栈类型,我将动画和随机播放函数压入堆栈,然后执行它们,但这感觉很笨拙,因为我需要区分许多并行淡出动画(对于例如,如果要猜的单词有 8 个字符,那么我的程序会触发 8 个淡出动画)然后调用 shuffle 函数。

根据 ,我还研究了使用 .whenComplete() 方法:

_animationController.forward().whenComplete(() {
 // put here the stuff you wanna do when animation completed!
});

但是在协调多个并行动画方面,我会遇到与堆栈方法相同的问题。

我考虑过设计我的文本字符小部件,这样我就可以传递一个标志,该标志会触发网格中第一个具有值的文本小部件的 .whenComplete() 方法,然后让另一个文本小部件淡出-out 动画 运行 分开。然后我可以使用回调在第一个文本小部件淡出动画结束时随机播放文本,并在随机播放函数后触发淡入动画。

同样,这感觉有点笨拙,我想知道我是否遗漏了什么。 Flutter 中是否内置了任何支持动画-> 非动画功能-> 动画链接的东西,或者是否有一种设计模式可以专门以优雅的方式解决这个问题?

我已经使用回调函数和堆栈实现了这个,因为我觉得这会给我最大的灵活性,例如如果我想 hide/show 文本小部件有不同的 start/end 次来给它一个更自然的感觉。这行得通,但根据我最初的问题 如果有更好的方法来实现这个,我愿意接受建议。

pseudo-code中的基本执行流程是:

网格显示

    shuffle.onPressed() {
      disable user input;
      iterate over the grid {
        if (cell contains a text value) {
          push Text widget key onto a stack (List);
          trigger the hide animation (pass callback #1);
        }
      }
    }

文本小部件隐藏动画

    hide animation.whenComplete() {
      call the next function (callback #1 - pass widget key);
    }

回调函数#1

    remove Text widget key from the stack;
    if (stack is empty) {
      executive shuffle function;
      iterate over the grid;
      if (cell contains a text value) {
        push Text widget key onto a stack (List);
        trigger the show animation (pass callback #2);
      }
    }

文本小部件显示动画

    show animation.whenComplete() {
      call the next function (callback #2 - pass widget key);
    }

回调函数#2

    remove Text widget key from the stack
    if (stack is empty) {
      enable user input;
    }

我在下面摘录了代码以展示我是如何实现它的。

在屏幕上显示网格的主要 class 具有以下变量和函数。

    class GridState extends State<Grid> {
      // List containing Text widgets to displays in cells including unique 
      // keys and text values
      final List<TextWidget> _letterList = _generateList(_generateKeys());
    
      // Keys of animated widgets - used to track when these finish
      final List<GlobalKey<TextWidgetState>> _animations = [];
    
      bool _isInputEnabled = true; // Flag to control user input
    
      @override
      Widget build(BuildContext context) {
        …
        ElevatedButton(
          onPressed: () {
            if (_isInputEnabled) {
              _hideTiles();
            }
          },
          child: Text('shuffle', style: TextStyle(fontSize: _fontSize)),
        ),
        …
      }
    
      // Function to hide the tiles on the screen using their animation
      void _hideTiles() {
        _isInputEnabled = false; // Disable user input
    
        // Hide the existing tiles using animation
        for (int i = 0; i < _letterList.length; i++) {
          // Only animate if the tile has a text value
          if (_letterList[i].hasText()) {
            _animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
            _letterList[i].hide(_shuffleAndShow);
          }
        }
      }
    
      // Function to shuffle the text on screen and then re-show the tiles using
      // their animations
      void _shuffleAndShow() {
        _animations.remove(key);
    
        if (_animations.isEmpty) {
          widget._letterGrid.shuffleText(
              widget._letterGrid.getInputText(), widget._options.getCharType());
    
          // Update the tiles with the new characters and show the new tile locations using animation
          for (int i = 0; i < _letterList.length; i++) {
            // Update tile with new character
            _letterList[i].setText(
                widget._letterGrid.getCell(i, widget._options.getCharType()));
    
            // If the tile has a character then animate it
            if (_letterList[i].hasText()) {
              _animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
              _letterList[i].show(_enableInput);
            }
          }
        }
      }
    
      // Function re-enable user input following the shuffle animations
      void _enableInput(GlobalKey<LetterTileState> key) {
        _animations.remove(key);
    
        if (_animations.isEmpty) {
          _isInputEnabled = true;
        }
      }

_letterList 中的Text widgets 有以下动画函数,完成后会调用回调函数。请注意,此代码处于 Statefulwidget 的状态。


    // Animation variables
    final Duration _timer = const Duration(milliseconds: 700);
    late AnimationController _animationController;
    late Animation<double> _rotateAnimation;
    late Animation<double> _scaleAnimation;
    
    @override
    void initState() {
      super.initState();
  
      // Set up the animation variables
      _animationController = AnimationController(vsync: this, duration: _timer);
    
      _rotateAnimation = Tween<double>(begin: 0, end: 6 * pi).animate(
          CurvedAnimation(
              parent: _animationController,
              curve: const Interval(0, 1, curve: Curves.easeIn)));
      _rotateAnimation.addListener(() {
        setState(() {});
      });
    
      _scaleAnimation = Tween<double>(begin: 1, end: 0).animate(CurvedAnimation(
          parent: _animationController,
          curve: const Interval(0, 0.95, curve: Curves.ease)));
      _scaleAnimation.addListener(() {
        setState(() {});
      });
    }

    ///
    /// Animation functions
    ///
    // Function to hide the tile - spin and shrink to nothing
    void hide(Function callback) {s
      _animationController.forward(from: 0).whenComplete(() {
        _animationController.reset();
        callback(widget.key);
      });
    }
  
    // Function to show the tile - spin and grow from nothing
    void show(Function callback) {
      _animationController.reverse(from: 1).whenComplete(() {
        _animationController.reset();
        callback(widget.key);
      });
    }

更新:

阅读更多内容并使用默认计数器示例构建了一个实验性应用程序后,我发现添加 Listeners 和 StatusListeners 是另一种可能更好的方法来完成我想要的事情。这也适用于我在之前的回答中使用的堆栈方法。

示例代码如下:

主要class:

    import 'package:flutter/material.dart';
    
    import 'counter.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
      Counter counter = Counter();
      late AnimationController animationController;
      late Animation<double> shrinkAnimation;
    
      @override
      void initState() {
        animationController = AnimationController(
            vsync: this, duration: const Duration(milliseconds: 500));
    
        shrinkAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
            CurvedAnimation(
                parent: animationController,
                curve: const Interval(0.0, 1.0, curve: Curves.linear)));
        shrinkAnimation.addListener(() {
          setState(() {}); // Refresh the screen
        });
        shrinkAnimation.addStatusListener((status) {
          switch (status) {
            // Completed status is after the end of forward animation
            case AnimationStatus.completed:
              {
                // Increment the counter
                counter.increment();
    
                // Do some work that isn't related to animation
                int value = 0;
                for (int i = 0; i < 1000; i++) {
                  value++;
                }
                print('finishing value is $value');
    
                // Then reverse the animation
                animationController.reverse();
              }
              break;
            // Dismissed status is after the end of reverse animation
            case AnimationStatus.dismissed:
              {
                animationController.reset();
              }
              break;
          }
        });
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                AnimatedBuilder(
                    animation: animationController,
                    builder: (context, child) {
                      return Transform.scale(
                        alignment: Alignment.center,
                        scale: shrinkAnimation.value,
                        child: Text(
                          '${counter.get()}',
                          style: Theme.of(context).textTheme.headline4,
                        ),
                      );
                    }),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              animationController.forward(); // Shrink current value first
            },
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }

计数器class:

    import 'package:flutter/cupertino.dart';
    
    class Counter {
      int _counter = 0;
    
      void increment() {
        _counter++;
      }
    
      int get() {
        return _counter;
      }
    }