flutter - 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'),
),
),
),
);
}
}
解决方案
问题在于您的计时器每秒 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 对象中
不遵循这些准则可能会导致以后出现奇怪的行为。
我知道一开始你只是想让它工作,然后你会完善它,只是说它作为一个提示,鼓励你尝试不同的方法,看看什么是最适合你的
推荐阅读
- javascript - 如何检查电子邮件是否不是 gmail 或 hotmail
- docker - 如何使用 selenium 网格和 docker 访问 localhost 应用程序
- rest - 属性值可以用作 REST url 中的标识符吗
- android - 混合内容:页面通过 HTTPS 加载,但请求的图像不安全
- angular - Angular + RxJS BehaviorSubject 订阅不起作用
- unity3d - 仅渲染框内的点云数据
- git - 如何解决阅读我的结束标签问题
- node.js - 我正在尝试安装 npm 包,但它不起作用。错误:EPERM:不允许操作,mkdir 'C:\Users\HAMZA~1'
- loops - 如何传递具有不同参数的场景大纲?
- android - BottomsheetDialog 未设置样式或本地化