Related
That's what you read. I don't want to hide AppBar when scrolling, there's a lot of info on that.
What I want is the exact opposite. I want my homepage to open with no AppBar and then, when the user starts scrolling, the appbar will be displayed.
This website does exactly what I want to reproduce: https://www.kirschnerbrasil.cc/ (in the desktop version).
I guess I need do use the SliverAppBar, but I haven't manage to do so yet. Can anyone help?
Thanks!
In that case, you will have to make your custom widget the appbar. Please have a look into below code, it will help you to understand the procedure:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Height of your Container
static final _containerHeight = 100.0;
// You don't need to change any of these variables
var _fromTop = -_containerHeight;
var _controller = ScrollController();
var _allowReverse = true, _allowForward = true;
var _prevOffset = 0.0;
var _prevForwardOffset = -_containerHeight;
var _prevReverseOffset = 0.0;
#override
void initState() {
super.initState();
_controller.addListener(_listener);
}
// entire logic is inside this listener for ListView
void _listener() {
double offset = _controller.offset;
var direction = _controller.position.userScrollDirection;
if (direction == ScrollDirection.reverse) {
_allowForward = true;
if (_allowReverse) {
_allowReverse = false;
_prevOffset = offset;
_prevForwardOffset = _fromTop;
}
var difference = offset - _prevOffset;
_fromTop = _prevForwardOffset + difference;
if (_fromTop > 0) _fromTop = 0;
} else if (direction == ScrollDirection.forward) {
_allowReverse = true;
if (_allowForward) {
_allowForward = false;
_prevOffset = offset;
_prevReverseOffset = _fromTop;
}
var difference = offset - _prevOffset;
_fromTop = _prevReverseOffset + difference;
if (_fromTop < -_containerHeight) _fromTop = -_containerHeight;
}
setState(() {}); // for simplicity I'm calling setState here, you can put bool values to only call setState when there is a genuine change in _fromTop
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListView")),
body: Stack(
children: <Widget>[
_yourListView(),
Positioned(
top: _fromTop,
left: 0,
right: 0,
child: _yourContainer(),
)
],
),
);
}
Widget _yourListView() {
return ListView.builder(
itemCount: 100,
controller: _controller,
itemBuilder: (_, index) => ListTile(title: Text("Item $index")),
);
}
Widget _yourContainer() {
return Opacity(
opacity: 1 - (-_fromTop / _containerHeight),
child: Container(
height: _containerHeight,
color: Colors.red,
alignment: Alignment.center,
child: Text("Your Container", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)),
),
);
}
}
Heres the code i edit from #Gourango Sutradhar answer, if you want the top to disappear only when it reach the top
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Height of your Container
static final _containerHeight = 100.0;
// You don't need to change any of these variables
var _fromTop = -_containerHeight;
var _controller = ScrollController();
var _allowReverse = true, _allowForward = true;
var _prevOffset = 0.0;
var _prevForwardOffset = -_containerHeight;
var _prevReverseOffset = 0.0;
#override
void initState() {
super.initState();
_controller.addListener(_listener);
}
// entire logic is inside this listener for ListView
void _listener() {
double offset = _controller.offset;
var direction = _controller.position.userScrollDirection;
if (direction == ScrollDirection.reverse) {
_allowForward = true;
if (_allowReverse) {
_allowReverse = false;
_prevOffset = offset;
_prevForwardOffset = _fromTop;
}
var difference = offset - _prevOffset;
_fromTop = _prevForwardOffset + difference;
if (_fromTop > 0) _fromTop = 0;
} else if (direction == ScrollDirection.forward) {
_allowReverse = true;
if (_allowForward) {
_allowForward = false;
_prevReverseOffset = _fromTop;
}
var difference = offset - _prevOffset;
if (offset > 100.0) {
_prevOffset = offset;
}
if (offset < 100.0) {
_fromTop = _prevReverseOffset + difference;
if (_fromTop < -_containerHeight) _fromTop = -_containerHeight;
}
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListView")),
body: Stack(
children: <Widget>[
_yourListView(),
Positioned(
top: _fromTop,
left: 0,
right: 0,
child: _yourContainer(),
)
],
),
);
}
Widget _yourListView() {
return ListView.builder(
itemCount: 100,
controller: _controller,
itemBuilder: (_, index) => ListTile(title: Text("Item $index")),
);
}
Widget _yourContainer() {
return Opacity(
opacity: 1,
child: Container(
height: _containerHeight,
color: Colors.red,
alignment: Alignment.center,
child: Text("Your Container", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)),
),
);
}
}
I have been writing a Sorting visualiser in flutter, I am so far able to animate the movement of blocks. But I also want to update the colours of the block, when the block goes through the states of being scanned, moved, and finally when it is completely sorted. I looked up the State management in flutter, and it is rather confusing to know what approach should I be using in my project. Below is the DashBoard Class:
import 'package:algolizer/sortingAlgorithms/Block.dart';
import 'package:flutter/material.dart';
import 'dart:math';
class DashBoard extends StatefulWidget {
double width;
double height;
DashBoard(#required this.width, #required this.height);
#override
_DashBoardState createState() => _DashBoardState();
}
class _DashBoardState extends State<DashBoard> {
double currentSliderValue = 50;
List<double> arr = new List(500);
List<Block> blockList;
bool running = false;
#override
void initState() {
// TODO: implement initState
super.initState();
fillArr((widget.width * 0.6) / 50, (widget.width * 0.1) / 50,
widget.height * 0.7);
}
void InsertionSort() async {
setState(() {
running = true;
});
int delay = (pow(15, 4) / pow(currentSliderValue, 2)).round();
for (int i = 1; i < currentSliderValue; i++) {
if (blockList[i] == null) break;
Block key = blockList[i];
int j = i - 1;
while (j >= 0 && blockList[j].height > key.height) {
setState(() {
blockList[j + 1] = blockList[j];
});
await Future.delayed(Duration(milliseconds: delay));
j--;
}
blockList[j + 1] = key;
}
setState(() {
running = false;
});
}
void BubbleSort() async {
setState(() {
running = true;
});
int delay = (pow(15, 4) / pow(currentSliderValue, 2)).round();
for (int i = 0; i < currentSliderValue - 1; i++) {
for (int j = 0; j < currentSliderValue - i - 1; j++) {
if (blockList[j].height > blockList[j + 1].height) {
Block temp = blockList[j + 1];
setState(() {
blockList[j + 1] = blockList[j];
blockList[j] = temp;
});
await Future.delayed(Duration(milliseconds: delay));
}
}
}
setState(() {
running = false;
});
}
// Map<String, >
void fillArr(double width, double margin, double height) {
for (int i = 0; i < arr.length; i++) arr[i] = null;
var rng = new Random();
for (int i = 0; i < currentSliderValue; i++) {
double val = rng.nextDouble() * height;
if (val == 0)
continue;
else
arr[i] = val;
}
blockList = [...arr.map((height) => Block(height, width, margin))];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 20),
Row(
children: [
Text(
"Length",
),
Slider(
value: currentSliderValue,
min: 5,
max: 200,
onChanged: (double value) {
setState(() {
currentSliderValue = value;
});
double newwidth =
(MediaQuery.of(context).size.width * 0.6) /
currentSliderValue;
double newmargin =
(MediaQuery.of(context).size.width * 0.1) /
currentSliderValue;
fillArr(newwidth, newmargin, widget.height * 0.7);
}),
RaisedButton(
child: Text("Insertion Sort"),
onPressed: InsertionSort,
),
RaisedButton(
onPressed: BubbleSort, child: Text("Bubble Sort")),
RaisedButton(onPressed: () {}, child: Text("Merge Sort")),
RaisedButton(onPressed: () {}, child: Text("Quick Sort")),
RaisedButton(
onPressed: () {}, child: Text("Counting Sort")),
RaisedButton(onPressed: () {}, child: Text("Radix Sort")),
RaisedButton(
onPressed: () {}, child: Text("Selection Sort")),
RaisedButton(onPressed: () {}, child: Text("Heap Sort")),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [...blockList],
),
// Row(
// children: [
// Container(
// child: Row(children: [
// Text("Algorithm")
// ],)
// )]
// ),
],
),
),
);
}
}
Here's the Block class:
import 'package:flutter/material.dart';
class Block extends StatefulWidget {
Block(#required this.height, #required this.width, #required this.mar);
double height;
double width;
double mar;
#override
_BlockState createState() => _BlockState();
}
class _BlockState extends State<Block> {
Color col = Colors.blue;
// void isKey() {
// setState(() {
// col = Colors.pink;
// });
// }
// void notKey() {
// setState(() {
// col = Colors.purple;
// });
// }
#override
Widget build(BuildContext context) {
return (widget.height == null)
? Container()
: Container(
height: this.widget.height,
width: widget.width,
margin: EdgeInsets.all(widget.mar),
decoration: BoxDecoration(
color: col,
),
);
}
}
As far as which state management route to go with, it really can be done with any of them. GetX to me is the easiest and has the least boilerplate.
Here's one way to do this. I just updated the insertionSort method to get you started and you can go from there. Any other changes you notice in your other classes are just to get rid of linter errors.
All your methods and variables can now live in a GetX class. With the exception of color, the rest are now observable streams.
class BlockController extends GetxController {
RxDouble currentSliderValue = 50.0.obs; // adding .obs makes variable obserable
RxList arr = List(500).obs;
RxList blockList = [].obs;
RxBool running = false.obs;
Color color = Colors.red;
void insertionSort() async {
running.value = true; // adding .value access the value of observable variable
color = Colors.blue;
int delay = (pow(15, 4) / pow(currentSliderValue.value, 2)).round();
for (int i = 1; i < currentSliderValue.value; i++) {
if (blockList[i] == null) break;
Block key = blockList[i];
int j = i - 1;
while (j >= 0 && blockList[j].height > key.height) {
blockList[j + 1] = blockList[j];
await Future.delayed(Duration(milliseconds: delay));
j--;
}
blockList[j + 1] = key;
}
color = Colors.green;
update(); // only needed for the color property because its not an observable stream
running.value = false;
}
void bubbleSort() async {
running.value = true;
int delay = (pow(15, 4) / pow(currentSliderValue.value, 2)).round();
for (int i = 0; i < currentSliderValue.value - 1; i++) {
for (int j = 0; j < currentSliderValue.value - i - 1; j++) {
if (blockList[j].height > blockList[j + 1].height) {
Block temp = blockList[j + 1];
blockList[j + 1] = blockList[j];
blockList[j] = temp;
await Future.delayed(Duration(milliseconds: delay));
}
}
}
running.value = false;
}
// Map<String, >
void fillArr(double width, double margin, double height) {
for (int i = 0; i < arr.length; i++) arr[i] = null;
var rng = new Random();
for (int i = 0; i < currentSliderValue.value; i++) {
double val = rng.nextDouble() * height;
if (val == 0)
continue;
else
arr[i] = val;
}
blockList = [...arr.map((height) => Block(height, width, margin))].obs;
}
}
Initialize the controller in your main before running your app. Generally it can be done anywhere as long as its before you try to access the controller.
Get.put(BlockController());
Here's your much less busy DashBoard now that all that logic is tucked away in a GetX class. Here we find the controller, and use it access all those variables and methods.
Obx is the GetX widget that rebuilds on changes.
class DashBoard extends StatefulWidget {
final double width;
final double height;
DashBoard(this.width, this.height);
#override
_DashBoardState createState() => _DashBoardState();
}
class _DashBoardState extends State<DashBoard> {
final controller = Get.find<BlockController>(); // finding same instance of BlockConroller that you initialized in `Main`
#override
void initState() {
super.initState();
controller.fillArr((widget.width * 0.6) / 50, (widget.width * 0.1) / 50,
widget.height * 0.7);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 50),
Obx(
// rebuilds when observable variables change
() => Column(
// changed to Column because a Row was overflowing
children: [
Text(
"Length",
),
Slider(
value: controller.currentSliderValue.value,
min: 5,
max: 200,
onChanged: (double value) {
controller.currentSliderValue.value = value;
double newwidth =
(MediaQuery.of(context).size.width * 0.6) /
controller.currentSliderValue.value;
double newmargin =
(MediaQuery.of(context).size.width * 0.1) /
controller.currentSliderValue.value;
controller.fillArr(
newwidth, newmargin, widget.height * 0.7);
}),
RaisedButton(
child: Text("Insertion Sort"),
onPressed: controller.insertionSort,
),
RaisedButton(
onPressed: controller.bubbleSort,
child: Text("Bubble Sort")),
RaisedButton(onPressed: () {}, child: Text("Merge Sort")),
RaisedButton(onPressed: () {}, child: Text("Quick Sort")),
RaisedButton(onPressed: () {}, child: Text("Counting Sort")),
RaisedButton(onPressed: () {}, child: Text("Radix Sort")),
RaisedButton(onPressed: () {}, child: Text("Selection Sort")),
RaisedButton(onPressed: () {}, child: Text("Heap Sort")),
],
),
),
Obx(
// rebuilds when observable variables change
() => Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [...controller.blockList],
),
),
// Row(
// children: [
// Container(
// child: Row(children: [
// Text("Algorithm")
// ],)
// )]
// ),
],
),
),
);
}
}
And here's your Block which can now be stateless. Key thing of note here is the GetBuilder widget that updates the color.
class Block extends StatelessWidget {
// now can be stateless
Block(this.height, this.width, this.mar);
final double height;
final double width;
final double mar;
#override
Widget build(BuildContext context) {
return (height == null)
? Container()
: GetBuilder<BlockController>(
// triggers rebuilds when update() is called from GetX class
builder: (controller) => Container(
height: this.height,
width: width,
margin: EdgeInsets.all(mar),
decoration: BoxDecoration(
color: controller.color,
),
),
);
}
}
I'm new with dart and flutter and currently I'm studying so please don't judge me for this (maybe) symple question.
I am trying to build a timer but all my code started from a stopwatch so I guess that's the first problem.
In few words, I'm trying to create a timer from 3 minutes that I can stop and start anytime I want (it suppose to be a referee tool) but my starting string is '03:00' and I can see that, so that's fine, I can't find any answer though on how the time can run from 03:00 minutes to 00:00.
Also, as you can see in my code I created a button to reset the time but it always goes back to 00:00 and not to 03:00.
Anyone who can help? I'm definitely missing something.
import 'dart:async';
import 'package:flutter/material.dart';
class NewStopWatch extends StatefulWidget {
#override
_NewStopWatchState createState() => _NewStopWatchState();
}
class _NewStopWatchState extends State<NewStopWatch> {
Stopwatch watch = Stopwatch();
Timer timer;
bool startStop = true;
IconData btnPlayStatus = Icons.play_arrow;
String elapsedTime = '03:00';
updateTime(Timer timer) {
if (watch.isRunning) {
setState(() {
elapsedTime = transformMilliSeconds(watch.elapsedMilliseconds);
});
}
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
Text(elapsedTime, style: TextStyle(fontSize: 60.0)),
SizedBox(height: 20.0),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 50,
child: FloatingActionButton(
shape: ContinuousRectangleBorder(),
heroTag: "btn1",
backgroundColor: Colors.blueGrey,
onPressed: () => startOrStop(),
child: Icon(btnPlayStatus)),
),
SizedBox(width: 20.0),
Container(
width: 30,
height: 50,
child: FloatingActionButton(
shape: ContinuousRectangleBorder(),
heroTag: "btn2",
backgroundColor: Colors.blueGrey,
onPressed: () => resetWatch(), //resetWatch,
child: Icon(Icons.subdirectory_arrow_left)),
),
],
)
],
),
);
}
resetWatch() {
setState(() {
watch.reset();
setTime();
});
}
startOrStop() {
if(startStop) {
setState(() {
btnPlayStatus = Icons.pause;
});
startWatch();
} else {
setState(() {
btnPlayStatus = Icons.play_arrow;
});
stopWatch();
}
}
startWatch() {
setState(() {
startStop = false;
watch.start();
timer = Timer.periodic(Duration(milliseconds: 100), updateTime);
});
}
stopWatch() {
setState(() {
startStop = true;
watch.stop();
setTime();
});
}
setTime() {
var timeSoFar = watch.elapsedMilliseconds;
setState(() {
elapsedTime = transformMilliSeconds(timeSoFar);
});
}
transformMilliSeconds(int milliseconds) {
int hundreds = (milliseconds / 10).truncate();
int seconds = (hundreds / 100).truncate();
int minutes = (seconds / 60).truncate();
String minutesStr = (minutes % 60).toString().padLeft(2, '0');
String secondsStr = (seconds % 60).toString().padLeft(2, '0');
return "$minutesStr:$secondsStr";
}
}
First of all you need to think if you always want to start from 3 minutes; if so create a static field as follows:
static duration = new Duration(minutes:3);
Edit: I've refactored your code and made it working.
updateTimer(Timer t) {
if (watch.isRunning) {
setState(() {
Duration newDuration = _NewStopWatchState.duration -
new Duration(milliseconds: watch.elapsedMilliseconds);
elapsedTime = durationToMinutesAndSeconds(newDuration);
});
}
}
This is the updateTimer function.
Next, you didn't update your stopWatch() function to take care of the new changes so I changed it for you.
stopWatch() {
setState(() {
startStop = true;
watch.stop();
Duration newDuration = _NewStopWatchState.duration -
new Duration(milliseconds: watch.elapsedMilliseconds);
elapsedTime = durationToMinutesAndSeconds(newDuration);
});
}
I've also updated the resetWatch() function
resetWatch() {
setState(() {
watch.reset();
elapsedTime = durationToMinutesAndSeconds(_NewStopWatchState.duration);
});
}
I've also created an utility function to convert a duration to minutes and seconds.
String durationToMinutesAndSeconds(Duration d) {
return "${d.inMinutes.toString().padLeft(2, '0')}" +
":${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
}
I've tried it on my machine and the code is working, hope this time is working even on your side.
That's the modified code:
import 'dart:async';
import 'package:flutter/material.dart';
class NewStopWatch extends StatefulWidget {
#override
_NewStopWatchState createState() => _NewStopWatchState();
}
class _NewStopWatchState extends State<NewStopWatch> {
Stopwatch watch = Stopwatch();
Timer timer;
bool startStop = true;
static Duration duration = Duration(minutes:3);
IconData btnPlayStatus = Icons.play_arrow;
String elapsedTime = '';
updateTimer() {
if (watch.isRunning) {
setState(() {
elapsedTime = transformMilliSeconds(_NewStopWatchState.duration.inMilliseconds - watch.elapsedMilliseconds);
});
}
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
Text(elapsedTime),
SizedBox(height: 20.0),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 50,
child: FloatingActionButton(
shape: ContinuousRectangleBorder(),
heroTag: "btn1",
backgroundColor: Colors.blueGrey,
onPressed: () => startOrStop(),
child: Icon(btnPlayStatus)),
),
SizedBox(width: 20.0),
Container(
width: 30,
height: 50,
child: FloatingActionButton(
shape: ContinuousRectangleBorder(),
heroTag: "btn2",
backgroundColor: Colors.blueGrey,
onPressed: () => resetWatch(), //resetWatch,
child: Icon(Icons.subdirectory_arrow_left)),
),
],
),
],
),
);
}
resetWatch() {
setState(() {
watch.reset();
setTime();
elapsedTime="${_NewStopWatchState.duration
.inMinutes}:${_NewStopWatchState.duration.inSeconds.remainder(60)}";
});
}
startOrStop() {
if(startStop) {
setState(() {
btnPlayStatus = Icons.pause;
});
startWatch();
} else {
setState(() {
btnPlayStatus = Icons.play_arrow;
});
stopWatch();
}
}
startWatch() {
setState(() {
startStop = false;
watch.start();
timer = Timer.periodic(Duration(milliseconds: 100), updateTimer());
});
}
stopWatch() {
setState(() {
startStop = true;
watch.stop();
setTime();
});
}
setTime() {
var timeSoFar = watch.elapsedMilliseconds;
setState(() {
elapsedTime = transformMilliSeconds(timeSoFar);
});
}
transformMilliSeconds(int milliseconds) {
int hundreds = (milliseconds / 10).truncate();
int seconds = (hundreds / 100).truncate();
int minutes = (seconds / 60).truncate();
String minutesStr = (minutes % 60).toString().padLeft(2, '0');
String secondsStr = (seconds % 60).toString().padLeft(2, '0');
return "$minutesStr:$secondsStr";
}
}
If you want a stop watch also with timer you can use my easy github project.
I hope this will will help some one. Thank you.
Here you can get my github repo
So from past few day I have been trying to get around this logical loop, and have tried everything I could and then finally decided to post it here (hence made my debut here). I am stuck, all the help is appreciated. Thanks !
Logic: In the app there are 2 container which displays White's and black's time, which is selected from a given five options -> 5,10,15,30,60, which then update's the time to display in those containers, used a provider package for this and everything else.
Now I have also added a Raised button named 'switch', which when pressed was supposed to:
Start's the white's countdown timer
Then if pressed again should stops white's timer and then start Black's countdown timer
Then if pressed again should stop the black's timer and then starts white's timer.
And repeat till the timer runs out and one out of the two is declared a winner.
Problem:
So with what I coded so far, when 'switch' is pressed it starts white's timer and if pressed again stops white's timer but it doesn't begins black's timer. I know its because of the way I have framed the if() conditions and also because I don't know how to stop the timer from outside. What I have done is to use a - bool checkTimerW and checkTimerB for each white and black, which I check in the if() condition to cancel the timer is based on it.
Code:
Provider -
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'),
),
),
),
],
),
],
),
),
],
),
),
);
}
}
I have coded this in the past. It might help you.
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 ?? '??');
});
}
}
I coded it withoud provider (Using only ValueNotifier) just to show you the logic
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(),
)
],
),
)
),
);
}
}
The idea is the same but what I want to show you is that you can use only one timer to do the whole logic and with some enum value (White and Black) change between both minutes.
The button change the turn of the player (switchPlayer method) and inside the timer you see that depending the turn of the player it reduces its time _isWhiteTurn ? --_whiteTime : --_blackTime;. As a ValueNotifier it updates only when value changes, but you can use your Provider with ChangeNotifier and update when you want (and it's better, because when changing player in my example I still have to wait for the second to end so the timer updates the text).
You can try change something like this with an enum to simplify the timer logic
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();
}
That way you have only one timer the whole match that will cancel when one of the timer gets to 0 (or if someone loese, but thats other logic in some other Provider I guess)
UPDATE
You can change the timeLeft getter to something like this
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
}
}
That way it will return a String with both times and only the timer of the player in turn will change each second. But as I saig try this logic with ChangeNotifierProvider and it should work too, and you can consume it in differents parts of your widget tree
I'm looking to recreate Snapchat's back-to-back video format in Flutter. Since video_player is lacking callbacks for when the video finishes (and is otherwise prone to callback hell), I was wondering if anyone has some pointers for building something like this.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyHomePage(),
));
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<VideoPlayerController> _controllers = [];
VoidCallback listener;
bool _isPlaying = false;
int _current = 0;
#override
void initState() {
super.initState();
// Add some sample videos
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
this.tick();
// Try refreshing by brute force (this isn't going too well)
new Timer.periodic(Duration(milliseconds: 100), (Timer t) {
int delta = 99999999;
if(_controllers[_current].value != null) {
delta = (_controllers[_current].value.duration.inMilliseconds - _controllers[_current].value.position.inMilliseconds);
}
print("Tick " + delta.toString());
if(delta < 500) {
_current += 1;
this.tick();
}
});
}
void tick() async {
print("Current: " + _current.toString());
await _controllers[_current].initialize();
await _controllers[_current].play();
print("Ready");
setState((){
_current = _current;
});
}
#override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: _controllers[_current].value.aspectRatio,
// Use the VideoPlayer widget to display the video
child: VideoPlayer(_controllers[_current]),
);
}
}
What I have now plays the first video, but there is a very long delay between the first and second. I believe it has to do with my inability to get rid of the listener attached to the 0th item.
Initializing a network VideoPlayerController may take some time to finish. You can initialize the controller of the next video while playing the current. This will take more memory but I don't think it will create huge problems if you prebuffer only one or two videos. Then when the next or previous buttons get pressed, video will be ready to play.
Here is my workaround. It's functional, it prebuffers previous and next videos, skips to the next video when finishes, shows the current position and buffer, pauses and plays on long press.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
main() {
runApp(MaterialApp(
home: VideoPlayerDemo(),
));
}
class VideoPlayerDemo extends StatefulWidget {
#override
_VideoPlayerDemoState createState() => _VideoPlayerDemoState();
}
class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
int index = 0;
double _position = 0;
double _buffer = 0;
bool _lock = true;
Map<String, VideoPlayerController> _controllers = {};
Map<int, VoidCallback> _listeners = {};
Set<String> _urls = {
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#3',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#5',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#6',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#7',
};
#override
void initState() {
super.initState();
if (_urls.length > 0) {
_initController(0).then((_) {
_playController(0);
});
}
if (_urls.length > 1) {
_initController(1).whenComplete(() => _lock = false);
}
}
VoidCallback _listenerSpawner(index) {
return () {
int dur = _controller(index).value.duration.inMilliseconds;
int pos = _controller(index).value.position.inMilliseconds;
int buf = _controller(index).value.buffered.last.end.inMilliseconds;
setState(() {
if (dur <= pos) {
_position = 0;
return;
}
_position = pos / dur;
_buffer = buf / dur;
});
if (dur - pos < 1) {
if (index < _urls.length - 1) {
_nextVideo();
}
}
};
}
VideoPlayerController _controller(int index) {
return _controllers[_urls.elementAt(index)];
}
Future<void> _initController(int index) async {
var controller = VideoPlayerController.network(_urls.elementAt(index));
_controllers[_urls.elementAt(index)] = controller;
await controller.initialize();
}
void _removeController(int index) {
_controller(index).dispose();
_controllers.remove(_urls.elementAt(index));
_listeners.remove(index);
}
void _stopController(int index) {
_controller(index).removeListener(_listeners[index]);
_controller(index).pause();
_controller(index).seekTo(Duration(milliseconds: 0));
}
void _playController(int index) async {
if (!_listeners.keys.contains(index)) {
_listeners[index] = _listenerSpawner(index);
}
_controller(index).addListener(_listeners[index]);
await _controller(index).play();
setState(() {});
}
void _previousVideo() {
if (_lock || index == 0) {
return;
}
_lock = true;
_stopController(index);
if (index + 1 < _urls.length) {
_removeController(index + 1);
}
_playController(--index);
if (index == 0) {
_lock = false;
} else {
_initController(index - 1).whenComplete(() => _lock = false);
}
}
void _nextVideo() async {
if (_lock || index == _urls.length - 1) {
return;
}
_lock = true;
_stopController(index);
if (index - 1 >= 0) {
_removeController(index - 1);
}
_playController(++index);
if (index == _urls.length - 1) {
_lock = false;
} else {
_initController(index + 1).whenComplete(() => _lock = false);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Playing ${index + 1} of ${_urls.length}"),
),
body: Stack(
children: <Widget>[
GestureDetector(
onLongPressStart: (_) => _controller(index).pause(),
onLongPressEnd: (_) => _controller(index).play(),
child: Center(
child: AspectRatio(
aspectRatio: _controller(index).value.aspectRatio,
child: Center(child: VideoPlayer(_controller(index))),
),
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _buffer,
color: Colors.grey,
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _position,
color: Colors.greenAccent,
),
),
],
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(onPressed: _previousVideo, child: Icon(Icons.arrow_back)),
SizedBox(width: 24),
FloatingActionButton(onPressed: _nextVideo, child: Icon(Icons.arrow_forward)),
],
),
);
}
}
All of the logic lives inside the state object, therefore makes it dirty. I might turn this into a package in the future.