Таймер обратного отсчета Flutter для шахматного приложения (застрял в логике)
Итак, последние несколько дней я пытался обойти этот логический цикл, перепробовал все, что мог, а затем, наконец, решил опубликовать его здесь (отсюда и мой дебют здесь). Я застрял, вся помощь приветствуется. Спасибо!
Логика: В приложении их 2container
который отображает время белых и черных, которое выбирается из заданных пяти вариантов -> 5,10,15,30,60, которые затем обновляют время для отображения в этих контейнерах, использовали provider
пакет для этого и всего остального.
Теперь я также добавил Raised button
с именем 'switch', который при нажатии должен был:
- Старт таймера обратного отсчета белых
- Затем, если нажать снова, таймер белых остановится, а затем начнется обратный отсчет черных.
- Затем, если нажать еще раз, должен остановить таймер черных, а затем запустить таймер белых.
- И повторяйте, пока не истечет время, и один из двух не будет объявлен победителем.
Проблема: Итак, с тем, что я закодировал до сих пор, при нажатии "переключателя" запускается таймер белого, а при повторном нажатии он останавливает таймер белого, но не запускает таймер черного. Я знаю это из-за того, как я сформулировал условия if(), а также потому, что не знаю, как остановить таймер извне. Что я сделал, так это использовать - bool checkTimerW и checkTimerB для каждого белого и черного, которые я проверяю в условии if() для отмены таймера, основанного на нем.
Код:
Провайдер -
import 'dart:async';
import 'package:flutter/foundation.dart';
class SettingsProvider extends ChangeNotifier {
int valueW = 0;
int valueB = 0;
// * with these booleans we will stop the timer.
bool checkTimerW = true;
bool checkTimerB = true;
String timeToDisplayW = ""; // for white
String timeToDisplayB = ""; // for black
bool switchT = false;
// this is called in the settings Modal Bottom Sheet
void changeValue(int valW, int valB) {
//? Changing the value in seconds
valueW = valW * 60;
valueB = valB * 60;
print(valueW);
timeToDisplayW = valueW.toString();
timeToDisplayB = valueB.toString();
notifyListeners();
}
void reset() {
started = true;
stopped = true;
checkTimerW = false;
checkTimerB = false;
notifyListeners();
}
void toggleSwitch() {
if (switchT == false) {
switchT = true;
print('true');
} else if (switchT == true) {
switchT = false;
print('false');
}
}
void switchTimer() {
if (switchT == false) {
// Starts white's timer
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if (valueW <= 1 || checkTimerW == false) {
t.cancel();
checkTimerW = true;
// TODO : Black Won
notifyListeners();
} else {
valueW = valueW - 1;
notifyListeners();
}
timeToDisplayW = valueW.toString();
notifyListeners();
},
);
// stops black's timer
checkTimerB = false;
toggleSwitch();
notifyListeners();
} else {
// Starts black's timer
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if (valueB <= 1 || checkTimerB == false) {
t.cancel();
checkTimerB = true;
// TODO : White won
notifyListeners();
} else {
valueB = valueB - 1;
notifyListeners();
}
timeToDisplayB = valueB.toString();
notifyListeners();
},
);
// stops white's timer
checkTimerW = false;
toggleSwitch();
notifyListeners();
}
}
}
Main.dart -
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'controller/countdown_controller.dart';
import 'widgets/blackButton.dart';
import 'widgets/bottom_sheet_design.dart';
import 'widgets/whiteButton.dart';
import 'providers/settings.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return ChangeNotifierProvider(
create: (ctx) => SettingsProvider(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.amber,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void settings(BuildContext ctx) {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
context: ctx,
builder: (_) => BottomSheetDesign(),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.grey[350],
body: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 1,
child: Transform.rotate(
angle: pi / 1,
child: GestureDetector(
onTap: () {
Provider.of<SettingsProvider>(context, listen: false)
.switchTimer();
},
child: Container(
width: 80.0,
height: 500,
child: Center(
child: Transform.rotate(
angle: pi / 2,
child: Text('Switch',
style: Theme.of(context).textTheme.bodyText2),
),
),
decoration: BoxDecoration(
color: Colors.blueGrey,
),
),
),
),
),
VerticalDivider(),
Expanded(
flex: 4,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
// container that displays black's timer
BlackButton(),
Expanded(
child: Transform.rotate(
angle: pi / 2,
child: RaisedButton(
onPressed: () {
settings(context);
},
color: Colors.blue[300],
child: Text('Settings'),
),
),
),
],
),
SizedBox(
height: 20,
),
Row(
children: <Widget>[
// container that displays white's timer
WhiteButton(),
Expanded(
child: Transform.rotate(
angle: pi / 2,
child: RaisedButton(
onPressed: () {
Provider.of<SettingsProvider>(context, listen: false).reset();
},
color: Colors.red[600],
child: Text('Reset'),
),
),
),
],
),
],
),
),
],
),
),
);
}
}
2 ответа
Я кодировал это в прошлом. Это может вам помочь.
class DoubleTimer extends StatefulWidget {
@override
_DoubleTimerState createState() => _DoubleTimerState();
}
class _DoubleTimerState extends State<DoubleTimer> {
int timeToGoA = 50000;
int timeToGoB = 50000;
int state = 0; //0: waiting, 1: counting A, 2: counting B
DateTime timeStamp;
_DoubleTimerState() {
print("init");
}
@override
Widget build(BuildContext context) {
print(
"${DateTime.now().compareTo(DateTime.now().add(Duration(seconds: 1)))}");
return Row(
children: <Widget>[
if (state == 1)
ToTime(timeStamp.add(Duration(milliseconds: timeToGoA))),
FlatButton(
onPressed: () {
setState(() {
switch (state) {
case 0:
state = 1;
timeStamp = DateTime.now();
print("Running A");
break;
case 1:
state = -1;
timeToGoA -=
DateTime.now().difference(timeStamp).inMilliseconds;
timeStamp = DateTime.now();
print("A: $timeToGoA\nRunning B");
break;
case -1:
state = 1;
timeToGoB -=
DateTime.now().difference(timeStamp).inMilliseconds;
timeStamp = DateTime.now();
print("B: $timeToGoB\nRunning A");
break;
}
});
},
child: Text("switch"),
),
if (state == -1)
ToTime(timeStamp.add(Duration(milliseconds: timeToGoB))),
],
);
}
}
class ToTime extends StatelessWidget {
final DateTime timeStamp;
const ToTime(this.timeStamp, {Key key}) : super(key: key);
static final Map<String, int> _times = <String, int>{
'y': -const Duration(days: 365).inMilliseconds,
'm': -const Duration(days: 30).inMilliseconds,
'w': -const Duration(days: 7).inMilliseconds,
'd': -const Duration(days: 1).inMilliseconds,
'h': -const Duration(hours: 1).inMilliseconds,
'\'': -const Duration(minutes: 1).inMilliseconds,
'"': -const Duration(seconds: 1).inMilliseconds,
"ms": -1,
};
Stream<String> get relativeStream async* {
while (true) {
int duration = DateTime.now().difference(timeStamp).inMilliseconds;
String res = '';
int level = 0;
int levelSize;
for (MapEntry<String, int> time in _times.entries) {
int timeDelta = (duration / time.value).floor();
if (timeDelta > 0) {
levelSize = time.value;
res += '$timeDelta${time.key} ';
duration -= time.value * timeDelta;
level++;
}
if (level == 2) {
break;
}
}
levelSize ??= _times.values.reduce(min);
if (level > 0 && level < 2) {
List<int> _tempList =
_times.values.where((element) => (element < levelSize)).toList();
if (_tempList.isNotEmpty) levelSize = _tempList.reduce(max);
}
if (res.isEmpty) {
yield 'now';
} else {
res.substring(0, res.length - 2);
yield res;
}
// print('levelsize $levelSize sleep ${levelSize - duration}ms');
await Future.delayed(Duration(milliseconds: levelSize - duration));
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: relativeStream,
builder: (context, snapshot) {
return Text(snapshot.data ?? '??');
});
}
}
Я закодировал его с помощью провайдера (используя только ValueNotifier), чтобы показать вам логику
enum Player{White, Black}
class MyTimer extends ValueNotifier<int>{
Player _turn; //White starts
int _minutes;
int _whiteTime;
int _blackTime;
MyTimer(int time) :
_minutes = time * 60,
_whiteTime = time * 60,
_blackTime = time * 60,
_turn = Player.White, //White starts
super(time * 60 * 2);
bool get _isWhiteTurn => Player.White == _turn;
String get timeLeft{
if(value != 0){
//int time = _isWhiteTurn ? _whiteTime : _blackTime; //use this instead of playerTime if you want to display the time in seconds
Duration left = Duration(seconds: _isWhiteTurn ? _whiteTime : _blackTime);
String playerTime = left.toString();
playerTime = playerTime.substring(0, playerTime.lastIndexOf('.'));
return '${describeEnum(_turn)} turn time left : $playerTime';
}
else{
return '${describeEnum(_turn)} wins!'; //We have a winner
}
}
void switchPlayer() => _turn = _isWhiteTurn ? Player.Black : Player.White;
void reset([int time]){
if(time != null) _minutes = time * 60; //if you want to start with a different value
_turn = Player.White; //White starts
_whiteTime = _minutes; //reset time
_blackTime = _minutes; //reset time
value = 2*_minutes; //reset time
//twice as long because it counts the whole time of the match (the time of the 2 players)
}
void start(){
_initilizeTimer();
}
void _initilizeTimer(){
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if(_whiteTime == 0 || _blackTime == 0){
t.cancel();
switchPlayer(); //the time of one player ends, so it switch to the winner player
value = 0; //end the game
}
else{
_isWhiteTurn ? --_whiteTime : --_blackTime;
--value;
}
},
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final MyTimer clock = MyTimer(1);
@override
void initState(){
super.initState();
clock.start();
}
@override
void dispose(){
clock.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.grey[350],
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ValueListenableBuilder<int>(
valueListenable: clock,
builder: (context, unit, _) =>
Text(clock.timeLeft ,style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500))
),
RaisedButton(
child: Text('Switch'),
onPressed: () => clock.switchPlayer(),
)
],
),
)
),
);
}
}
Идея та же самая, но я хочу показать вам, что вы можете использовать только один таймер для выполнения всей логики и с некоторым изменением значения перечисления (белого и черного) между обеими минутами.
Кнопка изменяет ход игрока (метод switchPlayer) и внутри таймера вы видите, что в зависимости от хода игрока он сокращает свое время _isWhiteTurn ? --_whiteTime : --_blackTime;
. Как ValueNotifier он обновляется только при изменении значения, но вы можете использовать своего Provider с ChangeNotifier и обновлять, когда хотите (и это лучше, потому что при смене игрока в моем примере мне все равно нужно ждать окончания второго, поэтому таймер обновляет текст).
Вы можете попробовать изменить что-то подобное с помощью перечисления, чтобы упростить логику таймера
bool get _isWhiteTurn => Player.White == _turn;
void startMatch() {
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if (valueW == 0 || valueB == 0) {
t.cancel();
if(valueW == 0) checkTimerB = true;
else checkTimerW = true
//it won the one whose time didn't end
} else {
_isWhiteTurn ? --valueW : --valueB;
}
timeToDisplayW = valueW.toString();
timeToDisplayB = valueB.toString();
//only one of them will change
notifyListeners();
},
);
}
void switchTimer(){
_turn = _isWhiteTurn ? Player.Black : Player.White;
notifyListeners();
}
Таким образом, у вас есть только один таймер на весь матч, который будет отменен, когда один из таймеров достигнет 0 (или если кто-то потеряет, но это другая логика в каком-то другом провайдере, я думаю)
ОБНОВИТЬ
Вы можете изменить геттер timeLeft на что-то вроде этого
String get timeLeft{
if(value != 0){
//int time = _isWhiteTurn ? _whiteTime : _blackTime; //use this instead of playerTime if you want to display the time in seconds
Duration white = Duration(seconds: _whiteTime);
Duration black = Duration(seconds: _blackTime);
String whiteTime = white.toString();
String blackTime = black.toString();
whiteTime = whiteTime.substring(0, whiteTime.lastIndexOf('.'));
blackTime = blackTime.substring(0, blackTime.lastIndexOf('.'));
return '''
${describeEnum(Player.White)} time left : $whiteTime
${describeEnum(Player.Black)} time left : $blackTime
''';
}
else{
return '${describeEnum(_turn)} wins!'; //We have a winner
}
}
Таким образом, он вернет String с обоими временами, и только таймер игрока, в свою очередь, будет меняться каждую секунду. Но, поскольку я говорю, попробуйте эту логику с ChangeNotifierProvider, и она тоже должна работать, и вы можете использовать ее в разных частях своего дерева виджетов.