Flutter: To-do List App (Part 4) - PageView Navigation
Project Structure
Source Code
lib/widgets/custom_modal_action_button.dart
lib/widgets/custom_icon_decoration.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
class CustomIconDecoration extends Decoration { | |
final double iconSize; | |
final double lineWidth; | |
final bool firstData; | |
final bool lastData; | |
CustomIconDecoration({ | |
@required double iconSize, | |
@required double lineWidth, | |
@required bool firstData, | |
@required bool lastData, | |
}) : this.iconSize = iconSize, | |
this.lineWidth = lineWidth, | |
this.firstData = firstData, | |
this.lastData = lastData; | |
@override | |
BoxPainter createBoxPainter([onChanged]) { | |
return IconLine( | |
iconSize: iconSize, | |
lineWidth: lineWidth, | |
firstData: firstData, | |
lastData: lastData); | |
} | |
} | |
class IconLine extends BoxPainter { | |
final double iconSize; | |
final bool firstData; | |
final bool lastData; | |
final Paint paintLine; | |
IconLine({ | |
@required double iconSize, | |
@required double lineWidth, | |
@required bool firstData, | |
@required bool lastData, | |
}) : this.iconSize = iconSize, | |
this.firstData = firstData, | |
this.lastData = lastData, | |
paintLine = Paint() | |
..color = Colors.red | |
..strokeCap = StrokeCap.round | |
..strokeWidth = lineWidth | |
..style = PaintingStyle.stroke; | |
@override | |
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { | |
final leftOffset = Offset((iconSize / 2) + 24, offset.dy); | |
final double iconSpace = iconSize / 1.5; | |
final Offset top = configuration.size.topLeft(Offset(leftOffset.dx, 0.0)); | |
final Offset centerTop = configuration.size | |
.centerLeft(Offset(leftOffset.dx, leftOffset.dy - iconSpace)); | |
final Offset centerBottom = configuration.size | |
.centerLeft(Offset(leftOffset.dx, leftOffset.dy + iconSpace)); | |
final Offset end = | |
configuration.size.bottomLeft(Offset(leftOffset.dx, leftOffset.dy * 2)); | |
if (!firstData) canvas.drawLine(top, centerTop, paintLine); | |
if (!lastData) canvas.drawLine(centerBottom, end, paintLine); | |
} | |
} |
lib/widgets/custom_date_time_picker.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
class CustomDateTimePicker extends StatelessWidget { | |
final VoidCallback onPressed; | |
final IconData icon; | |
final String value; | |
CustomDateTimePicker( | |
{@required this.onPressed, @required this.icon, @required this.value}); | |
@override | |
Widget build(BuildContext context) { | |
return FlatButton( | |
padding: EdgeInsets.zero, | |
onPressed: onPressed, | |
child: Padding( | |
padding: const EdgeInsets.only(left: 12.0), | |
child: Row( | |
children: <Widget>[ | |
Icon(icon, color: Theme.of(context).accentColor, size: 30), | |
SizedBox( | |
width: 12, | |
), | |
Text( | |
value, | |
style: TextStyle(fontSize: 14), | |
) | |
], | |
), | |
), | |
); | |
} | |
} |
lib/pages/add_task_page.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:todoapp/widgets/custom_date_time_picker.dart'; | |
import 'package:todoapp/widgets/custom_modal_action_button.dart'; | |
import 'package:todoapp/widgets/custom_textfield.dart'; | |
class AddTaskPage extends StatefulWidget { | |
@override | |
_AddTaskPageState createState() => _AddTaskPageState(); | |
} | |
class _AddTaskPageState extends State<AddTaskPage> { | |
String _selectedDate = 'Pick date'; | |
String _selectedTime = 'Pick time'; | |
Future _pickDate() async { | |
DateTime datepick = await showDatePicker( | |
context: context, | |
initialDate: new DateTime.now(), | |
firstDate: new DateTime.now().add(Duration(days: -365)), | |
lastDate: new DateTime.now().add(Duration(days: 365))); | |
if (datepick != null) | |
setState(() { | |
_selectedDate = datepick.toString(); | |
}); | |
} | |
Future _pickTime() async { | |
TimeOfDay timepick = await showTimePicker( | |
context: context, initialTime: new TimeOfDay.now()); | |
if (timepick != null) { | |
setState(() { | |
_selectedTime = timepick.toString(); | |
}); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.all(24.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Center( | |
child: Text( | |
"Add new task", | |
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | |
)), | |
SizedBox( | |
height: 24, | |
), | |
CustomTextField(labelText: 'Enter task name'), | |
SizedBox(height: 12), | |
CustomDateTimePicker( | |
icon: Icons.date_range, | |
onPressed: _pickDate, | |
value: _selectedDate, | |
), | |
CustomDateTimePicker( | |
icon: Icons.access_time, | |
onPressed: _pickTime, | |
value: _selectedTime, | |
), | |
SizedBox( | |
height: 24, | |
), | |
CustomModalActionButton( | |
onClose: () { | |
Navigator.of(context).pop(); | |
}, | |
onSave: () {}, | |
) | |
], | |
), | |
); | |
} | |
} |
lib/pages/add_event_page.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:todoapp/widgets/custom_date_time_picker.dart'; | |
import 'package:todoapp/widgets/custom_modal_action_button.dart'; | |
import 'package:todoapp/widgets/custom_textfield.dart'; | |
class AddEventPage extends StatefulWidget { | |
@override | |
_AddEventPageState createState() => _AddEventPageState(); | |
} | |
class _AddEventPageState extends State<AddEventPage> { | |
String _selectedDate = 'Pick date'; | |
String _selectedTime = 'Pick time'; | |
Future _pickDate() async { | |
DateTime datepick = await showDatePicker( | |
context: context, | |
initialDate: new DateTime.now(), | |
firstDate: new DateTime.now().add(Duration(days: -365)), | |
lastDate: new DateTime.now().add(Duration(days: 365))); | |
if (datepick != null) | |
setState(() { | |
_selectedDate = datepick.toString(); | |
}); | |
} | |
Future _pickTime() async { | |
TimeOfDay timepick = await showTimePicker( | |
context: context, initialTime: new TimeOfDay.now()); | |
if (timepick != null) { | |
setState(() { | |
_selectedTime = timepick.toString(); | |
}); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.all(24.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Center( | |
child: Text( | |
"Add new event", | |
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | |
)), | |
SizedBox( | |
height: 24, | |
), | |
CustomTextField(labelText: 'Enter event name'), | |
SizedBox(height: 12), | |
CustomTextField(labelText: 'Enter description'), | |
SizedBox(height: 12), | |
CustomDateTimePicker( | |
icon: Icons.date_range, | |
onPressed: _pickDate, | |
value: _selectedDate, | |
), | |
CustomDateTimePicker( | |
icon: Icons.access_time, | |
onPressed: _pickTime, | |
value: _selectedTime, | |
), | |
SizedBox( | |
height: 24, | |
), | |
CustomModalActionButton( | |
onClose: () { | |
Navigator.of(context).pop(); | |
}, | |
onSave: () {}, | |
) | |
], | |
), | |
); | |
} | |
} |
lib/pages/event_page.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:todoapp/widgets/custom_icon_decoration.dart'; | |
class EventPage extends StatefulWidget { | |
@override | |
_EventPageState createState() => _EventPageState(); | |
} | |
class Event { | |
final String time; | |
final String task; | |
final String desc; | |
final bool isFinish; | |
const Event(this.time, this.task, this.desc, this.isFinish); | |
} | |
final List<Event> _eventList = [ | |
new Event("08:00", "Have coffe with Sam", "Personal", true), | |
new Event("10:00", "Meet with sales", "Work", true), | |
new Event("12:00", "Call Tom about appointment", "Work", false), | |
new Event("14:00", "Fix onboarding experience", "Work", false), | |
new Event("16:00", "Edit API documentation", "Personal", false), | |
new Event("18:00", "Setup user focus group", "Personal", false), | |
]; | |
class _EventPageState extends State<EventPage> { | |
@override | |
Widget build(BuildContext context) { | |
double iconSize = 20; | |
return ListView.builder( | |
itemCount: _eventList.length, | |
padding: const EdgeInsets.all(0), | |
itemBuilder: (context, index) { | |
return Padding( | |
padding: const EdgeInsets.only(left: 24.0, right: 24), | |
child: Row( | |
children: <Widget>[ | |
_lineStyle(context, iconSize, index, _eventList.length, | |
_eventList[index].isFinish), | |
_displayTime(_eventList[index].time), | |
_displayContent(_eventList[index]) | |
], | |
), | |
); | |
}, | |
); | |
} | |
Widget _displayContent(Event event) { | |
return Expanded( | |
child: Padding( | |
padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), | |
child: Container( | |
padding: const EdgeInsets.all(14.0), | |
decoration: BoxDecoration( | |
color: Colors.white, | |
borderRadius: BorderRadius.all(Radius.circular(12)), | |
boxShadow: [ | |
BoxShadow( | |
color: Color(0x20000000), | |
blurRadius: 5, | |
offset: Offset(0, 3)) | |
]), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
Text(event.task), | |
SizedBox( | |
height: 12, | |
), | |
Text(event.desc) | |
], | |
), | |
), | |
), | |
); | |
} | |
Widget _displayTime(String time) { | |
return Container( | |
width: 80, | |
child: Padding( | |
padding: const EdgeInsets.only(left: 8.0), | |
child: Text(time), | |
)); | |
} | |
Widget _lineStyle(BuildContext context, double iconSize, int index, | |
int listLength, bool isFinish) { | |
return Container( | |
decoration: CustomIconDecoration( | |
iconSize: iconSize, | |
lineWidth: 1, | |
firstData: index == 0 ?? true, | |
lastData: index == listLength - 1 ?? true), | |
child: Container( | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.all(Radius.circular(50)), | |
boxShadow: [ | |
BoxShadow( | |
offset: Offset(0, 3), | |
color: Color(0x20000000), | |
blurRadius: 5) | |
]), | |
child: Icon( | |
isFinish | |
? Icons.fiber_manual_record | |
: Icons.radio_button_unchecked, | |
size: iconSize, | |
color: Theme.of(context).accentColor), | |
)); | |
} | |
} |
lib/main.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:todoapp/pages/add_event_page.dart'; | |
import 'package:todoapp/pages/add_task_page.dart'; | |
import 'package:todoapp/pages/event_page.dart'; | |
import 'package:todoapp/pages/task_page.dart'; | |
import 'package:todoapp/widgets/custom_button.dart'; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatelessWidget { | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
title: 'Flutter Demo', | |
theme: ThemeData(primarySwatch: Colors.red, fontFamily: "Montserrat"), | |
home: MyHomePage(), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
@override | |
_MyHomePageState createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
PageController _pageController = PageController(); | |
double currentPage = 0; | |
@override | |
Widget build(BuildContext context) { | |
_pageController.addListener(() { | |
setState(() { | |
currentPage = _pageController.page; | |
}); | |
}); | |
return Scaffold( | |
body: Stack( | |
children: <Widget>[ | |
Container( | |
height: 35, | |
color: Theme.of(context).accentColor, | |
), | |
Positioned( | |
right: 0, | |
child: Text( | |
"6", | |
style: TextStyle(fontSize: 200, color: Color(0x10000000)), | |
), | |
), | |
_mainContent(context), | |
], | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: () { | |
showDialog( | |
barrierDismissible: false, | |
context: context, | |
builder: (BuildContext context) { | |
return Dialog( | |
child: currentPage == 0 ? AddTaskPage() : AddEventPage(), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.all(Radius.circular(12)))); | |
}); | |
}, | |
child: Icon(Icons.add), | |
), | |
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, | |
bottomNavigationBar: BottomAppBar( | |
shape: CircularNotchedRectangle(), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: <Widget>[ | |
IconButton( | |
icon: Icon(Icons.settings), | |
onPressed: () {}, | |
), | |
IconButton( | |
icon: Icon(Icons.more_vert), | |
onPressed: () {}, | |
) | |
], | |
), | |
), | |
// This trailing comma makes auto-formatting nicer for build methods. | |
); | |
} | |
Widget _mainContent(BuildContext context) { | |
return Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
SizedBox(height: 60), | |
Padding( | |
padding: const EdgeInsets.all(24.0), | |
child: Text( | |
"Monday", | |
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(24.0), | |
child: _button(context), | |
), | |
Expanded( | |
child: PageView( | |
controller: _pageController, | |
children: <Widget>[TaskPage(), EventPage()], | |
)) | |
], | |
); | |
} | |
Widget _button(BuildContext context) { | |
return Row( | |
children: <Widget>[ | |
Expanded( | |
child: CustomButton( | |
onPressed: () { | |
_pageController.previousPage( | |
duration: Duration(milliseconds: 500), | |
curve: Curves.bounceInOut); | |
}, | |
buttonText: "Tasks", | |
color: | |
currentPage < 0.5 ? Theme.of(context).accentColor : Colors.white, | |
textColor: | |
currentPage < 0.5 ? Colors.white : Theme.of(context).accentColor, | |
borderColor: currentPage < 0.5 | |
? Colors.transparent | |
: Theme.of(context).accentColor, | |
)), | |
SizedBox( | |
width: 32, | |
), | |
Expanded( | |
child: CustomButton( | |
onPressed: () { | |
_pageController.nextPage( | |
duration: Duration(milliseconds: 500), | |
curve: Curves.bounceInOut); | |
}, | |
buttonText: "Events", | |
color: | |
currentPage > 0.5 ? Theme.of(context).accentColor : Colors.white, | |
textColor: | |
currentPage > 0.5 ? Colors.white : Theme.of(context).accentColor, | |
borderColor: currentPage > 0.5 | |
? Colors.transparent | |
: Theme.of(context).accentColor, | |
)) | |
], | |
); | |
} | |
} |
Comments
Post a Comment