├── README.md
├── example
├── model
│ └── Account.dart
└── view
│ ├── AccountDetail.dart
│ ├── AccountList.dart
│ └── AccountNodeData.dart
├── lib
└── src
│ ├── BasicPage.dart
│ ├── CustomExpansionTile.dart
│ ├── CustomListTile.dart
│ ├── Global.dart
│ ├── SearchBar.dart
│ └── TreeView.dart
└── pubspec.yaml
/README.md:
--------------------------------------------------------------------------------
1 | # piggy treeview
2 |
3 | A Treeview component for Flutter, featuring expand/collapse all, search, hilite, checkbox.
4 |
5 | ## Getting Started
6 |
7 | How to use?
8 | 1. Import TreeView.dart.
9 | 2. Derive TreeNodeData to publish your own data.
10 | 3. See AccountList.dart for the example detail.
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/model/Account.dart:
--------------------------------------------------------------------------------
1 | class Account{
2 | String username;
3 | String email;
4 | String phone;
5 | String type;
6 | String id;
7 | String parent_id;
8 | String full_path;
9 |
10 | Account(this.username, this.email, this.phone, this.type, this.id, this.parent_id, this.full_path );
11 | }
--------------------------------------------------------------------------------
/example/view/AccountDetail.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:piggy/common/BasicPage.dart';
3 | import 'package:piggy/common/Global.dart';
4 | import 'package:validator/validator.dart';
5 | import 'AccountNodeData.dart';
6 |
7 | class AccountDetail extends BasicPage {
8 | AccountNodeData _account;
9 |
10 | AccountNodeData get currentAccount => _account;
11 |
12 | AccountDetail(String title, {AccountNodeData account})
13 | : super(title, appBar: true, enableFAB: false) {
14 | _account = account;
15 | }
16 |
17 | @override
18 | AccountDetailState createState() => new AccountDetailState();
19 | }
20 |
21 | class AccountDetailState extends BasicPageState {
22 | final GlobalKey _formKey = new GlobalKey();
23 | bool _autovalidate = false;
24 |
25 | @override
26 | void initState() {
27 | super.initState();
28 | }
29 |
30 | @override
31 | Widget buildContent(BuildContext context) {
32 | AccountDetail theWidget = widget as AccountDetail;
33 |
34 | Widget requiredHint = new Container(
35 | padding: const EdgeInsets.only(top: 5.0, bottom: 5.0),
36 | child: new Text('* means the field is required',
37 | style: Theme
38 | .of(context)
39 | .textTheme
40 | .caption
41 | .copyWith(color: Colors.indigo)));
42 |
43 | Widget inputUserName = new TextFormField(
44 | decoration: new InputDecoration(
45 | hintText: "Type in account name",
46 | hintStyle: new TextStyle(color: Colors.black26),
47 | labelText: "Account Name *",
48 | labelStyle: new TextStyle(color: Colors.black),
49 | errorStyle: new TextStyle(color: Colors.red),
50 | ),
51 | initialValue: theWidget.currentAccount.data.username,
52 | onSaved: (String value) {
53 | theWidget.currentAccount.data.username = value;
54 | },
55 | validator: null,
56 | );
57 |
58 | Widget inputEmail = new TextFormField(
59 | decoration: new InputDecoration(
60 | hintText: "Email",
61 | hintStyle: new TextStyle(color: Colors.black26),
62 | labelText: "Email",
63 | labelStyle: new TextStyle(color: Colors.black),
64 | errorStyle: new TextStyle(color: Colors.red),
65 | ),
66 | initialValue: theWidget.currentAccount.data.email,
67 | onSaved: (String value) {
68 | theWidget.currentAccount.data.email = value;
69 | },
70 | validator: null,
71 | );
72 |
73 | Widget inputPhone = new TextFormField(
74 | decoration: new InputDecoration(
75 | hintText: "Contact number",
76 | hintStyle: new TextStyle(color: Colors.black26),
77 | labelText: "Contact Number *",
78 | labelStyle: new TextStyle(color: Colors.black),
79 | errorStyle: new TextStyle(color: Colors.red),
80 | ),
81 | initialValue: theWidget.currentAccount.data.phone,
82 | onSaved: (String value) {
83 | theWidget.currentAccount.data.phone = value;
84 | },
85 | validator: null,
86 | );
87 |
88 | Widget _getDropDownType() {
89 | return new Row(
90 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
91 | children: [
92 | const Text("Account type"),
93 | new DropdownButton(
94 | value: isNull(theWidget.currentAccount.data.type)
95 | ? ""
96 | : theWidget.currentAccount.data.type,
97 | onChanged: (String newValue) {
98 | setState(() {
99 | theWidget.currentAccount.data.type = newValue;
100 | });
101 | },
102 | items: ["master", "slaver", "root"].map((String value) {
103 | return new DropdownMenuItem(
104 | value: value,
105 | child: new Text(value == 'master' || value == "root"
106 | ? "Master"
107 | : 'Slaver'),
108 | );
109 | }).toList(),
110 | ),
111 | ],
112 | );
113 | }
114 |
115 | Widget inputType = _getDropDownType();
116 |
117 | Widget _getDropDownParent() {
118 | return new Row(
119 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
120 | children: [
121 | const Text("Direct master"),
122 | new DropdownButton(
123 | value: isNull(theWidget.currentAccount.data.parent_id)
124 | ? ""
125 | : theWidget.currentAccount.data.parent_id,
126 | onChanged: (String newValue) {
127 | setState(() {
128 | theWidget.currentAccount.data.parent_id = newValue;
129 | });
130 | },
131 | items: [
132 | isNull(theWidget.currentAccount.data.parent_id)
133 | ? ""
134 | : theWidget.currentAccount.data.parent_id
135 | ].map((String value) {
136 | return new DropdownMenuItem(
137 | value: value,
138 | child: new Text(value),
139 | );
140 | }).toList(),
141 | ),
142 | ],
143 | );
144 | }
145 |
146 | Widget inputParent = _getDropDownParent();
147 |
148 | Widget cmdButtons = new Padding(
149 | padding: new EdgeInsets.symmetric(vertical: Constants.VERTICAL_PADDING),
150 | child:
151 | new Row(mainAxisAlignment: MainAxisAlignment.end, children: [
152 | new RaisedButton(child: const Text("Save"), onPressed: _onDone),
153 | const SizedBox(
154 | width: 12.0,
155 | ),
156 | new RaisedButton(child: const Text("Cancel"), onPressed: _onCancel)
157 | ]),
158 | );
159 |
160 | return new Container(
161 | padding: new EdgeInsets.symmetric(
162 | vertical: Constants.VERTICAL_PADDING_FORM,
163 | horizontal: Constants.HORIZONTAL_PADDING_FORM),
164 | child: new Form(
165 | key: _formKey,
166 | autovalidate: _autovalidate,
167 | child: new Scrollbar(
168 | child: new ListView(children: [
169 | requiredHint,
170 | inputUserName,
171 | inputEmail,
172 | inputPhone,
173 | inputType,
174 | inputParent,
175 | cmdButtons
176 | ]))));
177 | }
178 |
179 | // Event handlers
180 | _onDone() {
181 | Navigator.pop(context);
182 | }
183 |
184 | _onCancel() {
185 | Navigator.pop(context);
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/example/view/AccountList.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter/material.dart';
3 | import 'package:piggy/common/BasicPage.dart';
4 | import 'package:piggy/common/Global.dart';
5 | import 'package:piggy/common/TreeView.dart';
6 | import 'package:piggy/model/Account.dart';
7 | import 'AccountNodeData.dart';
8 | import 'AccountDetail.dart';
9 | import 'package:piggy/common/SearchBar.dart';
10 |
11 | class AccountList extends BasicPage {
12 | AccountList(String title, {Key key})
13 | : super(title,
14 | key: key,
15 | actions: [
16 | ActionTypes.delete,
17 | ActionTypes.expandAll,
18 | ActionTypes.collapseAll
19 | ],
20 | enableFAB: true,
21 | appBar: true);
22 |
23 | @override
24 | AccountListState createState() => new AccountListState();
25 | }
26 |
27 | class AccountListState extends BasicPageState {
28 | List _lstAccount;
29 |
30 | //Callback as highlighting a node
31 | void _onHiliteNode(dynamic node) {
32 | assert(null != node);
33 |
34 | bool isExist = _mapHilitedNodes.containsKey(node.id);
35 | if (isExist)
36 | // flip/flop hilited state
37 | _mapHilitedNodes.remove(node.id);
38 | else if (_mapHilitedNodes.length > 0) {
39 | _mapHilitedNodes.forEach((key, value) {
40 | value.hilited = false;
41 | });
42 | _treeComponent.broadcast(_mapHilitedNodes);
43 |
44 | _mapHilitedNodes.clear();
45 | // add it
46 | // node id is actually the full path
47 | _mapHilitedNodes[node.id] = node;
48 | } else
49 | _mapHilitedNodes[node.id] = node;
50 |
51 | // print("On hilte node call back:${_mapHilitedNodes.length}");
52 | }
53 |
54 | //Callback as selecting a node by ticking the respective checkbox
55 | void _onSelectNode(dynamic node) {
56 | assert(null != node);
57 | setState(() {
58 | bool isExist = _mapSelectNodes.containsKey(node.id);
59 | if (node.isChecked)
60 | // add it
61 | // node id is actually the full path
62 | _mapSelectNodes[node.id] = node;
63 | else if (isExist) _mapSelectNodes.remove(node.id);
64 |
65 | widget.enableDelete = _mapSelectNodes.length > 0 ? true : false;
66 | });
67 |
68 | print("On select node call back:${_mapSelectNodes.length}");
69 | }
70 |
71 | void _onCollapseAll() {
72 | _treeComponent.expandIt = false;
73 | _treeComponent.toggle(_treeComponent.expandIt);
74 | }
75 |
76 | void _onExpandAll() {
77 | _treeComponent.expandIt = true;
78 | _treeComponent.toggle(_treeComponent.expandIt);
79 | }
80 |
81 | void _onAddNew() {
82 | Util.alert(context);
83 | }
84 |
85 | void _onDelete() {
86 | Util.alert(context, content: "Delete");
87 | }
88 |
89 | void _onSearch(String textToSearch) {
90 | setState(() {
91 | widget.isLoading = true;
92 | });
93 |
94 | Iterable foundNodes = this | textToSearch;
95 |
96 | // Turn off hilited nodes
97 | if (_mapHilitedNodes.length > 0) {
98 | _mapHilitedNodes.forEach((key, value) {
99 | value.hilited = false;
100 | });
101 | _treeComponent.broadcast(_mapHilitedNodes);
102 | }
103 | // Hilite new found nodes
104 | _mapHilitedNodes.clear();
105 | foundNodes?.forEach((element) {
106 | _mapHilitedNodes[element.id] = element;
107 | });
108 |
109 | _treeComponent.broadcast(_mapHilitedNodes);
110 |
111 | if (_mapHilitedNodes.length > 0) _onExpandAll();
112 |
113 | setState(() {
114 | widget.isLoading = false;
115 | });
116 | }
117 |
118 | void _onEdit(AccountNodeData node) {
119 | setState(() {
120 | print(node.toString());
121 | });
122 | _toggleFAB();
123 |
124 | Navigator.push(
125 | context,
126 | new MaterialPageRoute(
127 | builder: (context) => new AccountDetail(
128 | Constants.TITLE_ACCOUNT_DETAIL_PAGE,
129 | account: node),
130 | fullscreenDialog: true));
131 | _toggleFAB();
132 | }
133 |
134 | void _toggleFAB() {
135 | setState(() => widget.enableFAB = !widget.enableFAB);
136 | }
137 |
138 | void initListItems() {
139 | _lstAccount = [
140 | new Account(
141 | "Root", "root@gmail.com", "0988877766", "root", "0", "", "\$0"),
142 | new Account("Master1", "master1@gmail.com", "0988877766", "master", "1",
143 | "0", "\$0\$1"),
144 | new Account("Master11", "master11@gmail.com", "0988877766", "master",
145 | "11", "1", "\$0\%1\$11"),
146 | new Account("Master12", "master12@gmail.com", "0988877766", "master",
147 | "12", "1", "\$0\$1\$12"),
148 | new Account("Slaver13", "slaver13@gmail.com", "0988877766", "slaver",
149 | "13", "1", "\$0\$1\$13"),
150 | new Account("Master2", "master2@gmail.com", "0988877766", "master", "2",
151 | "0", "\$0\$2"),
152 | new Account("Slaver21", "slaver21@gmail.com", "0988877766", "slaver",
153 | "21", "2", "\$0\$2\$21"),
154 | new Account("Slaver22", "slaver22@gmail.com", "0988877766", "slaver",
155 | "22", "2", "\$0\$2\$22"),
156 | new Account("Master23", "master23@gmail.com", "0988877766", "master",
157 | "23", "2", "\$0\$2\$23"),
158 | new Account("Master30", "master30@gmail.com", "0988877766", "master",
159 | "30", "23", "\$0\$2\$23\$30"),
160 | ];
161 | }
162 |
163 | @override
164 | void initState() {
165 | super.initState();
166 |
167 | initListItems();
168 |
169 | // Call API to get the list of shops
170 | Future future = _getAccountList();
171 |
172 | future.then((value) {
173 | _treeComponent = new TreeView.multipleRoots(_lstTreeNode,
174 | header: new Heading(
175 | key: new Key("PiggyHeader"),
176 | searchCallback: _onSearch,
177 | ));
178 | _treeComponent.expandIt = false;
179 | _treeComponent.onSelectNode = _onSelectNode;
180 | _treeComponent.onEditNode = _onEdit;
181 | _treeComponent.onHiliteNode = _onHiliteNode;
182 | })
183 | ..catchError((error) => print(error));
184 |
185 | _displayLongPressGuide();
186 | }
187 |
188 | Future _displayLongPressGuide() async {
189 | showInSnackBar("Press long on an account to edit.");
190 | }
191 |
192 | @override
193 | void didUpdateWidget(covariant AccountList oldWidget) {
194 | super.didUpdateWidget(oldWidget);
195 | if (_onDelete != null && widget.onDelete == null)
196 | widget.onDelete = _onDelete;
197 | if (_onExpandAll != null && widget.onExpandAll == null)
198 | widget.onExpandAll = _onExpandAll;
199 | if (_onCollapseAll != null && widget.onCollapseAll == null)
200 | widget.onCollapseAll = _onCollapseAll;
201 | if (_onAddNew != null && widget.onFABressed == null)
202 | widget.onFABressed = _onAddNew;
203 | if (_lstTreeNode.length > 0) widget.enableExpandCollap = true;
204 | }
205 |
206 | @override
207 | Widget buildContent(BuildContext context) {
208 | return new Container(
209 | child: new Scrollbar(child: _treeComponent),
210 | );
211 | }
212 |
213 | TreeView _treeComponent;
214 | List _lstTreeNode;
215 |
216 | Map _mapSelectNodes = {};
217 | Map get selectedNodes => _mapSelectNodes;
218 |
219 | Map _mapHilitedNodes = {};
220 | Map get hilitedNodes => _mapHilitedNodes;
221 |
222 | _AccountMap _mapAccount;
223 | // API calls
224 | Future _getAccountList() async {
225 | setState(() => widget.isLoading = true);
226 |
227 | _mapAccount = new _AccountMap(_lstAccount);
228 |
229 | _processTreeData();
230 |
231 | setState(() => widget.isLoading = false);
232 | }
233 |
234 | void _processTreeData() {
235 | // Make sure the list is empty
236 | if (_lstTreeNode != null) {
237 | _lstTreeNode.clear();
238 | _lstTreeNode = null;
239 | }
240 | _lstTreeNode = [];
241 |
242 | // Get all roots first
243 | _mapAccount.interMap?.forEach((String key, Account account) {
244 | if (account.parent_id == null || account.parent_id.isEmpty) {
245 | // root _lstTreeNode: e.g. "$1"
246 | AccountNodeData root = new AccountNodeData.root(account);
247 | _lstTreeNode.add(root);
248 | }
249 | });
250 |
251 | void _continueBuildTree(AccountNodeData node) {
252 | List lstAccountNodeDataTemp = [];
253 |
254 | Iterable children = _mapAccount ^ node.id;
255 |
256 | children?.forEach((Account account) {
257 | var theChild = node.createChild(
258 | account.username, account.email, account.full_path, account);
259 | lstAccountNodeDataTemp.add(theChild);
260 | });
261 |
262 | // Recursively build tree.
263 | lstAccountNodeDataTemp.forEach((node) => _continueBuildTree(node));
264 | }
265 |
266 | _lstTreeNode.forEach((node) => _continueBuildTree(node));
267 | }
268 |
269 | // Util
270 | // Search the tree for the text
271 | Iterable operator |(String textToSearch) sync* {
272 | if (_lstTreeNode?.length > 0) {
273 | for (AccountNodeData account in _lstTreeNode) {
274 | String cat =
275 | "${account.data.username} ${account.data.email} ${account.data.type} ${account.data.phone}";
276 | if (cat.toLowerCase().indexOf(textToSearch.toLowerCase()) > -1) {
277 | account.hilited = true;
278 | yield account;
279 | }
280 | if (account.hasChildren)
281 | for (AccountNodeData acc in account.children)
282 | yield* acc | textToSearch;
283 | }
284 | } else
285 | yield null;
286 | }
287 | }
288 |
289 | class _AccountMap {
290 | Map _mAccount;
291 |
292 | _AccountMap(List lstAccount) {
293 | if (lstAccount?.length > 0) {
294 | _mAccount = new Map.fromIterable(lstAccount,
295 | key: (Account account) => account.full_path);
296 | }
297 | }
298 | // Return all the direct children of this key
299 | Iterable operator ^(String thisKey) sync* {
300 | if (_mAccount?.length > 0) {
301 | if (_mAccount[thisKey] == null) yield null;
302 | String pattern = "$thisKey\$";
303 | for (Account account in _mAccount.values) {
304 | if (account.full_path.replaceFirst(pattern, '') == account.id)
305 | yield account;
306 | }
307 | } else
308 | yield null;
309 | }
310 |
311 | Account operator [](String key) {
312 | if (_mAccount?.length > 0)
313 | return _mAccount[key];
314 | else
315 | return null;
316 | }
317 |
318 | void operator []=(String key, Account value) {
319 | if (_mAccount != null) _mAccount[key] = value;
320 | }
321 |
322 | void clear() {
323 | if (_mAccount != null) {
324 | _mAccount.clear();
325 | _mAccount = null;
326 | }
327 | }
328 |
329 | void remove(String key) {
330 | if (_mAccount != null) _mAccount.remove(key);
331 | }
332 |
333 | int get length => _mAccount?.length;
334 |
335 | Map get interMap => _mAccount;
336 | }
337 |
--------------------------------------------------------------------------------
/example/view/AccountNodeData.dart:
--------------------------------------------------------------------------------
1 | import 'package:piggy/common/TreeView.dart' show TreeNodeData;
2 | import 'package:piggy/model/Account.dart';
3 |
4 | class AccountNodeData extends TreeNodeData {
5 | AccountNodeData.root(Account data)
6 | : super.root(data.username, data.email, data.full_path, data);
7 |
8 | AccountNodeData.node(Account data, AccountNodeData parent)
9 | : super.node(parent, data.username, data.email, data.full_path, data);
10 |
11 | @override
12 | AccountNodeData createChild(
13 | String title, String subTitle, String id, Account data,
14 | [bool expanded = false]) {
15 | var child = new AccountNodeData.node(data, this);
16 | return child;
17 | }
18 |
19 | @override
20 | String get title => data.username;
21 |
22 | @override
23 | String get subTitle =>
24 | "${data.email} ${data.phone}\n${data.type=='root' || data.type=='master'?"Agent":"Player"}\n";
25 |
26 | @override
27 | String toString() {
28 | return "${super.toString()}"
29 | "\nfullpath:${data.full_path} belongs to:${data.parent_id}";
30 | }
31 |
32 | @override
33 | Iterable operator |(String textToSearch) sync* {
34 | String cat = "${data.username} ${data.email} ${data.type} ${data.phone}";
35 | if (cat.toLowerCase().indexOf(textToSearch.toLowerCase()) > -1) {
36 | hilited = true;
37 | yield this;
38 | }
39 | if (hasChildren)
40 | for (AccountNodeData account in children) yield* account | textToSearch;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/src/BasicPage.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:piggy/common/Global.dart';
3 |
4 | typedef dynamic ActionCallBack(dynamic inputParam);
5 |
6 | enum ActionTypes { search, delete, manage, expandAll, collapseAll }
7 |
8 | class BasicPage extends StatefulWidget {
9 | final String _title;
10 | bool _isLoading = false;
11 | bool _appBar;
12 | bool _enableFAB;
13 | bool _enableExpanCollap;
14 | bool _enableDelete;
15 | bool _enableSearch;
16 | List _actions;
17 |
18 | VoidCallback onFABressed;
19 | VoidCallback onSearch;
20 | VoidCallback onDelete;
21 | VoidCallback onManage;
22 | VoidCallback onExpandAll = null;
23 | VoidCallback onCollapseAll = null;
24 |
25 | // Properties
26 | String get title => _title;
27 | bool get isLoading => _isLoading;
28 | void set isLoading(bool value) => _isLoading = value;
29 |
30 | bool get enableFAB => _enableFAB;
31 | void set enableFAB(bool value) => _enableFAB = value;
32 |
33 | bool get enableExpandCollap => _enableExpanCollap;
34 | void set enableExpandCollap(bool value) => _enableExpanCollap = value;
35 |
36 | bool get enableSearch => _enableSearch;
37 | void set enableSearch(bool value) => _enableSearch = value;
38 |
39 | bool get enableDelete => _enableDelete;
40 | void set enableDelete(bool value) => _enableDelete = value;
41 |
42 | BasicPage(this._title,
43 | {Key key,
44 | List actions,
45 | appBar = false,
46 | enableFAB = true,
47 | enableExpanCollap = false,
48 | enableSearch = true,
49 | enableDelete = false})
50 | : super(key: key) {
51 | _enableFAB = enableFAB;
52 | _appBar = appBar;
53 | _actions = actions;
54 | _enableExpanCollap = enableExpanCollap;
55 | _enableDelete = enableDelete;
56 | _enableSearch = enableSearch;
57 | }
58 |
59 | @override
60 | State createState() => new BasicPageState();
61 | }
62 |
63 | class BasicPageState extends State {
64 | final GlobalKey _scaffoldKey = new GlobalKey();
65 | // Utils
66 | void showInSnackBar(String value) {
67 | _scaffoldKey.currentState
68 | .showSnackBar(new SnackBar(content: new Text(value)));
69 | }
70 |
71 | List _buildActions() {
72 | List lstActions = [];
73 | int nIndex = 0;
74 | if (widget._actions != null)
75 | for (ActionTypes action in widget._actions) {
76 | if (action == ActionTypes.delete)
77 | lstActions.add(new IconButton(
78 | icon: const Icon(Icons.delete),
79 | tooltip: 'Delete',
80 | onPressed: widget.enableDelete ? widget.onDelete : null,
81 | ));
82 | else if (action == ActionTypes.search)
83 | lstActions.add(new IconButton(
84 | icon: const Icon(Icons.search),
85 | tooltip: 'Search',
86 | onPressed: widget.enableSearch ? widget.onSearch : null));
87 | else if (action == ActionTypes.manage)
88 | lstActions.add(new IconButton(
89 | icon: const Icon(Icons.people_outline),
90 | tooltip: 'Manage people',
91 | onPressed: widget.onManage));
92 | else if ((action == ActionTypes.expandAll ||
93 | action == ActionTypes.collapseAll) &&
94 | nIndex == 0) {
95 | lstActions.add(new PopupMenuButton(
96 | itemBuilder: (BuildContext context) =>
97 | >[
98 | new PopupMenuItem(
99 | enabled: widget.enableExpandCollap,
100 | value: ActionTypes.expandAll,
101 | child: const Text('Expand all')),
102 | new PopupMenuItem(
103 | enabled: widget.enableExpandCollap,
104 | value: ActionTypes.collapseAll,
105 | child: const Text('Collapse all'))
106 | ],
107 | onSelected: (ActionTypes doIt) {
108 | switch (doIt) {
109 | case ActionTypes.collapseAll:
110 | if (widget.onCollapseAll != null) widget.onCollapseAll();
111 | break;
112 | case ActionTypes.expandAll:
113 | if (widget.onExpandAll != null) widget.onExpandAll();
114 | break;
115 | default:
116 | break;
117 | }
118 | }));
119 | nIndex++;
120 | }
121 | }
122 | return lstActions;
123 | }
124 |
125 | @override
126 | Widget build(BuildContext context) {
127 | Widget content = buildContent(context);
128 |
129 | final ThemeData theme = Theme.of(context);
130 | final TextStyle titleStyle = theme.textTheme.subhead.copyWith(
131 | fontStyle: FontStyle.italic, color: theme.textTheme.caption.color);
132 |
133 | Widget loadingIndicatior = widget.isLoading
134 | ? new Column(
135 | children: [
136 | const LinearProgressIndicator(),
137 | new Text("Wait for a moment...", style: titleStyle)
138 | ],
139 | )
140 | : new Container();
141 |
142 | Widget stack = new SizedBox.expand(
143 | child: new Stack(
144 | children: [
145 | new Positioned.fill(child: content),
146 | new Positioned(
147 | bottom: Constants.VIEW_PADDING * 1.5,
148 | left: Constants.VIEW_PADDING * 5.2,
149 | right: Constants.VIEW_PADDING * 5.2,
150 | child: loadingIndicatior)
151 | ],
152 | ));
153 |
154 | Widget wrapperContainer;
155 | wrapperContainer = new Scaffold(
156 | appBar: widget._appBar
157 | ? new AppBar(
158 | title: new Text(widget.title), actions: _buildActions())
159 | : null,
160 | key: _scaffoldKey,
161 | floatingActionButton: widget._enableFAB
162 | ? new FloatingActionButton(
163 | onPressed: widget.onFABressed, child: const Icon(Icons.add))
164 | : null,
165 | body: new Container(child: stack));
166 |
167 | return wrapperContainer;
168 | }
169 |
170 | Widget buildContent(BuildContext context) {
171 | return null;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/lib/src/CustomExpansionTile.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2017 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/foundation.dart';
6 | import 'package:flutter/widgets.dart';
7 | import 'package:flutter/material.dart';
8 | import 'package:piggy/common/CustomListTile.dart';
9 |
10 | const Duration _kExpand = const Duration(milliseconds: 200);
11 |
12 | /// A single-line [ListTile] with a trailing button that expands or collapses
13 | /// the tile to reveal or hide the [children].
14 | ///
15 | /// This widget is typically used with [ListView] to create an
16 | /// "expand / collapse" list entry. When used with scrolling widgets like
17 | /// [ListView], a unique [PageStorageKey] must be specified to enable the
18 | /// [ExpansionTile] to save and restore its expanded state when it is scrolled
19 | /// in and out of view.
20 | ///
21 | /// See also:
22 | ///
23 | /// * [ListTile], useful for creating expansion tile [children] when the
24 | /// expansion tile represents a sublist.
25 | /// * The "Expand/collapse" section of
26 | /// .
27 | class CustomExpansionTile extends StatefulWidget {
28 | /// Creates a single-line [ListTile] with a trailing button that expands or collapses
29 | /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
30 | /// be non-null.
31 | CustomExpansionTile({
32 | Key key,
33 | this.leading,
34 | @required this.title,
35 | this.backgroundColor,
36 | this.onExpansionChanged,
37 | this.children: const [],
38 | this.trailing,
39 | this.initiallyExpanded: false,
40 | })
41 | : assert(initiallyExpanded != null),
42 | super(key: key);
43 |
44 | /// A widget to display before the title.
45 | ///
46 | /// Typically a [CircleAvatar] widget.
47 | final Widget leading;
48 |
49 | /// The primary content of the list item.
50 | ///
51 | /// Typically a [Text] widget.
52 | final Widget title;
53 |
54 | /// Called when the tile expands or collapses.
55 | ///
56 | /// When the tile starts expanding, this function is called with the value
57 | /// true. When the tile starts collapsing, this function is called with
58 | /// the value false.
59 | final ValueChanged onExpansionChanged;
60 |
61 | /// The widgets that are displayed when the tile expands.
62 | ///
63 | /// Typically [ListTile] widgets.
64 | final List children;
65 |
66 | /// The color to display behind the sublist when expanded.
67 | final Color backgroundColor;
68 |
69 | /// A widget to display instead of a rotating arrow icon.
70 | final Widget trailing;
71 |
72 | /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
73 | final bool initiallyExpanded;
74 |
75 | _CustomExpansionTileState _state;
76 |
77 | void toggle(bool bExpand) => _state.toggle(bExpand);
78 |
79 | @override
80 | _CustomExpansionTileState createState() {
81 | _state = new _CustomExpansionTileState();
82 | return _state;
83 | }
84 | }
85 |
86 | class _CustomExpansionTileState extends State
87 | with SingleTickerProviderStateMixin {
88 | AnimationController _controller;
89 | CurvedAnimation _easeOutAnimation;
90 | CurvedAnimation _easeInAnimation;
91 | ColorTween _borderColor;
92 | ColorTween _headerColor;
93 | ColorTween _iconColor;
94 | ColorTween _backgroundColor;
95 | Animation _iconTurns;
96 |
97 | bool _isExpanded = false;
98 |
99 | @override
100 | void initState() {
101 | super.initState();
102 | _controller = new AnimationController(duration: _kExpand, vsync: this);
103 | _easeOutAnimation =
104 | new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
105 | _easeInAnimation =
106 | new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
107 | _borderColor = new ColorTween();
108 | _headerColor = new ColorTween();
109 | _iconColor = new ColorTween();
110 | _iconTurns =
111 | new Tween(begin: 0.0, end: 0.5).animate(_easeInAnimation);
112 | _backgroundColor = new ColorTween();
113 |
114 | _isExpanded = widget
115 | .initiallyExpanded; //PageStorage.of(context)?.readState(context) ??
116 | if (_isExpanded) _controller.value = 1.0;
117 | }
118 |
119 | @override
120 | void dispose() {
121 | _controller.dispose();
122 | super.dispose();
123 | }
124 |
125 | void toggle(bool bExpand) {
126 | if (mounted)
127 | setState(() {
128 | _isExpanded = bExpand;
129 | if (_isExpanded)
130 | _controller.forward();
131 | else
132 | _controller.reverse().then((Null value) {
133 | setState(() {
134 | // Rebuild without widget.children.
135 | });
136 | });
137 | PageStorage.of(context)?.writeState(context, _isExpanded);
138 | });
139 | if (widget.onExpansionChanged != null)
140 | widget.onExpansionChanged(_isExpanded);
141 | }
142 |
143 | void _handleTap() {
144 | setState(() {
145 | _isExpanded = !_isExpanded;
146 | if (_isExpanded)
147 | _controller.forward();
148 | else
149 | _controller.reverse().then((Null value) {
150 | setState(() {
151 | // Rebuild without widget.children.
152 | });
153 | });
154 | PageStorage.of(context)?.writeState(context, _isExpanded);
155 | });
156 | if (widget.onExpansionChanged != null)
157 | widget.onExpansionChanged(_isExpanded);
158 | }
159 |
160 | Widget _buildChildren(BuildContext context, Widget child) {
161 | final Color borderSideColor =
162 | _borderColor.evaluate(_easeOutAnimation) ?? Colors.transparent;
163 | final Color titleColor = _headerColor.evaluate(_easeInAnimation);
164 |
165 | return new Container(
166 | // decoration: new BoxDecoration(
167 | // color: _backgroundColor.evaluate(_easeOutAnimation) ??
168 | // Colors.transparent,
169 | // border: new Border(
170 | // top: new BorderSide(color: borderSideColor),
171 | // bottom: new BorderSide(color: borderSideColor),
172 | // )),
173 | child: new Column(
174 | mainAxisSize: MainAxisSize.min,
175 | children: [
176 | IconTheme.merge(
177 | data:
178 | new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)),
179 | child: new CustomListTile(
180 | onTap: _handleTap,
181 | leading: widget.leading ??
182 | new RotationTransition(
183 | turns: _iconTurns,
184 | child: const Icon(Icons.expand_more),
185 | ),
186 | title: new DefaultTextStyle(
187 | style: Theme
188 | .of(context)
189 | .textTheme
190 | .subhead
191 | .copyWith(color: titleColor),
192 | child: widget.title,
193 | ),
194 | trailing: widget.trailing),
195 | ),
196 | new ClipRect(
197 | child: new Align(
198 | heightFactor: _easeInAnimation.value,
199 | child: child,
200 | ),
201 | ),
202 | ],
203 | ),
204 | );
205 | }
206 |
207 | @override
208 | Widget build(BuildContext context) {
209 | final ThemeData theme = Theme.of(context);
210 | _borderColor.end = theme.dividerColor;
211 | _headerColor
212 | ..begin = theme.textTheme.subhead.color
213 | ..end = theme.accentColor;
214 | _iconColor
215 | ..begin = theme.unselectedWidgetColor
216 | ..end = theme.accentColor;
217 | _backgroundColor.end = widget.backgroundColor;
218 |
219 | final bool closed = !_isExpanded && _controller.isDismissed;
220 | return new AnimatedBuilder(
221 | animation: _controller.view,
222 | builder: _buildChildren,
223 | child: closed ? null : new Column(children: widget.children),
224 | );
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/lib/src/CustomListTile.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/foundation.dart';
6 | import 'package:flutter/widgets.dart';
7 |
8 | import 'package:flutter/material.dart';
9 | //import 'constants.dart';
10 | //import 'debug.dart';
11 | //import 'ink_well.dart';
12 | //import 'theme.dart';
13 |
14 | /// Defines the title font used for [CustomListTile] descendants of a [CustomListTileTheme].
15 | ///
16 | /// List tiles that appear in a [Drawer] use the theme's [TextTheme.body2]
17 | /// text style, which is a little smaller than the theme's [TextTheme.subhead]
18 | /// text style, which is used by default.
19 | enum CustomListTileStyle {
20 | /// Use a title font that's appropriate for a [CustomListTile] in a list.
21 | list,
22 |
23 | /// Use a title font that's appropriate for a [CustomListTile] that appears in a [Drawer].
24 | drawer,
25 | }
26 |
27 | /// An inherited widget that defines color and style parameters for [CustomListTile]s
28 | /// in this widget's subtree.
29 | ///
30 | /// Values specified here are used for [CustomListTile] properties that are not given
31 | /// an explicit non-null value.
32 | ///
33 | /// The [Drawer] widget specifies a tile theme for its children which sets
34 | /// [style] to [CustomListTileStyle.drawer].
35 | class CustomListTileTheme extends InheritedWidget {
36 | /// Creates a list tile theme that controls the color and style parameters for
37 | /// [CustomListTile]s.
38 | const CustomListTileTheme({
39 | Key key,
40 | this.dense: false,
41 | this.style: CustomListTileStyle.list,
42 | this.selectedColor,
43 | this.iconColor,
44 | this.textColor,
45 | Widget child,
46 | })
47 | : super(key: key, child: child);
48 |
49 | /// Creates a list tile theme that controls the color and style parameters for
50 | /// [CustomListTile]s, and merges in the current list tile theme, if any.
51 | ///
52 | /// The [child] argument must not be null.
53 | static Widget merge({
54 | Key key,
55 | bool dense,
56 | CustomListTileStyle style,
57 | Color selectedColor,
58 | Color iconColor,
59 | Color textColor,
60 | @required Widget child,
61 | }) {
62 | assert(child != null);
63 | return new Builder(
64 | builder: (BuildContext context) {
65 | final CustomListTileTheme parent = CustomListTileTheme.of(context);
66 | return new CustomListTileTheme(
67 | key: key,
68 | dense: dense ?? parent.dense,
69 | style: style ?? parent.style,
70 | selectedColor: selectedColor ?? parent.selectedColor,
71 | iconColor: iconColor ?? parent.iconColor,
72 | textColor: textColor ?? parent.textColor,
73 | child: child,
74 | );
75 | },
76 | );
77 | }
78 |
79 | /// If true then [CustomListTile]s will have the vertically dense layout.
80 | final bool dense;
81 |
82 | /// If specified, [style] defines the font used for [CustomListTile] titles.
83 | final CustomListTileStyle style;
84 |
85 | /// If specified, the color used for icons and text when a [CustomListTile] is selected.
86 | final Color selectedColor;
87 |
88 | /// If specified, the icon color used for enabled [CustomListTile]s that are not selected.
89 | final Color iconColor;
90 |
91 | /// If specified, the text color used for enabled [CustomListTile]s that are not selected.
92 | final Color textColor;
93 |
94 | /// The closest instance of this class that encloses the given context.
95 | ///
96 | /// Typical usage is as follows:
97 | ///
98 | /// ```dart
99 | /// CustomListTileTheme theme = CustomListTileTheme.of(context);
100 | /// ```
101 | static CustomListTileTheme of(BuildContext context) {
102 | final CustomListTileTheme result =
103 | context.inheritFromWidgetOfExactType(CustomListTileTheme);
104 | return result ?? const CustomListTileTheme();
105 | }
106 |
107 | @override
108 | bool updateShouldNotify(CustomListTileTheme oldTheme) {
109 | return dense != oldTheme.dense ||
110 | style != oldTheme.style ||
111 | selectedColor != oldTheme.selectedColor ||
112 | iconColor != oldTheme.iconColor ||
113 | textColor != oldTheme.textColor;
114 | }
115 | }
116 |
117 | /// Where to place the control in widgets that use [CustomListTile] to position a
118 | /// control next to a label.
119 | ///
120 | /// See also:
121 | ///
122 | /// * [CheckboxCustomListTile], which combines a [CustomListTile] with a [Checkbox].
123 | /// * [RadioCustomListTile], which combines a [CustomListTile] with a [Radio] button.
124 | enum CustomListTileControlAffinity {
125 | /// Position the control on the leading edge, and the secondary widget, if
126 | /// any, on the trailing edge.
127 | leading,
128 |
129 | /// Position the control on the trailing edge, and the secondary widget, if
130 | /// any, on the leading edge.
131 | trailing,
132 |
133 | /// Position the control relative to the text in the fashion that is typical
134 | /// for the current platform, and place the secondary widget on the opposite
135 | /// side.
136 | platform,
137 | }
138 |
139 | /// A single fixed-height row that typically contains some text as well as
140 | /// a leading or trailing icon.
141 | ///
142 | /// A list tile contains one to three lines of text optionally flanked by icons or
143 | /// other widgets, such as check boxes. The icons (or other widgets) for the
144 | /// tile are defined with the [leading] and [trailing] parameters. The first
145 | /// line of text is not optional and is specified with [title]. The value of
146 | /// [subtitle], which _is_ optional, will occupy the space allocated for an
147 | /// additional line of text, or two lines if [isThreeLine] is true. If [dense]
148 | /// is true then the overall height of this tile and the size of the
149 | /// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced.
150 | ///
151 | /// List tiles are always a fixed height (which height depends on how
152 | /// [isThreeLine], [dense], and [subtitle] are configured); they do not grow in
153 | /// height based on their contents. If you are looking for a widget that allows
154 | /// for arbitrary layout in a row, consider [Row].
155 | ///
156 | /// List tiles are typically used in [ListView]s, or arranged in [Column]s in
157 | /// [Drawer]s and [Card]s.
158 | ///
159 | /// Requires one of its ancestors to be a [Material] widget.
160 | ///
161 | /// ## Sample code
162 | ///
163 | /// Here is a simple tile with an icon and some text.
164 | ///
165 | /// ```dart
166 | /// new CustomListTile(
167 | /// leading: const Icon(Icons.event_seat),
168 | /// title: const Text('The seat for the narrator'),
169 | /// )
170 | /// ```
171 | ///
172 | /// Tiles can be much more elaborate. Here is a tile which can be tapped, but
173 | /// which is disabled when the `_act` variable is not 2. When the tile is
174 | /// tapped, the whole row has an ink splash effect (see [InkWell]).
175 | ///
176 | /// ```dart
177 | /// int _act = 1;
178 | /// // ...
179 | /// new CustomListTile(
180 | /// leading: const Icon(Icons.flight_land),
181 | /// title: const Text('Trix\'s airplane'),
182 | /// subtitle: _act != 2 ? const Text('The airplane is only in Act II.') : null,
183 | /// enabled: _act == 2,
184 | /// onTap: () { /* react to the tile being tapped */ }
185 | /// )
186 | /// ```
187 | ///
188 | /// See also:
189 | ///
190 | /// * [CustomListTileTheme], which defines visual properties for [CustomListTile]s.
191 | /// * [ListView], which can display an arbitrary number of [CustomListTile]s
192 | /// in a scrolling list.
193 | /// * [CircleAvatar], which shows an icon representing a person and is often
194 | /// used as the [leading] element of a CustomListTile.
195 | /// * [Card], which can be used with [Column] to show a few [CustomListTile]s.
196 | /// * [Divider], which can be used to separate [CustomListTile]s.
197 | /// * [CustomListTile.divideTiles], a utility for inserting [Divider]s in between [CustomListTile]s.
198 | /// * [CheckboxCustomListTile], [RadioCustomListTile], and [SwitchCustomListTile], widgets
199 | /// that combine [CustomListTile] with other controls.
200 | /// *
201 | class CustomListTile extends StatelessWidget {
202 | /// Creates a list tile.
203 | ///
204 | /// If [isThreeLine] is true, then [subtitle] must not be null.
205 | ///
206 | /// Requires one of its ancestors to be a [Material] widget.
207 | const CustomListTile({
208 | Key key,
209 | this.leading,
210 | this.title,
211 | this.subtitle,
212 | this.trailing,
213 | this.isThreeLine: false,
214 | this.dense,
215 | this.enabled: true,
216 | this.onTap,
217 | this.onLongPress,
218 | this.selected: false,
219 | })
220 | : assert(isThreeLine != null),
221 | assert(enabled != null),
222 | assert(selected != null),
223 | assert(!isThreeLine || subtitle != null),
224 | super(key: key);
225 |
226 | /// A widget to display before the title.
227 | ///
228 | /// Typically an [Icon] or a [CircleAvatar] widget.
229 | final Widget leading;
230 |
231 | /// The primary content of the list tile.
232 | ///
233 | /// Typically a [Text] widget.
234 | final Widget title;
235 |
236 | /// Additional content displayed below the title.
237 | ///
238 | /// Typically a [Text] widget.
239 | final Widget subtitle;
240 |
241 | /// A widget to display after the title.
242 | ///
243 | /// Typically an [Icon] widget.
244 | final Widget trailing;
245 |
246 | /// Whether this list tile is intended to display three lines of text.
247 | ///
248 | /// If false, the list tile is treated as having one line if the subtitle is
249 | /// null and treated as having two lines if the subtitle is non-null.
250 | final bool isThreeLine;
251 |
252 | /// Whether this list tile is part of a vertically dense list.
253 | ///
254 | /// If this property is null then its value is based on [CustomListTileTheme.dense].
255 | final bool dense;
256 |
257 | /// Whether this list tile is interactive.
258 | ///
259 | /// If false, this list tile is styled with the disabled color from the
260 | /// current [Theme] and the [onTap] and [onLongPress] callbacks are
261 | /// inoperative.
262 | final bool enabled;
263 |
264 | /// Called when the user taps this list tile.
265 | ///
266 | /// Inoperative if [enabled] is false.
267 | final GestureTapCallback onTap;
268 |
269 | /// Called when the user long-presses on this list tile.
270 | ///
271 | /// Inoperative if [enabled] is false.
272 | final GestureLongPressCallback onLongPress;
273 |
274 | /// If this tile is also [enabled] then icons and text are rendered with the same color.
275 | ///
276 | /// By default the selected color is the theme's primary color. The selected color
277 | /// can be overridden with a [CustomListTileTheme].
278 | final bool selected;
279 |
280 | /// Add a one pixel border in between each tile. If color isn't specified the
281 | /// [ThemeData.dividerColor] of the context's [Theme] is used.
282 | ///
283 | /// See also:
284 | ///
285 | /// * [Divider], which you can use to obtain this effect manually.
286 | static Iterable divideTiles(
287 | {BuildContext context,
288 | @required Iterable tiles,
289 | Color color}) sync* {
290 | assert(tiles != null);
291 | assert(color != null || context != null);
292 |
293 | final Color dividerColor = color ?? Theme.of(context).dividerColor;
294 | final Iterator iterator = tiles.iterator;
295 | final bool isNotEmpty = iterator.moveNext();
296 |
297 | Widget tile = iterator.current;
298 | while (iterator.moveNext()) {
299 | yield new DecoratedBox(
300 | position: DecorationPosition.foreground,
301 | decoration: new BoxDecoration(
302 | border: new Border(
303 | bottom: new BorderSide(color: dividerColor, width: 0.0),
304 | ),
305 | ),
306 | child: tile,
307 | );
308 | tile = iterator.current;
309 | }
310 | if (isNotEmpty) yield tile;
311 | }
312 |
313 | Color _iconColor(ThemeData theme, CustomListTileTheme tileTheme) {
314 | if (!enabled) return theme.disabledColor;
315 |
316 | if (selected && tileTheme?.selectedColor != null)
317 | return tileTheme.selectedColor;
318 |
319 | if (!selected && tileTheme?.iconColor != null) return tileTheme.iconColor;
320 |
321 | switch (theme.brightness) {
322 | case Brightness.light:
323 | return selected ? theme.primaryColor : Colors.black45;
324 | case Brightness.dark:
325 | return selected
326 | ? theme.accentColor
327 | : null; // null - use current icon theme color
328 | }
329 | assert(theme.brightness != null);
330 | return null;
331 | }
332 |
333 | Color _textColor(
334 | ThemeData theme, CustomListTileTheme tileTheme, Color defaultColor) {
335 | if (!enabled) return theme.disabledColor;
336 |
337 | if (selected && tileTheme?.selectedColor != null)
338 | return tileTheme.selectedColor;
339 |
340 | if (!selected && tileTheme?.textColor != null) return tileTheme.textColor;
341 |
342 | if (selected) {
343 | switch (theme.brightness) {
344 | case Brightness.light:
345 | return theme.primaryColor;
346 | case Brightness.dark:
347 | return theme.accentColor;
348 | }
349 | }
350 | return defaultColor;
351 | }
352 |
353 | bool _denseLayout(CustomListTileTheme tileTheme) {
354 | return dense != null ? dense : (tileTheme?.dense ?? false);
355 | }
356 |
357 | TextStyle _titleTextStyle(ThemeData theme, CustomListTileTheme tileTheme) {
358 | TextStyle style;
359 | if (tileTheme != null) {
360 | switch (tileTheme.style) {
361 | case CustomListTileStyle.drawer:
362 | style = theme.textTheme.body2;
363 | break;
364 | case CustomListTileStyle.list:
365 | style = theme.textTheme.subhead;
366 | break;
367 | }
368 | } else {
369 | style = theme.textTheme.subhead;
370 | }
371 | final Color color = _textColor(theme, tileTheme, style.color);
372 | return _denseLayout(tileTheme)
373 | ? style.copyWith(fontSize: 13.0, color: color)
374 | : style.copyWith(color: color);
375 | }
376 |
377 | TextStyle _subtitleTextStyle(ThemeData theme, CustomListTileTheme tileTheme) {
378 | final TextStyle style = theme.textTheme.body1;
379 | final Color color =
380 | _textColor(theme, tileTheme, theme.textTheme.caption.color);
381 | return _denseLayout(tileTheme)
382 | ? style.copyWith(color: color, fontSize: 12.0)
383 | : style.copyWith(color: color);
384 | }
385 |
386 | @override
387 | Widget build(BuildContext context) {
388 | assert(debugCheckHasMaterial(context));
389 | final ThemeData theme = Theme.of(context);
390 | final CustomListTileTheme tileTheme = CustomListTileTheme.of(context);
391 |
392 | final bool isTwoLine = !isThreeLine && subtitle != null;
393 | final bool isOneLine = !isThreeLine && !isTwoLine;
394 | double tileHeight;
395 | if (isOneLine)
396 | tileHeight = _denseLayout(tileTheme) ? 48.0 : 56.0;
397 | else if (isTwoLine)
398 | tileHeight = _denseLayout(tileTheme) ? 60.0 : 72.0;
399 | else
400 | tileHeight = _denseLayout(tileTheme) ? 76.0 : 88.0;
401 |
402 | // Overall, the list tile is a Row() with these children.
403 | final List children = [];
404 |
405 | IconThemeData iconThemeData;
406 | if (leading != null || trailing != null)
407 | iconThemeData = new IconThemeData(color: _iconColor(theme, tileTheme));
408 |
409 | if (leading != null) {
410 | children.add(IconTheme.merge(
411 | data: iconThemeData,
412 | child: new Container(
413 | // margin: const EdgeInsetsDirectional.only(end: 16.0),
414 | width: 30.0,
415 | alignment: AlignmentDirectional.centerStart,
416 | child: leading,
417 | ),
418 | ));
419 | }
420 |
421 | final Widget primaryLine = new AnimatedDefaultTextStyle(
422 | style: _titleTextStyle(theme, tileTheme),
423 | duration: kThemeChangeDuration,
424 | child: title ?? new Container());
425 | Widget center = primaryLine;
426 | if (subtitle != null && (isTwoLine || isThreeLine)) {
427 | center = new Column(
428 | mainAxisSize: MainAxisSize.min,
429 | crossAxisAlignment: CrossAxisAlignment.start,
430 | children: [
431 | primaryLine,
432 | new AnimatedDefaultTextStyle(
433 | style: _subtitleTextStyle(theme, tileTheme),
434 | duration: kThemeChangeDuration,
435 | child: subtitle,
436 | ),
437 | ],
438 | );
439 | }
440 | children.add(new Expanded(
441 | child: center,
442 | ));
443 |
444 | if (trailing != null) {
445 | children.add(IconTheme.merge(
446 | data: iconThemeData,
447 | child: new Container(
448 | margin: const EdgeInsetsDirectional.only(start: 16.0),
449 | alignment: AlignmentDirectional.centerEnd,
450 | child: trailing,
451 | ),
452 | ));
453 | }
454 |
455 | return new InkWell(
456 | onTap: enabled ? onTap : null,
457 | onLongPress: enabled ? onLongPress : null,
458 | child: new ConstrainedBox(
459 | constraints: new BoxConstraints(minHeight: tileHeight),
460 | child: new Padding(
461 | padding: const EdgeInsets.symmetric(horizontal: 0.0),
462 | child: new UnconstrainedBox(
463 | constrainedAxis: Axis.horizontal,
464 | child: new Row(children: children),
465 | ),
466 | )),
467 | );
468 | }
469 | }
470 |
--------------------------------------------------------------------------------
/lib/src/Global.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter/material.dart';
3 |
4 |
5 | class Constants {
6 |
7 | static const VIEW_PADDING = 15.0;
8 | static const LIST_ITEM_INDENT = 16.0;
9 | static const VERTICAL_PADDING = 15.0;
10 | static const HORIZONTAL_PADDING = 48.0;
11 | static const VERTICAL_PADDING_FORM = 10.0;
12 | static const HORIZONTAL_PADDING_FORM = 8.0;
13 | }
14 | class Util {
15 | static String getKey() {
16 | List strings = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
17 | strings.shuffle();
18 | String randId = strings.join("");
19 | return randId;
20 | }
21 |
22 | static Future alert(BuildContext context,
23 | {String title = "Notification",
24 | String content = "Detail",
25 | bool allowCancel = false}) async {
26 | var alert = new AlertDialog(
27 | title: new Text(title),
28 | content: new Text(content),
29 | actions: [
30 | new FlatButton(
31 | child: const Text('OK'),
32 | onPressed: () {
33 | Navigator.of(context).pop(true);
34 | },
35 | ),
36 | allowCancel
37 | ? new FlatButton(
38 | child: const Text('Cancel'),
39 | onPressed: () {
40 | Navigator.of(context).pop(false);
41 | },
42 | )
43 | : new Container()
44 | ],
45 | );
46 |
47 | return await showDialog(
48 | context: context, child: alert, barrierDismissible: false);
49 | }
50 | }
51 | // Stack emulator
52 | class StackEmul {
53 | List _internalList;
54 |
55 | StackEmul({List list}) {
56 | if (list != null)
57 | _internalList = list;
58 | else
59 | _internalList = [];
60 | }
61 |
62 | void push(T object) {
63 | assert(_internalList != null);
64 | _internalList.add(object);
65 | }
66 |
67 | T pop() {
68 | assert(_internalList != null);
69 | if(_internalList.length>0)
70 | return _internalList.removeLast();
71 | else
72 | return null;
73 | }
74 |
75 | int get count=> _internalList?.length;
76 | }
77 |
78 |
79 |
--------------------------------------------------------------------------------
/lib/src/SearchBar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:validator/validator.dart';
3 |
4 | typedef void SearchCallback(String stringToSearch);
5 |
6 | class SearchBar extends StatefulWidget {
7 | SearchCallback _onSearch;
8 |
9 | SearchBar({SearchCallback onSearch}) {
10 | _onSearch = onSearch;
11 | }
12 |
13 | @override
14 | SearchBarState createState() => new SearchBarState();
15 | }
16 |
17 | class SearchBarState extends State {
18 | bool _isSearching = false;
19 | final GlobalKey> _textsearchFieldKey =
20 | new GlobalKey>();
21 | final GlobalKey _formKey = new GlobalKey();
22 | String _searchValue = "";
23 | FocusNode _inputFocus = new FocusNode();
24 |
25 | // Event handlers
26 | _onSearch() {
27 | FocusScope.of(context).requestFocus(new FocusNode());
28 | final FormState form = _formKey.currentState;
29 | form.save();
30 | // print("Search: '$_searchValue'");
31 |
32 | if (!isNull(_searchValue))
33 | setState(() {
34 | _isSearching = true;
35 | if (widget._onSearch != null) widget._onSearch(_searchValue);
36 | });
37 | }
38 |
39 | _onDelete() {
40 | final FormFieldState _textsearchField =
41 | _textsearchFieldKey.currentState;
42 | _textsearchField.reset();
43 |
44 | FocusScope.of(context).requestFocus(_inputFocus);
45 | setState(() {
46 | _isSearching = false;
47 | _searchValue = "";
48 | });
49 | }
50 |
51 | @override
52 | Widget build(BuildContext context) {
53 | return new SizedBox(
54 | height: 40.0,
55 | child: new Form(
56 | key: _formKey,
57 | child: new Row(
58 | mainAxisSize: MainAxisSize.min,
59 | children: [
60 | new SizedBox(
61 | width: 180.0,
62 | child: new TextFormField(
63 | focusNode: _inputFocus,
64 | key: _textsearchFieldKey,
65 | style: new TextStyle(color: Colors.black),
66 | decoration: new InputDecoration(
67 | hintText: "Enter...",
68 | hintStyle: new TextStyle(color: Colors.black),
69 | ),
70 | onSaved: (String value) {
71 | _searchValue = value;
72 | },
73 | ),
74 | ),
75 | new Expanded(
76 | child: _isSearching
77 | ? new IconButton(
78 | icon: new Icon(Icons.clear),
79 | onPressed: _onDelete,
80 | )
81 | : new IconButton(
82 | icon: new Icon(Icons.search),
83 | onPressed: _onSearch,
84 | ))
85 | ],
86 | ),
87 | ));
88 | }
89 | }
90 |
91 | class _HeadingLayout extends MultiChildLayoutDelegate {
92 | _HeadingLayout();
93 |
94 | static final String searchBar = 'searchBar';
95 |
96 | @override
97 | void performLayout(Size size) {
98 | const double marginX = 16.0;
99 | const double marginY = 5.0;
100 |
101 | final double maxHeaderWidth = 200.0;
102 | final BoxConstraints headerBoxConstraints =
103 | new BoxConstraints(maxWidth: maxHeaderWidth);
104 | final Size headerSize = layoutChild(searchBar, headerBoxConstraints);
105 |
106 | final double searchbarX = (size.width - headerSize.width) / 2.0;
107 | positionChild(searchBar, new Offset(searchbarX, marginY));
108 | }
109 |
110 | @override
111 | bool shouldRelayout(_HeadingLayout oldDelegate) => false;
112 | }
113 |
114 | class Heading extends StatelessWidget {
115 | SearchCallback _searchCallback;
116 |
117 | Heading({Key key, SearchCallback searchCallback}) : super(key: key) {
118 | _searchCallback = searchCallback;
119 | }
120 |
121 | @override
122 | Widget build(BuildContext context) {
123 | final Size screenSize = MediaQuery.of(context).size;
124 | final ThemeData theme = Theme.of(context);
125 | return new MergeSemantics(
126 | child: new SizedBox(
127 | height: screenSize.width > screenSize.height
128 | ? (screenSize.height - kToolbarHeight) * 0.10
129 | : (screenSize.height - kToolbarHeight) * 0.08,
130 | child: new Container(
131 | decoration: new BoxDecoration(
132 | color: Theme.of(context).accentColor.withOpacity(0.015),
133 | border:
134 | new Border(bottom: new BorderSide(color: theme.dividerColor)),
135 | ),
136 | child: new CustomMultiChildLayout(
137 | delegate: new _HeadingLayout(),
138 | children: [
139 | new LayoutId(
140 | id: _HeadingLayout.searchBar,
141 | child: new SearchBar(
142 | onSearch: _searchCallback,
143 | ),
144 | ),
145 | ],
146 | ),
147 | ),
148 | ),
149 | );
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/lib/src/TreeView.dart:
--------------------------------------------------------------------------------
1 | library piggy_treeview;
2 |
3 | import 'package:flutter/material.dart';
4 | import './CustomExpansionTile.dart';
5 | import './CustomListTile.dart';
6 | import './Global.dart';
7 |
8 | typedef void NodeCallback(T node);
9 | typedef void EditNodeCallback(T node);
10 | typedef void ManageCallback(T node);
11 |
12 | ///
13 | /// T: any data passed to the node.
14 | ///
15 |
16 | class TreeNodeData {
17 | T _data;
18 | Widget _bindedWidget;
19 |
20 | String _id;
21 | String _title;
22 | String _subtitle;
23 |
24 | TreeNodeData _parent;
25 | List> _children = [];
26 |
27 | bool _hidden;
28 | bool _expanded;
29 | bool _checked;
30 | bool _hilited;
31 |
32 | // Constructors
33 | TreeNodeData();
34 |
35 | TreeNodeData.root(this._title, this._subtitle, this._id,
36 | [this._data,
37 | this._checked = false,
38 | this._expanded = true,
39 | this._hidden = false,
40 | this._hilited = false]) {
41 | _checkInit();
42 | }
43 |
44 | TreeNodeData.node(this._parent, this._title, this._subtitle, this._id,
45 | [this._data,
46 | this._checked = false,
47 | this._expanded = false,
48 | this._hidden = false,
49 | this._hilited = false]) {
50 | _checkInit();
51 | _parent._children.add(this);
52 | }
53 |
54 | void _checkInit() {
55 | if (this._checked == null) this._checked = false;
56 | if (this._expanded == null) this._expanded = false;
57 | if (this._hidden == null) this._hidden = false;
58 | if (this._hilited == null) this._hilited = false;
59 | }
60 |
61 | // Properties
62 | T get data => _data;
63 |
64 | set data(T nodeData) => _data = nodeData;
65 |
66 | String get id => _id;
67 |
68 | String get title => _title;
69 |
70 | String get subTitle => _subtitle;
71 |
72 | bool get isRoot => _parent == null;
73 |
74 | bool get isLeaf => !hasChildren;
75 |
76 | bool get hasChildren => _children.isNotEmpty;
77 |
78 | bool get isExpanded => _expanded;
79 |
80 | bool get isChecked => _checked;
81 |
82 | bool get isHidden => _hidden;
83 |
84 | bool get isHilited => _hilited;
85 |
86 | set checked(bool checked) => _checked = checked;
87 |
88 | set expanded(bool expanded) => _expanded = expanded;
89 |
90 | set hidden(bool hidden) => _hidden = hidden;
91 |
92 | set hilited(bool hilite) => _hilited = hilite;
93 |
94 | set bindedWidget(Widget widget) => _bindedWidget = widget;
95 |
96 | List> get children => _children;
97 |
98 | TreeNodeData get parent => _parent;
99 |
100 | Widget get bindedWidget => _bindedWidget;
101 |
102 | // Methods
103 | TreeNodeData getNodeByID(String id) {
104 | if (this.id == id) return this;
105 |
106 | for (var node in _children) {
107 | var found = node.getNodeByID(id);
108 | if (found != null) return found;
109 | }
110 |
111 | return null;
112 | }
113 |
114 | List> getAllSubNodes([bool addThisNode = true]) {
115 | List> all = [];
116 |
117 | if (addThisNode) all.add(this);
118 |
119 | _addAllSubNodes(all);
120 |
121 | return all;
122 | }
123 |
124 | void _addAllSubNodes(List> all) {
125 | for (var node in _children) {
126 | all.add(node);
127 | node._addAllSubNodes(all);
128 | }
129 | }
130 |
131 | TreeNodeData getRoot() {
132 | TreeNodeData cursor = this;
133 | do {
134 | if (cursor.isRoot) return cursor;
135 | cursor = cursor.parent;
136 | } while (cursor != null);
137 |
138 | throw new StateError("Cấu trúc cây không hợp lệ");
139 | }
140 |
141 | TreeNodeData createChild(String title, String subTitle, String id, T data,
142 | [bool expanded = false]) {
143 | var child =
144 | new TreeNodeData.node(this, title, subTitle, id, data, expanded);
145 | return child;
146 | }
147 |
148 | String toString() =>
149 | "title: id=$title id:$id subTittle:$subTitle isExpanded:$isExpanded checked: $isChecked";
150 | }
151 |
152 | class TreeNode extends StatefulWidget {
153 | TreeView _treeComponent;
154 | TreeNodeData _nodeData;
155 | NodeCallback onSelectNode;
156 | NodeCallback onHiliteNode;
157 | EditNodeCallback onEditNode;
158 | ManageCallback onManage;
159 | // Constructor
160 | TreeNode(this._treeComponent, this._nodeData, {Key key}) : super(key: key);
161 | TreeNodeState _state;
162 |
163 | void toggle(bool bExpand) => _state?.toggle(bExpand);
164 |
165 | void broadcast(Map map) => _state?.broadcast(map);
166 |
167 | void openIt() => _state?.openIt();
168 |
169 | @override
170 | TreeNodeState createState() {
171 | _state = new TreeNodeState();
172 | return _state;
173 | }
174 | }
175 |
176 | class TreeNodeState extends State {
177 | CustomExpansionTile _wExpand;
178 | List _lstNodes;
179 |
180 | void openIt() {
181 | _wExpand?.toggle(true);
182 | }
183 |
184 | void toggle(bool bExpand) {
185 | _wExpand?.toggle(bExpand);
186 | if (_lstNodes != null)
187 | for (TreeNode node in _lstNodes) {
188 | node?.toggle(bExpand);
189 | }
190 | }
191 |
192 | void broadcast(Map map) {
193 | TreeNodeData tempNode = map[widget._nodeData.id];
194 | if (tempNode != null && mounted)
195 | setState(() => widget._nodeData.hilited = tempNode.isHilited);
196 | _lstNodes?.forEach((node) => node.broadcast(map));
197 | }
198 |
199 | @override
200 | void initState() {
201 | super.initState();
202 | _lstNodes = widget._nodeData.hasChildren
203 | ? widget._nodeData.children.map((TreeNodeData subNode) {
204 | TreeNode treeNode = new TreeNode(widget._treeComponent, subNode,
205 | key: new Key(Util.getKey()));
206 | treeNode.onSelectNode = widget.onSelectNode;
207 | treeNode.onEditNode = widget.onEditNode;
208 | treeNode.onManage = widget.onManage;
209 | treeNode.onHiliteNode = widget.onHiliteNode;
210 | // Make sure to store a reference to the associated widget
211 | subNode.bindedWidget = treeNode;
212 | return treeNode;
213 | }).toList()
214 | : null;
215 | }
216 |
217 | @override
218 | void dispose() {
219 | super.dispose();
220 | _wExpand = null;
221 | _lstNodes?.clear();
222 | _lstNodes = null;
223 | }
224 |
225 | @override
226 | Widget build(BuildContext context) {
227 | return _buildNode();
228 | }
229 |
230 | Widget _buildNode() {
231 | var currentNode = widget._nodeData;
232 | if (currentNode.isLeaf) {
233 | return buildNormalNode(currentNode);
234 | } else {
235 | return buildExpandedNode(currentNode);
236 | }
237 | }
238 |
239 | // Handlers
240 | void selectNodes(TreeNodeData currentNode, bool value) {
241 | currentNode.checked = value;
242 |
243 | if (widget.onSelectNode != null) widget.onSelectNode(currentNode);
244 |
245 | if (currentNode.hasChildren) {
246 | for (var node in currentNode.children) {
247 | selectNodes(node, value);
248 | }
249 | }
250 | }
251 |
252 | /// The subclass has to override the two methods below to display a node
253 | Widget buildNormalNode(TreeNodeData currentNode) {
254 | return new Container(
255 | padding: new EdgeInsets.only(left: Constants.LIST_ITEM_INDENT),
256 | child: new CustomListTile(
257 | key: new Key(Util.getKey()),
258 | leading: new Checkbox(
259 | value: currentNode.isChecked,
260 | onChanged: (bool value) {
261 | setState(() => currentNode.checked = value);
262 | if (widget.onSelectNode != null)
263 | widget.onSelectNode(currentNode);
264 | }),
265 | title: new Text(
266 | currentNode.title,
267 | ),
268 | subtitle: new Text(currentNode.subTitle),
269 | selected: currentNode.isHilited,
270 | onTap: () {
271 | setState(() => currentNode.hilited = !currentNode.isHilited);
272 | if (widget.onHiliteNode != null) widget.onHiliteNode(currentNode);
273 | },
274 | onLongPress: () {
275 | setState(() {
276 | if (widget.onEditNode != null) widget.onEditNode(currentNode);
277 | });
278 | },
279 | ));
280 | }
281 |
282 | Widget buildExpandedNode(TreeNodeData currentNode) {
283 | _wExpand = new CustomExpansionTile(
284 | initiallyExpanded: currentNode.parent == null
285 | ? currentNode.isExpanded
286 | : widget._treeComponent.expandIt,
287 | onExpansionChanged: (bool value) {
288 | currentNode.expanded = value;
289 | },
290 | key: new Key(Util.getKey()),
291 | title: new CustomListTile(
292 | key: new Key(currentNode.id),
293 | leading: new Checkbox(
294 | value: currentNode.isChecked,
295 | onChanged: (bool value) {
296 | setState(() {
297 | selectNodes(currentNode, value);
298 | });
299 | }),
300 | title: new Text(currentNode.title),
301 | subtitle: new Text(currentNode.subTitle),
302 | selected: currentNode.isHilited,
303 | onTap: () {
304 | if (mounted) {
305 | setState(() => currentNode.hilited = !currentNode.isHilited);
306 | if (widget.onHiliteNode != null) widget.onHiliteNode(currentNode);
307 | }
308 | },
309 | onLongPress: () {
310 | setState(() {
311 | if (widget.onEditNode != null) widget.onEditNode(currentNode);
312 | });
313 | },
314 | ),
315 | backgroundColor: Theme.of(context).accentColor.withOpacity(0.015),
316 | children: [
317 | new Container(
318 | padding:
319 | new EdgeInsets.only(left: Constants.LIST_ITEM_INDENT * 2),
320 | child: new Column(
321 | mainAxisSize: MainAxisSize.min, children: _lstNodes))
322 | ]);
323 | return _wExpand;
324 | }
325 | }
326 |
327 | class TreeView extends StatefulWidget {
328 | List _roots;
329 | Widget _header;
330 | bool expandIt;
331 |
332 | NodeCallback onSelectNode;
333 | NodeCallback onHiliteNode;
334 | EditNodeCallback onEditNode;
335 | ManageCallback onManage;
336 |
337 | // Constructors
338 | TreeView(TreeNodeData roots) {
339 | _roots = [roots];
340 | }
341 |
342 | TreeView.multipleRoots(this._roots, {Widget header}) {
343 | _header = header;
344 | }
345 |
346 | // Methods
347 | TreeNodeData getNodeByID(String id) {
348 | for (var root in _roots) {
349 | var found = root.getNodeByID(id);
350 | if (found != null) return found;
351 | }
352 | return null;
353 | }
354 |
355 | TreeViewState _state;
356 |
357 | void toggle(bool bExpand) => _state?.toggle(bExpand);
358 |
359 | void broadcast(Map map) => _state?.broadcast(map);
360 |
361 | void expandOnTo(Map map) => _state?.expandOnTo(map);
362 |
363 | // Overrides
364 | @override
365 | TreeViewState createState() {
366 | _state = new TreeViewState();
367 | return _state;
368 | }
369 | }
370 |
371 | class TreeViewState extends State {
372 | List _lstNodes;
373 |
374 | void toggle(bool bExpand) {
375 | for (TreeNode node in _lstNodes) {
376 | node?.toggle(bExpand);
377 | }
378 | }
379 |
380 | void broadcast(Map map) {
381 | _lstNodes?.forEach((node) {
382 | TreeNodeData tempNode = map[node._nodeData.id];
383 | if (tempNode != null)
384 | setState(() => node._nodeData.hilited = tempNode.isHilited);
385 | node.broadcast(map);
386 | });
387 | }
388 |
389 | void expandOnTo(Map map) {
390 | _openIt(TreeNodeData node) {
391 | // Try to expand the tree to this node
392 | TreeNodeData parent = node.parent;
393 | StackEmul stackWidget = new StackEmul();
394 | while (parent != null) {
395 | TreeNode parentWidget = parent.bindedWidget as TreeNode;
396 | assert(parentWidget != null);
397 | stackWidget.push(parentWidget);
398 | parent = parent.parent;
399 | }
400 | // Now expand the tree the top-down way
401 | TreeNode openWidget = stackWidget.pop();
402 | while (openWidget != null) {
403 | openWidget.openIt();
404 | openWidget = stackWidget.pop();
405 | }
406 | }
407 |
408 | map.forEach((key, node) {
409 | _openIt(node);
410 | node.hilited = true;
411 | });
412 | broadcast(map);
413 | }
414 |
415 | @override
416 | void initState() {
417 | super.initState();
418 | _lstNodes = widget._roots?.length > 0
419 | ? widget._roots.map((TreeNodeData root) {
420 | TreeNode treeNode =
421 | new TreeNode(widget, root, key: new Key(Util.getKey()));
422 | treeNode.onSelectNode = widget.onSelectNode;
423 | treeNode.onEditNode = widget.onEditNode;
424 | treeNode.onManage = widget.onManage;
425 | treeNode.onHiliteNode = widget.onHiliteNode;
426 | // Make sure to store a reference to the associated widget
427 | root.bindedWidget = treeNode;
428 | return treeNode;
429 | }).toList()
430 | : null;
431 | }
432 |
433 | @override
434 | void dispose() {
435 | super.dispose();
436 | _lstNodes?.clear();
437 | _lstNodes = null;
438 | }
439 |
440 | @override
441 | Widget build(BuildContext context) {
442 | List _lstWidget = [];
443 | if (widget._header != null) _lstWidget.add(widget._header);
444 |
445 | _lstWidget.addAll(_lstNodes);
446 |
447 | return new ListView(children: _lstWidget);
448 | }
449 | }
450 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: piggy
2 | description: A piggy client for turk server.
3 |
4 | dependencies:
5 | flutter:
6 | sdk: flutter
7 |
8 | # The following adds the Cupertino Icons font to your application.
9 | # Use with the CupertinoIcons class for iOS style icons.
10 | cupertino_icons: ^0.1.0
11 | shared_preferences: "^0.4.3"
12 | validators: ^1.0.0+1
13 | jaguar_serializer: "^2.2.6"
14 | intl: 0.15.7
15 |
16 | dev_dependencies:
17 | flutter_test:
18 | sdk: flutter
19 |
20 | environment:
21 | sdk: ">=2.0.0 <3.0.0"
22 |
23 |
24 | # For information on the generic Dart part of this file, see the
25 | # following page: https://www.dartlang.org/tools/pub/pubspec
26 |
27 | # The following section is specific to Flutter.
28 | flutter:
29 |
30 | # The following line ensures that the Material Icons font is
31 | # included with your application, so that you can use the icons in
32 | # the material Icons class.
33 | uses-material-design: true
34 |
35 |
36 | # To add assets to your application, add an assets section, like this:
37 | # assets:
38 | # - images/a_dot_burr.jpeg
39 | # - images/a_dot_ham.jpeg
40 |
41 | # An image asset can refer to one or more resolution-specific "variants", see
42 | # https://flutter.io/assets-and-images/#resolution-aware.
43 |
44 | # For details regarding adding assets from package dependencies, see
45 | # https://flutter.io/assets-and-images/#from-packages
46 |
47 | # To add custom fonts to your application, add a fonts section here,
48 | # in this "flutter" section. Each entry in this list should have a
49 | # "family" key with the font family name, and a "fonts" key with a
50 | # list giving the asset and other descriptors for the font. For
51 | # example:
52 | # fonts:
53 | # - family: Schyler
54 | # fonts:
55 | # - asset: fonts/Schyler-Regular.ttf
56 | # - asset: fonts/Schyler-Italic.ttf
57 | # style: italic
58 | # - family: Trajan Pro
59 | # fonts:
60 | # - asset: fonts/TrajanPro.ttf
61 | # - asset: fonts/TrajanPro_Bold.ttf
62 | # weight: 700
63 | #
64 | # For details regarding fonts from package dependencies,
65 | # see https://flutter.io/custom-fonts/#from-packages
66 |
--------------------------------------------------------------------------------