首页 > 解决方案 > Timer.periodic() 回调干扰 GestureDetector

问题描述

编辑

本质上,我只是想知道是否有一种方法可以让两个小部件在 Flutter 中并行运行。就我而言,我希望 Timer 小部件和 Game 小部件一起运行而不会干扰彼此的执行。

我曾尝试使用计算和隔离,但显然,隔离不能用于更改小部件状态。

我正在开发一个记忆游戏,我想在卡片组的顶部添加一个计时器。下面是我的代码。

问题是,当计时器启动时,

@override
  void initState() {
    previousClick = _CardTileState();
    super.initState();
    st.start();
    timer = Timer.periodic(Duration(seconds: 1), (Timer t) {
      int ms = st.elapsedMilliseconds;
      int sec = (ms ~/ 1000) % 60;
      int min = (ms ~/ 1000) ~/ 60;
      if (min == 2) {
        st.stop();
        st.reset();
        timer.cancel();
      } else {
        setState(() {
          mins = strFormat(min);
          secs = strFormat(sec);
        });
      }
    });
  }

我不能再点击我的卡片来匹配它们。GestureDetector 的 onTap() 回调没有被执行。

@override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        if (widget.isClickable) {
          print('clicked ' + widget.index.toString());
          widget.parent.handleClick(this);
        }
      },

我该如何解决这个问题?这是完整的代码:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:save_the_planet/data.dart';

class GameScreen extends StatefulWidget {
  GameScreen({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _GameScreenState createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> {
  List<CardTile> cards = getCards();
  _CardTileState previousClick;
  String click = "";
  int score = 0;
  String mins = "00";
  String secs = "00";
  Stopwatch st = Stopwatch();
  Timer timer;

  @override
  void initState() {
    previousClick = _CardTileState();
    super.initState();
    st.start();
    timer = Timer.periodic(Duration(seconds: 1), (Timer t) {
      int ms = st.elapsedMilliseconds;
      int sec = (ms ~/ 1000) % 60;
      int min = (ms ~/ 1000) ~/ 60;
      if (min == 2) {
        st.stop();
        st.reset();
        timer.cancel();
      } else {
        setState(() {
          mins = strFormat(min);
          secs = strFormat(sec);
        });
      }
    });
  }

  String strFormat(int val) {
    String res = "";
    if (val < 10)
      res = "0" + val.toString();
    else
      res = val.toString();
    return res;
  }

  void handleClick(_CardTileState source) {
    try {
      if (previousClick.widget.getIsUncovered()) {
        if (click == 'first click') {
          print('second click');
          click = 'second click';
          source.setState(() {
            source.widget.setIsUncovered(true);
            source.widget.setIsClickable(false);
          });
          Timer(Duration(milliseconds: 1000), () {
            if (source.widget.getImagePath() ==
                previousClick.widget.getImagePath()) {
              score += 100;
              print(score);
              if (score == 1000) {
                print('game over');
              }
              previousClick = _CardTileState();
            } else {
              source.setState(() {
                source.widget.setIsUncovered(false);
                source.widget.setIsClickable(true);
              });
              previousClick.setState(() {
                previousClick.widget.setIsUncovered(false);
                previousClick.widget.setIsClickable(true);
              });
              previousClick = _CardTileState();
            }
          });
        }
      }
    } catch (e) {
      print('first click');
      click = 'first click';
      source.setState(() {
        source.widget.setIsUncovered(true);
        source.widget.setIsClickable(false);
      });
      previousClick = source;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        height: MediaQuery.of(context).size.height,
        color: Colors.black,
        child: Column(
          children: <Widget>[
            Padding(padding: EdgeInsets.only(top: 25)),
            SizedBox(
              width: MediaQuery.of(context).size.width / 3,
              child: Card(
                margin: EdgeInsets.all(5),
                color: Colors.white54,
                child: Row(
                  children: <Widget>[
                    Icon(Icons.access_time),
                    Text(mins + ":" + secs),
                  ],
                ),
              ),
            ),
            GridView(
              gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                  maxCrossAxisExtent: 100.0, mainAxisSpacing: 0.0),
              shrinkWrap: true,
              scrollDirection: Axis.vertical,
              children: List.generate(
                cards.length,
                (index) {
                  cards[index].setIsUncovered(true);
                  cards[index].setIsClickable(false);
                  cards[index].setIndex(index);
                  cards[index].setParent(this);
                  return cards[index];
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ignore: must_be_immutable
class CardTile extends StatefulWidget {
  String imagePath;
  bool isClickable;
  bool isUncovered;
  int index;
  _GameScreenState parent;

  CardTile(
      {Key key,
      this.imagePath,
      this.isClickable,
      this.isUncovered,
      this.index,
      this.parent})
      : super(key: key);

  void setImagePath(String path) {
    this.imagePath = path;
  }

  void setIsClickable(bool val) {
    this.isClickable = val;
  }

  void setIsUncovered(bool val) {
    this.isUncovered = val;
  }

  void setIndex(int val) {
    this.index = val;
  }

  void setParent(_GameScreenState val) {
    this.parent = val;
  }

  String getImagePath() {
    return this.imagePath;
  }

  bool getIsClickable() {
    return this.isClickable;
  }

  bool getIsUncovered() {
    return this.isUncovered;
  }

  int getIndex() {
    return this.index;
  }

  _GameScreenState getParent() {
    return this.parent;
  }

  @override
  _CardTileState createState() => _CardTileState();
}

class _CardTileState extends State<CardTile> {
  @override
  void initState() {
    super.initState();
    Timer(
      Duration(seconds: 5),
      () {
        setState(() {
          widget.setIsUncovered(false);
          widget.setIsClickable(true);
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        if (widget.isClickable) {
          print('clicked ' + widget.index.toString());
          widget.parent.handleClick(this);
        }
      },
      child: Card(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
        child: Container(
          margin: EdgeInsets.all(5),
          child: Container(
            child: Image.asset(
                widget.isUncovered ? widget.imagePath : 'assets/default1.png'),
          ),
        ),
      ),
    );
  }
}

标签: flutterdarttimergesturedetector

解决方案


问题在于您的计时器每秒 setState,并且您在 build 方法中生成列表,其属性可单击和未覆盖分别为 false 和 true,当您 setState 时,此列表将每秒重建,即使您更改了它们的值在 initState 中设置为 true 一秒钟后它们将变回 false 并且您将被卡在未发现的卡片上而无需点击

timer = Timer.periodic(Duration(seconds: 1), (Timer t) {
  int ms = st.elapsedMilliseconds;
  int sec = (ms ~/ 1000) % 60;
  int min = (ms ~/ 1000) ~/ 60;
  if (min == 2) {
    st.stop();
    st.reset();
    timer.cancel();
  } else {
    setState(() { //this setState
      mins = strFormat(min);
      secs = strFormat(sec);
    });
  }
});

只需在 initState 中生成 List 并将其传递给 GridView 的 children 参数

List<Widget> listOfCards; //Create a variable to save your Widget List

@override
  void initState() {
    previousClick = _CardTileState();
    listOfCards = List<Widget>.generate(cards.length, //Generate it here in initState
      (index) {
         //Each time setState ran this code generated the List with this attributes to true and false
         // Now in initState you create this list once, and even with the setState this list will not be generated again
         cards[index].setIsUncovered(true);
              cards[index].setIsClickable(false);
              cards[index].setIndex(index);
              cards[index].setParent(this);
              return cards[index];
      },
   );
    super.initState();
    st.start();
    timer = Timer.periodic(Duration(seconds: 1), (Timer t) {
      int ms = st.elapsedMilliseconds;
      int sec = (ms ~/ 1000) % 60;
      int min = (ms ~/ 1000) ~/ 60;
      if (min == 2) {
        st.stop();
        st.reset();
        timer.cancel();
      } else {
        setState(() {
          mins = strFormat(min);
          secs = strFormat(sec);
        });
      }
    });
  }

然后在 GridView 中只需传递变量listOfCards

  GridView(
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 100.0, mainAxisSpacing: 0.0),
      shrinkWrap: true,
      scrollDirection: Axis.vertical,
      children: listOfCards
  ),

在测试代​​码是否按您想要的那样工作之后,我只能告诉您尝试使用一些状态管理解决方案(Bloc、get_it、Provider 等)来更改整个逻辑,如果您尝试维护它,则此代码将变得一团糟您不会将逻辑拆分到其他地方。还:

忽略:must_be_immutable

这应该是一个危险信号,因为阅读 StatefulWidget 文档你会看到

StatefulWidget 实例本身是不可变的,并将其可变状态存储在由 createState 方法创建的单独 State 对象中

不遵循这些准则可能会导致以后出现奇怪的行为。

我知道一开始你只是想让它工作,然后你会完善它,只是说它作为一个提示,鼓励你尝试不同的方法,看看什么是最适合你的


推荐阅读