fCreate

一个简单的眼力测试游戏,由 Flutter CustomPainter 提供支持。

一个简单的Android应用程序,使用Flutter构建。该应用程序正在参加Flutter Create竞赛。因此,它功能有限,仅使用5112字节的Dart代码构建。而且它是一个纯Dart应用程序,目标是Android SDK版本28。

它是如何工作的?

  • 执行从App()类开始,该类扩展StatelessWidget

  • 由于此应用程序仅针对Android,因此它是MaterialApp

  • 由于我将为我们的应用程序添加交互性,因此我需要一个StatefulWidgetHome()满足了这一需求。

  • 由于代码限制,我将应用程序限制为仅在纵向模式下运行。

      SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
    
  • build方法中,我返回了一个Scaffold Widget,它提供了一个健全的Material Design骨架。

  • Scaffoldbody是一个Stack widget,它非常擅长根据特定条件或用户事件处理重叠小部件的隐藏,通过使用Opacity Widget作为其父级。

  • 我还在使用一个FloatingActionButton(),用于在帮助小部件和游戏小部件之间切换。

      floatingActionButton: FloatingActionButton(
        onPressed: _e
            ? () {
                swap();
                // _tm holds a periodic Timer, which helps us to generate a new Paint widget, every 1 seconds.
                _tm = Timer.periodic(Duration(seconds: 1), (t) {
                  if (t.isActive)
                    setState(() {
                      _b = Random().nextInt(12) + 8; // _b, holds number of columns and rows, to be drawn in Paint widget
                      // which also gets updated in random fashion
                    });
                });
              }
            : () {
                swap();
                _tm.cancel(); // timer, _tm is cancelled, to avoid unnecessary periodic computational tasks.
              }, // here `e` is a boolean variable, which decides behaviour of this button
        child: Icon(_e ? Icons.play_arrow : Icons.help), // icon also gets updated
        backgroundColor: Colors.cyanAccent,
        elevation: 16,
        tooltip: _e ? 'Init' : 'Help', // tooltip text is also updated as value of `e` gets updated.
      ),
    
  • 所以,你遇到一个新函数swap(),它只是改变两个小部件的不透明度值,并使它们可见或不可见。

      swap() {
    var tmp = _h; // simple swapping of `_h` and `_g` is done here.
    setState(() {
      _h = _g;
      _g = tmp;
      _e = !_e; // putting negation of `_e` into `_e`, helps us to change behaviour of floatingActionButton.
    });
    }
    
  • 你可能已经找到了一个名为setW()的方法,由于在_HomeState中实现了White类而存在。

      @override
    setW(int c) {
    if (!_tm.isActive)
      setState(() {
        _wh = c;
      });
    else
      _wh = c; // here I'm not interested in updating the UI too, that's why assignment is put outside of setState((){}).
      // otherwise that might collide with scheduled UI updated operation, which runs periodically using Timer, and updates CustomPaint widget.
    }
    
  • GestureDetector widget用作CustomPaint的父级,以处理用户输入事件。单击一次表示用户选择了当前显示的金漆小部件,并想知道他/她是否选择了至少有50%白色球的小部件。作为响应,会弹出一个Dialog来指示当前选择的状态。

      onTap: () {
                  if (_tm.isActive) {
                    _tm.cancel();
                    showDialog(
                        context: context,
                        builder: (cx) => Dialog(
                            elevation: 9,
                            child: Padding(
                                padding: EdgeInsets.all(16),
                                child: Text(
                                    '$_wh/${_b * _b} White Balls ${_wh >= (_b * _b) / 2 ? '\u{2714}' : '\u{2716}'}',
                                    style: TextStyle(
                                        fontSize: 25, letterSpacing: 2)))));
                  }
                },
    
  • 现在,如果你双击屏幕,取消的计时器将重新开始运行。

      
                onDoubleTap: () {
                  if (!_tm.isActive)
                    _tm = Timer.periodic(Duration(seconds: 1), (t) {
                      if (t.isActive)
                        setState(() {
                          _b = Random().nextInt(12) + 8;
                        });
                    });
                }
    
  • 一个居中对齐的小部件用作Opacity的子级,用于显示帮助页面。更改不透明度值可以显示帮助页面或游戏页面。当然,这些Opacity小部件的父级是Stack小部件。

      Center(
                child: Padding(
                    padding: EdgeInsets.all(12),
                    child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: <Widget>[
                          Text('An EYE Test Game',
                              style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  letterSpacing: 3,
                                  fontSize: 30)),
                          Divider(color: Colors.white, height: 24),
                          Text(
                              'Click to reveal whether it has atleast 50% WHITE Balls. Double clicking restarts loop.',
                              maxLines: 3)
                        ])))
    
  • 让我们来谈谈Painter类,它继承了CustomPainter,主要负责在CustomPaint小部件中执行的绘图操作。Painter的构造函数接受两个参数:沿行和列放置的球的数量(它是正方形)以及一个White类的实例,该实例充当回调机制,用于更新在绘图过程中随机生成的白色球的值,这些值存储在_HomeState类中的一个变量中。

  • 根据CustomPainter的定义,需要在Painter中重写paint()shouldRepaint()方法。

  • 所以,我将绘制一些圆圈,为此我需要一个Offset。X轴和Y轴上的位置确定如下。

    double y = size.height / (b * 2); // b is # of balls along x and y axis.
    double x = size.width / (b * 2); 
    
  • 白色球的数量确定如下。

      w += (p.color == Colors.white) ? 1 : 0; // p variable holds instance of Paint(), which is instantiated just in previous line.
    
  • 这就是如何以迭代方式绘制圆圈,使用while循环直到我们到达X轴上的size.width或Y轴上的size.height

      canvas.drawCircle(
            Offset(x, y), min(size.height / (b * 2), size.width / (b * 2)), p); // radius of circle is decreased, so that no two circle gets overlapped.
    
  • paint()的内部while循环中,x的坐标值在每次迭代中增加size.width / b

    x += size.width / b;
    
  • y也是如此,在外while循环中。

    y += size.height / b;
    
  • 绘制完成后,屏幕上绘制的白色球的数量会更新,其中White抽象类起着作用。

    wh.setW(w); // wh is an instance of WHite, which is passed into constructor of Painter.
    
  • 就是这样。

Screenshot_20190407-005819

Screenshot_20190407-005833

Screenshot_20190407-005845

GitHub

https://github.com/itzmeanjan/fCreate