├── CHANGELOG.md
├── .gitignore
├── LICENSE
├── breathing_flutter.iml
├── pubspec.yaml
├── README.md
├── pubspec.lock
└── lib
└── breathing_flutter.dart
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.0.1] - TODO: Add release date.
2 |
3 | * TODO: Describe initial release.
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | build/
8 | ios/.generated/
9 | ios/Flutter/Generated.xcconfig
10 | ios/Runner/GeneratedPluginRegistrant.*
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Pawan Kumar
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/breathing_flutter.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: breathing_flutter
2 | description: A breathing library in Flutter.
3 | version: 0.0.1
4 | author: Pawan Kumar
5 | homepage: https://github.com/iampawan/BreathingFlutter
6 |
7 | environment:
8 | sdk: ">=2.0.0-dev.28.0 <3.0.0"
9 |
10 | dependencies:
11 | flutter:
12 | sdk: flutter
13 |
14 | dev_dependencies:
15 | flutter_test:
16 | sdk: flutter
17 |
18 | # For information on the generic Dart part of this file, see the
19 | # following page: https://www.dartlang.org/tools/pub/pubspec
20 |
21 | # The following section is specific to Flutter.
22 | flutter:
23 |
24 | # To add assets to your package, add an assets section, like this:
25 | # assets:
26 | # - images/a_dot_burr.jpeg
27 | # - images/a_dot_ham.jpeg
28 | #
29 | # For details regarding assets in packages, see
30 | # https://flutter.io/assets-and-images/#from-packages
31 | #
32 | # An image asset can refer to one or more resolution-specific "variants", see
33 | # https://flutter.io/assets-and-images/#resolution-aware.
34 |
35 | # To add custom fonts to your package, add a fonts section here,
36 | # in this "flutter" section. Each entry in this list should have a
37 | # "family" key with the font family name, and a "fonts" key with a
38 | # list giving the asset and other descriptors for the font. For
39 | # example:
40 | # fonts:
41 | # - family: Schyler
42 | # fonts:
43 | # - asset: fonts/Schyler-Regular.ttf
44 | # - asset: fonts/Schyler-Italic.ttf
45 | # style: italic
46 | # - family: Trajan Pro
47 | # fonts:
48 | # - asset: fonts/TrajanPro.ttf
49 | # - asset: fonts/TrajanPro_Bold.ttf
50 | # weight: 700
51 | #
52 | # For details regarding fonts in packages, see
53 | # https://flutter.io/custom-fonts/#from-packages
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BreathingFlutter
2 |
3 | [](https://saythanks.io/to/iampawan) [](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fiampawan%2FBreathingFlutterr)
4 |
5 | A breathing appbar and widget library in Flutter.
6 |
7 | The source code is **100% Dart**, and everything resides in the [/lib](https://github.com/iampawan/BreathingFlutter/tree/master/lib) folder.
8 |
9 |
10 | ### Show some :heart: and star the repo to support the project
11 |
12 | [](https://github.com/iampawan/BreathingFlutter) [](https://github.com/iampawan/BreathingFlutter/fork) [](https://github.com/iampawan/BreathingFlutter) [](https://github.com/iampawan/BreathingFlutter)
13 | [](https://twitter.com/imthepk)
14 |
15 | [](https://opensource.org/licenses/Apache-2.0)
16 |
17 | ## YouTube Channel
18 |
19 | [MTechViral](https://www.youtube.com/c/MTechViral)
20 |
21 | ## Facebook Group
22 |
23 | [Let's Flutter](https://www.facebook.com/groups/425920117856409/)
24 |
25 |
26 |
27 | ## 💻 Installation
28 | In the `dependencies:` section of your `pubspec.yaml`, add the following line:
29 |
30 | ```yaml
31 | breathing_flutter:
32 | ```
33 |
34 | ## ❔ Usage
35 |
36 | ### Breathing AppBar
37 |
38 | ```dart
39 | import 'package:breathing_flutter/breathing_flutter.dart';
40 |
41 | class HomePage extends StatelessWidget {
42 | final GlobalKey _breathingAppBarKey =
43 | GlobalKey();
44 | @override
45 | Widget build(BuildContext context) {
46 | return Scaffold(
47 | appBar: BreathingAppBar(
48 | key: _breathingAppBarKey,
49 | title: Text("Breathing App Bar"),
50 | breathColors: [Colors.black, Colors.red, Colors.blue],
51 | ),
52 | );
53 | }
54 | }
55 |
56 | ```
57 |
58 | ### Breathing Widget
59 |
60 | ```dart
61 | import 'package:breathing_flutter/breathing_flutter.dart';
62 |
63 | class HomePage extends StatelessWidget {
64 | final GlobalKey _breathingWidgetKey =
65 | GlobalKey();
66 | @override
67 | Widget build(BuildContext context) {
68 | return Scaffold(
69 | body: Center(
70 | child: BreathingWidget(
71 | key: _breathingWidgetKey,
72 | breathColors: [
73 | Colors.black,
74 | Colors.deepOrange,
75 | Colors.green,
76 | Colors.indigo
77 | ],
78 | child: Container(
79 | width: 100.0,
80 | height: 100.0,
81 | child: Center(
82 | child: Text(
83 | "Breathing Widget",
84 | textAlign: TextAlign.center,
85 | style: TextStyle(color: Colors.white),
86 | ),
87 | ),
88 | )),
89 | ));
90 | }
91 | }
92 | ```
93 |
94 | ## Stop Breathing
95 | This is how you can stop breathing for an appbar and the widget. Also,
96 | Specify Color in the parameter to fix the appbar or widget color.
97 |
98 | BreathingAppBar
99 | ```
100 | _breathingAppBarKey.currentState.stopBreath(Colors.teal);
101 |
102 | ```
103 | BreathingWigdet
104 |
105 | ```
106 | _breathingWidgetKey.currentState.stopBreath(Colors.amber);
107 |
108 |
109 | ```
110 |
111 | ## Start Breathing
112 | BreathingAppBar
113 | ```
114 | _breathingAppBarKey.currentState.startBreath();
115 |
116 | ```
117 | BreathingWigdet
118 |
119 | ```
120 | _breathingWidgetKey.currentState.startBreath();
121 |
122 |
123 | ```
124 |
125 |
126 | ## 💰 Donations
127 |
128 | This project needs you! If you would like to support this project's further development, the creator of this project or the continuous maintenance of this project, feel free to donate. Your donation is highly appreciated (and I love food, coffee and beer). Thank you!
129 |
130 | **PayPal**
131 |
132 | * **[Donate $5](https://www.paypal.me/imthepk/5)**: Thank's for creating this project, here's a tea (or some juice) for you!
133 | * **[Donate $10](https://www.paypal.me/imthepk/10)**: Wow, I am stunned. Let me take you to the movies!
134 | * **[Donate $15](https://www.paypal.me/imthepk/15)**: I really appreciate your work, let's grab some lunch!
135 | * **[Donate $25](https://www.paypal.me/imthepk/25)**: That's some awesome stuff you did right there, dinner is on me!
136 | * **[Donate $50](https://www.paypal.me/imthepk/50)**: I really really want to support this project, great job!
137 | * **[Donate $100](https://www.paypal.me/imthepk/100)**: You are the man! This project saved me hours (if not days) of struggle and hard work, simply awesome!
138 | * **[Donate $2799](https://www.paypal.me/imthepk/2799)**: Go buddy, buy Macbook Pro for yourself!
139 |
140 | Of course, you can also choose what you want to donate, all donations are awesome!
141 |
142 | ## 👨 Developed By
143 |
144 | ```
145 | Pawan Kumar
146 | ```
147 | GDE (Google Developer Expert) for Flutter. Passionate #Flutter, #Android Developer. #Entrepreneur #YouTuber
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | # 👍 How to Contribute
156 | 1. Fork it
157 | 2. Create your feature branch (git checkout -b my-new-feature)
158 | 3. Commit your changes (git commit -am 'Add some feature')
159 | 4. Push to the branch (git push origin my-new-feature)
160 | 5. Create new Pull Request
161 |
162 | # 📃 License
163 |
164 | Copyright (c) 2018 Pawan Kumar
165 |
166 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
167 |
168 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
169 |
170 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
171 |
172 | ## Getting Started
173 |
174 | For help getting started with Flutter, view our online [documentation](https://flutter.io/).
175 |
176 | For help on editing package code, view the [documentation](https://flutter.io/developing-packages/).
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile
3 | packages:
4 | analyzer:
5 | dependency: transitive
6 | description:
7 | name: analyzer
8 | url: "https://pub.dartlang.org"
9 | source: hosted
10 | version: "0.31.2-alpha.2"
11 | args:
12 | dependency: transitive
13 | description:
14 | name: args
15 | url: "https://pub.dartlang.org"
16 | source: hosted
17 | version: "1.4.3"
18 | async:
19 | dependency: transitive
20 | description:
21 | name: async
22 | url: "https://pub.dartlang.org"
23 | source: hosted
24 | version: "2.0.7"
25 | boolean_selector:
26 | dependency: transitive
27 | description:
28 | name: boolean_selector
29 | url: "https://pub.dartlang.org"
30 | source: hosted
31 | version: "1.0.3"
32 | charcode:
33 | dependency: transitive
34 | description:
35 | name: charcode
36 | url: "https://pub.dartlang.org"
37 | source: hosted
38 | version: "1.1.1"
39 | collection:
40 | dependency: transitive
41 | description:
42 | name: collection
43 | url: "https://pub.dartlang.org"
44 | source: hosted
45 | version: "1.14.6"
46 | convert:
47 | dependency: transitive
48 | description:
49 | name: convert
50 | url: "https://pub.dartlang.org"
51 | source: hosted
52 | version: "2.0.1"
53 | crypto:
54 | dependency: transitive
55 | description:
56 | name: crypto
57 | url: "https://pub.dartlang.org"
58 | source: hosted
59 | version: "2.0.5"
60 | csslib:
61 | dependency: transitive
62 | description:
63 | name: csslib
64 | url: "https://pub.dartlang.org"
65 | source: hosted
66 | version: "0.14.4"
67 | flutter:
68 | dependency: "direct main"
69 | description: flutter
70 | source: sdk
71 | version: "0.0.0"
72 | flutter_test:
73 | dependency: "direct dev"
74 | description: flutter
75 | source: sdk
76 | version: "0.0.0"
77 | front_end:
78 | dependency: transitive
79 | description:
80 | name: front_end
81 | url: "https://pub.dartlang.org"
82 | source: hosted
83 | version: "0.1.0-alpha.12"
84 | glob:
85 | dependency: transitive
86 | description:
87 | name: glob
88 | url: "https://pub.dartlang.org"
89 | source: hosted
90 | version: "1.1.5"
91 | html:
92 | dependency: transitive
93 | description:
94 | name: html
95 | url: "https://pub.dartlang.org"
96 | source: hosted
97 | version: "0.13.3+1"
98 | http:
99 | dependency: transitive
100 | description:
101 | name: http
102 | url: "https://pub.dartlang.org"
103 | source: hosted
104 | version: "0.11.3+16"
105 | http_multi_server:
106 | dependency: transitive
107 | description:
108 | name: http_multi_server
109 | url: "https://pub.dartlang.org"
110 | source: hosted
111 | version: "2.0.5"
112 | http_parser:
113 | dependency: transitive
114 | description:
115 | name: http_parser
116 | url: "https://pub.dartlang.org"
117 | source: hosted
118 | version: "3.1.2"
119 | io:
120 | dependency: transitive
121 | description:
122 | name: io
123 | url: "https://pub.dartlang.org"
124 | source: hosted
125 | version: "0.3.2+1"
126 | js:
127 | dependency: transitive
128 | description:
129 | name: js
130 | url: "https://pub.dartlang.org"
131 | source: hosted
132 | version: "0.6.1"
133 | json_rpc_2:
134 | dependency: transitive
135 | description:
136 | name: json_rpc_2
137 | url: "https://pub.dartlang.org"
138 | source: hosted
139 | version: "2.0.8"
140 | kernel:
141 | dependency: transitive
142 | description:
143 | name: kernel
144 | url: "https://pub.dartlang.org"
145 | source: hosted
146 | version: "0.3.0-alpha.12"
147 | logging:
148 | dependency: transitive
149 | description:
150 | name: logging
151 | url: "https://pub.dartlang.org"
152 | source: hosted
153 | version: "0.11.3+1"
154 | matcher:
155 | dependency: transitive
156 | description:
157 | name: matcher
158 | url: "https://pub.dartlang.org"
159 | source: hosted
160 | version: "0.12.2"
161 | meta:
162 | dependency: transitive
163 | description:
164 | name: meta
165 | url: "https://pub.dartlang.org"
166 | source: hosted
167 | version: "1.1.5"
168 | mime:
169 | dependency: transitive
170 | description:
171 | name: mime
172 | url: "https://pub.dartlang.org"
173 | source: hosted
174 | version: "0.9.6+1"
175 | multi_server_socket:
176 | dependency: transitive
177 | description:
178 | name: multi_server_socket
179 | url: "https://pub.dartlang.org"
180 | source: hosted
181 | version: "1.0.1"
182 | node_preamble:
183 | dependency: transitive
184 | description:
185 | name: node_preamble
186 | url: "https://pub.dartlang.org"
187 | source: hosted
188 | version: "1.4.2"
189 | package_config:
190 | dependency: transitive
191 | description:
192 | name: package_config
193 | url: "https://pub.dartlang.org"
194 | source: hosted
195 | version: "1.0.3"
196 | package_resolver:
197 | dependency: transitive
198 | description:
199 | name: package_resolver
200 | url: "https://pub.dartlang.org"
201 | source: hosted
202 | version: "1.0.3"
203 | path:
204 | dependency: transitive
205 | description:
206 | name: path
207 | url: "https://pub.dartlang.org"
208 | source: hosted
209 | version: "1.6.1"
210 | plugin:
211 | dependency: transitive
212 | description:
213 | name: plugin
214 | url: "https://pub.dartlang.org"
215 | source: hosted
216 | version: "0.2.0+2"
217 | pool:
218 | dependency: transitive
219 | description:
220 | name: pool
221 | url: "https://pub.dartlang.org"
222 | source: hosted
223 | version: "1.3.5"
224 | pub_semver:
225 | dependency: transitive
226 | description:
227 | name: pub_semver
228 | url: "https://pub.dartlang.org"
229 | source: hosted
230 | version: "1.4.1"
231 | quiver:
232 | dependency: transitive
233 | description:
234 | name: quiver
235 | url: "https://pub.dartlang.org"
236 | source: hosted
237 | version: "0.29.0+1"
238 | shelf:
239 | dependency: transitive
240 | description:
241 | name: shelf
242 | url: "https://pub.dartlang.org"
243 | source: hosted
244 | version: "0.7.3+1"
245 | shelf_packages_handler:
246 | dependency: transitive
247 | description:
248 | name: shelf_packages_handler
249 | url: "https://pub.dartlang.org"
250 | source: hosted
251 | version: "1.0.3"
252 | shelf_static:
253 | dependency: transitive
254 | description:
255 | name: shelf_static
256 | url: "https://pub.dartlang.org"
257 | source: hosted
258 | version: "0.2.7+1"
259 | shelf_web_socket:
260 | dependency: transitive
261 | description:
262 | name: shelf_web_socket
263 | url: "https://pub.dartlang.org"
264 | source: hosted
265 | version: "0.2.2+2"
266 | sky_engine:
267 | dependency: transitive
268 | description: flutter
269 | source: sdk
270 | version: "0.0.99"
271 | source_map_stack_trace:
272 | dependency: transitive
273 | description:
274 | name: source_map_stack_trace
275 | url: "https://pub.dartlang.org"
276 | source: hosted
277 | version: "1.1.4"
278 | source_maps:
279 | dependency: transitive
280 | description:
281 | name: source_maps
282 | url: "https://pub.dartlang.org"
283 | source: hosted
284 | version: "0.10.5"
285 | source_span:
286 | dependency: transitive
287 | description:
288 | name: source_span
289 | url: "https://pub.dartlang.org"
290 | source: hosted
291 | version: "1.4.0"
292 | stack_trace:
293 | dependency: transitive
294 | description:
295 | name: stack_trace
296 | url: "https://pub.dartlang.org"
297 | source: hosted
298 | version: "1.9.2"
299 | stream_channel:
300 | dependency: transitive
301 | description:
302 | name: stream_channel
303 | url: "https://pub.dartlang.org"
304 | source: hosted
305 | version: "1.6.7+1"
306 | string_scanner:
307 | dependency: transitive
308 | description:
309 | name: string_scanner
310 | url: "https://pub.dartlang.org"
311 | source: hosted
312 | version: "1.0.2"
313 | term_glyph:
314 | dependency: transitive
315 | description:
316 | name: term_glyph
317 | url: "https://pub.dartlang.org"
318 | source: hosted
319 | version: "1.0.0"
320 | test:
321 | dependency: transitive
322 | description:
323 | name: test
324 | url: "https://pub.dartlang.org"
325 | source: hosted
326 | version: "0.12.41"
327 | typed_data:
328 | dependency: transitive
329 | description:
330 | name: typed_data
331 | url: "https://pub.dartlang.org"
332 | source: hosted
333 | version: "1.1.5"
334 | utf:
335 | dependency: transitive
336 | description:
337 | name: utf
338 | url: "https://pub.dartlang.org"
339 | source: hosted
340 | version: "0.9.0+4"
341 | vector_math:
342 | dependency: transitive
343 | description:
344 | name: vector_math
345 | url: "https://pub.dartlang.org"
346 | source: hosted
347 | version: "2.0.6"
348 | vm_service_client:
349 | dependency: transitive
350 | description:
351 | name: vm_service_client
352 | url: "https://pub.dartlang.org"
353 | source: hosted
354 | version: "0.2.4+3"
355 | watcher:
356 | dependency: transitive
357 | description:
358 | name: watcher
359 | url: "https://pub.dartlang.org"
360 | source: hosted
361 | version: "0.9.7+8"
362 | web_socket_channel:
363 | dependency: transitive
364 | description:
365 | name: web_socket_channel
366 | url: "https://pub.dartlang.org"
367 | source: hosted
368 | version: "1.0.8"
369 | yaml:
370 | dependency: transitive
371 | description:
372 | name: yaml
373 | url: "https://pub.dartlang.org"
374 | source: hosted
375 | version: "2.1.14"
376 | sdks:
377 | dart: ">=2.0.0-dev.62.0 <=2.0.0-dev.63.0.flutter-4c9689c1d2"
378 |
--------------------------------------------------------------------------------
/lib/breathing_flutter.dart:
--------------------------------------------------------------------------------
1 | library breathing_flutter;
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter/services.dart';
6 |
7 | class BreathingWidget extends StatefulWidget {
8 | final Widget child;
9 |
10 | final List breathColors;
11 |
12 | const BreathingWidget(
13 | {Key key, @required this.breathColors, @required this.child})
14 | : super(key: key);
15 |
16 | @override
17 | BreathingWidgetState createState() {
18 | return new BreathingWidgetState();
19 | }
20 | }
21 |
22 | class BreathingWidgetState extends State
23 | with TickerProviderStateMixin {
24 | AnimationController breathColorController;
25 | Animation breathColorAnimation;
26 | Animation breathOpacityAnimation;
27 | List breathColors;
28 |
29 | @override
30 | void initState() {
31 | super.initState();
32 | breathColors = widget.breathColors;
33 | breathColorController =
34 | AnimationController(vsync: this, duration: Duration(milliseconds: 7000))
35 | ..repeat();
36 |
37 | setColorAnimation();
38 | setOpacityAnimation(0.3);
39 | }
40 |
41 | @override
42 | void dispose() {
43 | breathColorController?.dispose();
44 | super.dispose();
45 | }
46 |
47 | void setOpacityAnimation(double begin) {
48 | breathOpacityAnimation = Tween(
49 | begin: begin,
50 | end: 1.0,
51 | ).animate(CurvedAnimation(
52 | parent: breathColorController,
53 | curve: Curves.linear,
54 | ));
55 | }
56 |
57 | void setColorAnimation() {
58 | breathColorAnimation = IntTween(
59 | begin: 0,
60 | end: breathColors.length - 1,
61 | ).animate(breathColorController);
62 | }
63 |
64 | stopBreath(var fixedColor) {
65 | setState(() {
66 | breathColors = [fixedColor];
67 | setColorAnimation();
68 | setOpacityAnimation(1.0);
69 | });
70 | breathColorController.stop();
71 | }
72 |
73 | void startBreath() {
74 | setState(() {
75 | breathColors = widget.breathColors;
76 | setColorAnimation();
77 | setOpacityAnimation(0.3);
78 | });
79 | breathColorController.repeat();
80 | }
81 |
82 | @override
83 | Widget build(BuildContext context) {
84 | return AnimatedBuilder(
85 | animation: breathColorAnimation,
86 | builder: (context, child) => FadeTransition(
87 | opacity: breathOpacityAnimation,
88 | child: DecoratedBox(
89 | decoration: BoxDecoration(
90 | color: breathColors[breathColorAnimation.value],
91 | ),
92 | child: widget.child,
93 | ),
94 | ));
95 | }
96 | }
97 |
98 | const double _kLeadingWidth =
99 | kToolbarHeight; // So the leading button is square.
100 |
101 | class BreathingAppBar extends StatefulWidget implements PreferredSizeWidget {
102 | BreathingAppBar({
103 | Key key,
104 | this.leading,
105 | this.automaticallyImplyLeading = true,
106 | this.title,
107 | this.actions,
108 | this.flexibleSpace,
109 | this.bottom,
110 | this.elevation = 4.0,
111 | this.brightness,
112 | this.iconTheme,
113 | this.textTheme,
114 | this.primary = true,
115 | this.centerTitle,
116 | this.titleSpacing = NavigationToolbar.kMiddleSpacing,
117 | this.toolbarOpacity = 1.0,
118 | this.bottomOpacity = 1.0,
119 | @required this.breathColors,
120 | }) : assert(automaticallyImplyLeading != null),
121 | assert(elevation != null),
122 | assert(primary != null),
123 | assert(titleSpacing != null),
124 | assert(toolbarOpacity != null),
125 | assert(bottomOpacity != null),
126 | preferredSize = new Size.fromHeight(
127 | kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
128 | super(key: key);
129 |
130 | final List breathColors;
131 | final Widget leading;
132 |
133 | final bool automaticallyImplyLeading;
134 |
135 | final Widget title;
136 |
137 | final List actions;
138 |
139 | /// This widget is stacked behind the toolbar and the tabbar. It's height will
140 | /// be the same as the app bar's overall height.
141 | ///
142 | /// A flexible space isn't actually flexible unless the [AppBar]'s container
143 | /// changes the [AppBar]'s size. A [SliverAppBar] in a [CustomScrollView]
144 | /// changes the [AppBar]'s height when scrolled.
145 | ///
146 | /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details.
147 | final Widget flexibleSpace;
148 |
149 | /// This widget appears across the bottom of the app bar.
150 | ///
151 | /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can
152 | /// be used at the bottom of an app bar.
153 | ///
154 | /// See also:
155 | ///
156 | /// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
157 | final PreferredSizeWidget bottom;
158 |
159 | /// The z-coordinate at which to place this app bar. This controls the size of
160 | /// the shadow below the app bar.
161 | ///
162 | /// Defaults to 4, the appropriate elevation for app bars.
163 | final double elevation;
164 |
165 | /// The color to use for the app bar's material. Typically this should be set
166 | /// along with [brightness], [iconTheme], [textTheme].
167 | ///
168 |
169 | /// The brightness of the app bar's material. Typically this is set along
170 | /// with [backgroundColor], [iconTheme], [textTheme].
171 | ///
172 | /// Defaults to [ThemeData.primaryColorBrightness].
173 | final Brightness brightness;
174 |
175 | /// The color, opacity, and size to use for app bar icons. Typically this
176 | /// is set along with [backgroundColor], [brightness], [textTheme].
177 | ///
178 | /// Defaults to [ThemeData.primaryIconTheme].
179 | final IconThemeData iconTheme;
180 |
181 | /// The typographic styles to use for text in the app bar. Typically this is
182 | /// set along with [brightness] [backgroundColor], [iconTheme].
183 | ///
184 | /// Defaults to [ThemeData.primaryTextTheme].
185 | final TextTheme textTheme;
186 |
187 | /// Whether this app bar is being displayed at the top of the screen.
188 | ///
189 | /// If true, the appbar's toolbar elements and [bottom] widget will be
190 | /// padded on top by the height of the system status bar. The layout
191 | /// of the [flexibleSpace] is not affected by the [primary] property.
192 | final bool primary;
193 |
194 | /// Whether the title should be centered.
195 | ///
196 | /// Defaults to being adapted to the current [TargetPlatform].
197 | final bool centerTitle;
198 |
199 | /// The spacing around [title] content on the horizontal axis. This spacing is
200 | /// applied even if there is no [leading] content or [actions]. If you want
201 | /// [title] to take all the space available, set this value to 0.0.
202 | ///
203 | /// Defaults to [NavigationToolbar.kMiddleSpacing].
204 | final double titleSpacing;
205 |
206 | /// How opaque the toolbar part of the app bar is.
207 | ///
208 | /// A value of 1.0 is fully opaque, and a value of 0.0 is fully transparent.
209 | ///
210 | /// Typically, this value is not changed from its default value (1.0). It is
211 | /// used by [SliverAppBar] to animate the opacity of the toolbar when the app
212 | /// bar is scrolled.
213 | final double toolbarOpacity;
214 |
215 | /// How opaque the bottom part of the app bar is.
216 | ///
217 | /// A value of 1.0 is fully opaque, and a value of 0.0 is fully transparent.
218 | ///
219 | /// Typically, this value is not changed from its default value (1.0). It is
220 | /// used by [SliverAppBar] to animate the opacity of the toolbar when the app
221 | /// bar is scrolled.
222 | final double bottomOpacity;
223 |
224 | /// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's
225 | /// preferred height.
226 | ///
227 | /// [Scaffold] uses this this size to set its app bar's height.
228 | @override
229 | final Size preferredSize;
230 |
231 | bool _getEffectiveCenterTitle(ThemeData themeData) {
232 | if (centerTitle != null) return centerTitle;
233 | assert(themeData.platform != null);
234 | switch (themeData.platform) {
235 | case TargetPlatform.android:
236 | case TargetPlatform.fuchsia:
237 | return false;
238 | case TargetPlatform.iOS:
239 | return actions == null || actions.length < 2;
240 | }
241 | return null;
242 | }
243 |
244 | @override
245 | BreathingAppBarState createState() => new BreathingAppBarState();
246 | }
247 |
248 | class BreathingAppBarState extends State
249 | with TickerProviderStateMixin {
250 | List breathColors;
251 |
252 | void _handleDrawerButton() {
253 | Scaffold.of(context).openDrawer();
254 | }
255 |
256 | void _handleDrawerButtonEnd() {
257 | Scaffold.of(context).openEndDrawer();
258 | }
259 |
260 | AnimationController breathColorController;
261 | Animation breathColorAnimation;
262 | Animation breathOpacityAnimation;
263 |
264 | @override
265 | void initState() {
266 | super.initState();
267 | breathColors = widget.breathColors;
268 | breathColorController =
269 | AnimationController(vsync: this, duration: Duration(milliseconds: 7000))
270 | ..repeat();
271 |
272 | setColorAnimation();
273 | setOpacityAnimation(0.3);
274 | }
275 |
276 | @override
277 | void dispose() {
278 | breathColorController?.dispose();
279 | super.dispose();
280 | }
281 |
282 | void setOpacityAnimation(double begin) {
283 | breathOpacityAnimation = Tween(
284 | begin: begin,
285 | end: 1.0,
286 | ).animate(CurvedAnimation(
287 | parent: breathColorController,
288 | curve: Curves.linear,
289 | ));
290 | }
291 |
292 | void setColorAnimation() {
293 | breathColorAnimation = IntTween(
294 | begin: 0,
295 | end: breathColors.length - 1,
296 | ).animate(breathColorController);
297 | }
298 |
299 | stopBreath(var fixedColor) {
300 | setState(() {
301 | breathColors = [fixedColor];
302 | setColorAnimation();
303 | setOpacityAnimation(1.0);
304 | });
305 | breathColorController.stop();
306 | }
307 |
308 | void startBreath() {
309 | setState(() {
310 | breathColors = widget.breathColors;
311 | setColorAnimation();
312 | setOpacityAnimation(0.3);
313 | });
314 | breathColorController.repeat();
315 | }
316 |
317 | @override
318 | Widget build(BuildContext context) {
319 | assert(!widget.primary || debugCheckHasMediaQuery(context));
320 | final ThemeData themeData = Theme.of(context);
321 | final ScaffoldState scaffold = Scaffold.of(context, nullOk: true);
322 | final ModalRoute parentRoute = ModalRoute.of(context);
323 |
324 | final bool hasDrawer = scaffold?.hasDrawer ?? false;
325 | final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
326 | final bool canPop = parentRoute?.canPop ?? false;
327 | final bool useCloseButton =
328 | parentRoute is PageRoute && parentRoute.fullscreenDialog;
329 |
330 | IconThemeData appBarIconTheme =
331 | widget.iconTheme ?? themeData.primaryIconTheme;
332 | TextStyle centerStyle =
333 | widget.textTheme?.title ?? themeData.primaryTextTheme.title;
334 | TextStyle sideStyle =
335 | widget.textTheme?.body1 ?? themeData.primaryTextTheme.body1;
336 |
337 | if (widget.toolbarOpacity != 1.0) {
338 | final double opacity =
339 | const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn)
340 | .transform(widget.toolbarOpacity);
341 | if (centerStyle?.color != null)
342 | centerStyle =
343 | centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity));
344 | if (sideStyle?.color != null)
345 | sideStyle =
346 | sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity));
347 | appBarIconTheme = appBarIconTheme.copyWith(
348 | opacity: opacity * (appBarIconTheme.opacity ?? 1.0));
349 | }
350 |
351 | Widget leading = widget.leading;
352 | if (leading == null && widget.automaticallyImplyLeading) {
353 | if (hasDrawer) {
354 | leading = new IconButton(
355 | icon: const Icon(Icons.menu),
356 | onPressed: _handleDrawerButton,
357 | tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
358 | );
359 | } else {
360 | if (canPop)
361 | leading = useCloseButton ? const CloseButton() : const BackButton();
362 | }
363 | }
364 | if (leading != null) {
365 | leading = new ConstrainedBox(
366 | constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
367 | child: leading,
368 | );
369 | }
370 |
371 | Widget title = widget.title;
372 | if (title != null) {
373 | bool namesRoute;
374 | switch (defaultTargetPlatform) {
375 | case TargetPlatform.android:
376 | case TargetPlatform.fuchsia:
377 | namesRoute = true;
378 | break;
379 | case TargetPlatform.iOS:
380 | break;
381 | }
382 | title = new DefaultTextStyle(
383 | style: centerStyle,
384 | softWrap: false,
385 | overflow: TextOverflow.ellipsis,
386 | child: new Semantics(
387 | namesRoute: namesRoute,
388 | child: title,
389 | header: true,
390 | ),
391 | );
392 | }
393 |
394 | Widget actions;
395 | if (widget.actions != null && widget.actions.isNotEmpty) {
396 | actions = new Row(
397 | mainAxisSize: MainAxisSize.min,
398 | crossAxisAlignment: CrossAxisAlignment.stretch,
399 | children: widget.actions,
400 | );
401 | } else if (hasEndDrawer) {
402 | actions = new IconButton(
403 | icon: const Icon(Icons.menu),
404 | onPressed: _handleDrawerButtonEnd,
405 | tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
406 | );
407 | }
408 |
409 | final Widget toolbar = new NavigationToolbar(
410 | leading: leading,
411 | middle: title,
412 | trailing: actions,
413 | centerMiddle: widget._getEffectiveCenterTitle(themeData),
414 | middleSpacing: widget.titleSpacing,
415 | );
416 |
417 | // If the toolbar is allocated less than kToolbarHeight make it
418 | // appear to scroll upwards within its shrinking container.
419 | Widget appBar = new ClipRect(
420 | child: new CustomSingleChildLayout(
421 | delegate: const _ToolbarContainerLayout(),
422 | child: IconTheme.merge(
423 | data: appBarIconTheme,
424 | child: new DefaultTextStyle(
425 | style: sideStyle,
426 | child: toolbar,
427 | ),
428 | ),
429 | ),
430 | );
431 | if (widget.bottom != null) {
432 | appBar = new Column(
433 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
434 | children: [
435 | new Flexible(
436 | child: new ConstrainedBox(
437 | constraints: const BoxConstraints(maxHeight: kToolbarHeight),
438 | child: appBar,
439 | ),
440 | ),
441 | widget.bottomOpacity == 1.0
442 | ? widget.bottom
443 | : new Opacity(
444 | opacity:
445 | const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn)
446 | .transform(widget.bottomOpacity),
447 | child: widget.bottom,
448 | ),
449 | ],
450 | );
451 | }
452 |
453 | // The padding applies to the toolbar and tabbar, not the flexible space.
454 | if (widget.primary) {
455 | appBar = new SafeArea(
456 | top: true,
457 | child: appBar,
458 | );
459 | }
460 |
461 | appBar = new Align(
462 | alignment: Alignment.topCenter,
463 | child: appBar,
464 | );
465 |
466 | if (widget.flexibleSpace != null) {
467 | appBar = new Stack(
468 | fit: StackFit.passthrough,
469 | children: [
470 | widget.flexibleSpace,
471 | appBar,
472 | ],
473 | );
474 | }
475 | final Brightness brightness =
476 | widget.brightness ?? themeData.primaryColorBrightness;
477 | final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark
478 | ? SystemUiOverlayStyle.light
479 | : SystemUiOverlayStyle.dark;
480 |
481 | return new Semantics(
482 | container: true,
483 | explicitChildNodes: true,
484 | child: new AnnotatedRegion(
485 | value: overlayStyle,
486 | child: AnimatedBuilder(
487 | animation: breathOpacityAnimation,
488 | builder: (context, child) => FadeTransition(
489 | opacity: breathOpacityAnimation,
490 | child: new Material(
491 | color: breathColors[breathColorAnimation.value],
492 | elevation: widget.elevation,
493 | child: appBar,
494 | ),
495 | )),
496 | ),
497 | );
498 | }
499 | }
500 |
501 | class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
502 | const _ToolbarContainerLayout();
503 |
504 | @override
505 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
506 | return constraints.tighten(height: kToolbarHeight);
507 | }
508 |
509 | @override
510 | Size getSize(BoxConstraints constraints) {
511 | return new Size(constraints.maxWidth, kToolbarHeight);
512 | }
513 |
514 | @override
515 | Offset getPositionForChild(Size size, Size childSize) {
516 | return new Offset(0.0, size.height - childSize.height);
517 | }
518 |
519 | @override
520 | bool shouldRelayout(_ToolbarContainerLayout oldDelegate) => false;
521 | }
522 |
--------------------------------------------------------------------------------