Line data Source code
1 : import 'dart:async';
2 : import 'dart:ui';
3 : import 'snack.dart';
4 : import 'package:flutter/material.dart';
5 : import 'package:flutter/scheduler.dart';
6 :
7 : class SnackRoute<T> extends OverlayRoute<T> {
8 : Animation<double> _filterBlurAnimation;
9 : Animation<Color> _filterColorAnimation;
10 :
11 1 : SnackRoute({
12 : @required this.snack,
13 : RouteSettings settings,
14 1 : }) : super(settings: settings) {
15 2 : this._builder = Builder(builder: (BuildContext innerContext) {
16 0 : return GestureDetector(
17 0 : child: snack,
18 0 : onTap: snack.onTap != null
19 0 : ? () {
20 0 : snack.onTap(snack);
21 : }
22 : : null,
23 : );
24 : });
25 :
26 3 : _configureAlignment(this.snack.snackPosition);
27 3 : _onStatusChanged = snack.onStatusChanged;
28 : }
29 :
30 1 : _configureAlignment(SnackPosition snackPosition) {
31 2 : switch (snack.snackPosition) {
32 1 : case SnackPosition.TOP:
33 : {
34 4 : _initialAlignment = Alignment(-1.0, -2.0);
35 4 : _endAlignment = Alignment(-1.0, -1.0);
36 : break;
37 : }
38 1 : case SnackPosition.BOTTOM:
39 : {
40 3 : _initialAlignment = Alignment(-1.0, 2.0);
41 3 : _endAlignment = Alignment(-1.0, 1.0);
42 : break;
43 : }
44 : }
45 : }
46 :
47 : GetBar snack;
48 : Builder _builder;
49 :
50 0 : Future<T> get completed => _transitionCompleter.future;
51 : final Completer<T> _transitionCompleter = Completer<T>();
52 :
53 : SnackStatusCallback _onStatusChanged;
54 : Alignment _initialAlignment;
55 : Alignment _endAlignment;
56 : bool _wasDismissedBySwipe = false;
57 :
58 : Timer _timer;
59 :
60 1 : bool get opaque => false;
61 :
62 1 : @override
63 : Iterable<OverlayEntry> createOverlayEntries() {
64 1 : List<OverlayEntry> overlays = [];
65 :
66 3 : if (snack.overlayBlur > 0.0) {
67 0 : overlays.add(
68 0 : OverlayEntry(
69 0 : builder: (BuildContext context) {
70 0 : return GestureDetector(
71 0 : onTap: snack.isDismissible ? () => snack.dismiss() : null,
72 0 : child: AnimatedBuilder(
73 0 : animation: _filterBlurAnimation,
74 0 : builder: (context, child) {
75 0 : return BackdropFilter(
76 0 : filter: ImageFilter.blur(
77 0 : sigmaX: _filterBlurAnimation.value,
78 0 : sigmaY: _filterBlurAnimation.value),
79 0 : child: Container(
80 0 : constraints: BoxConstraints.expand(),
81 0 : color: _filterColorAnimation.value,
82 : ),
83 : );
84 : },
85 : ),
86 : );
87 : },
88 : maintainState: false,
89 0 : opaque: opaque),
90 : );
91 : }
92 :
93 1 : overlays.add(
94 1 : OverlayEntry(
95 0 : builder: (BuildContext context) {
96 0 : final Widget annotatedChild = Semantics(
97 0 : child: AlignTransition(
98 0 : alignment: _animation,
99 0 : child: snack.isDismissible
100 0 : ? GetImplDismissibleSnack(_builder)
101 0 : : GetImplSnack(),
102 : ),
103 : focused: false,
104 : container: true,
105 : explicitChildNodes: true,
106 : );
107 : return annotatedChild;
108 : },
109 : maintainState: false,
110 1 : opaque: opaque),
111 : );
112 :
113 : return overlays;
114 : }
115 :
116 : /// This string is a workaround until Dismissible supports a returning item
117 : String dismissibleKeyGen = "";
118 :
119 0 : Widget GetImplDismissibleSnack(Widget child) {
120 0 : return Dismissible(
121 0 : direction: GetImplDismissDirection(),
122 : resizeDuration: null,
123 0 : confirmDismiss: (_) {
124 0 : if (currentStatus == SnackStatus.IS_APPEARING ||
125 0 : currentStatus == SnackStatus.IS_HIDING) {
126 0 : return Future.value(false);
127 : }
128 0 : return Future.value(true);
129 : },
130 0 : key: Key(dismissibleKeyGen),
131 0 : onDismissed: (_) {
132 0 : dismissibleKeyGen += "1";
133 0 : _cancelTimer();
134 0 : _wasDismissedBySwipe = true;
135 :
136 0 : if (isCurrent) {
137 0 : navigator.pop();
138 : } else {
139 0 : navigator.removeRoute(this);
140 : }
141 : },
142 0 : child: GetImplSnack(),
143 : );
144 : }
145 :
146 0 : Widget GetImplSnack() {
147 0 : return Container(
148 0 : margin: snack.margin,
149 0 : child: _builder,
150 : );
151 : }
152 :
153 0 : DismissDirection GetImplDismissDirection() {
154 0 : if (snack.dismissDirection == SnackDismissDirection.HORIZONTAL) {
155 : return DismissDirection.horizontal;
156 : } else {
157 0 : if (snack.snackPosition == SnackPosition.TOP) {
158 : return DismissDirection.up;
159 : } else {
160 : return DismissDirection.down;
161 : }
162 : }
163 : }
164 :
165 1 : @override
166 : bool get finishedWhenPopped =>
167 3 : _controller.status == AnimationStatus.dismissed;
168 :
169 : /// The animation that drives the route's transition and the previous route's
170 : /// forward transition.
171 0 : Animation<Alignment> get animation => _animation;
172 : Animation<Alignment> _animation;
173 :
174 : /// The animation controller that the route uses to drive the transitions.
175 : ///
176 : /// The animation itself is exposed by the [animation] property.
177 0 : @protected
178 0 : AnimationController get controller => _controller;
179 : AnimationController _controller;
180 :
181 : /// Called to create the animation controller that will drive the transitions to
182 : /// this route from the previous one, and back to the previous route from this
183 : /// one.
184 1 : AnimationController createAnimationController() {
185 2 : assert(!_transitionCompleter.isCompleted,
186 0 : 'Cannot reuse a $runtimeType after disposing it.');
187 2 : assert(snack.animationDuration != null &&
188 3 : snack.animationDuration >= Duration.zero);
189 1 : return AnimationController(
190 2 : duration: snack.animationDuration,
191 1 : debugLabel: debugLabel,
192 1 : vsync: navigator,
193 : );
194 : }
195 :
196 : /// Called to create the animation that exposes the current progress of
197 : /// the transition controlled by the animation controller created by
198 : /// [createAnimationController()].
199 1 : Animation<Alignment> createAnimation() {
200 2 : assert(!_transitionCompleter.isCompleted,
201 0 : 'Cannot reuse a $runtimeType after disposing it.');
202 1 : assert(_controller != null);
203 4 : return AlignmentTween(begin: _initialAlignment, end: _endAlignment).animate(
204 1 : CurvedAnimation(
205 1 : parent: _controller,
206 2 : curve: snack.forwardAnimationCurve,
207 2 : reverseCurve: snack.reverseAnimationCurve,
208 : ),
209 : );
210 : }
211 :
212 1 : Animation<double> createBlurFilterAnimation() {
213 4 : return Tween(begin: 0.0, end: snack.overlayBlur).animate(
214 1 : CurvedAnimation(
215 1 : parent: _controller,
216 1 : curve: Interval(
217 : 0.0,
218 : 0.35,
219 : curve: Curves.easeInOutCirc,
220 : ),
221 : ),
222 : );
223 : }
224 :
225 1 : Animation<Color> createColorFilterAnimation() {
226 3 : return ColorTween(begin: Colors.transparent, end: snack.overlayColor)
227 1 : .animate(
228 1 : CurvedAnimation(
229 1 : parent: _controller,
230 1 : curve: Interval(
231 : 0.0,
232 : 0.35,
233 : curve: Curves.easeInOutCirc,
234 : ),
235 : ),
236 : );
237 : }
238 :
239 : T _result;
240 : SnackStatus currentStatus;
241 :
242 : //copy of `routes.dart`
243 1 : void _handleStatusChanged(AnimationStatus status) {
244 : switch (status) {
245 1 : case AnimationStatus.completed:
246 0 : currentStatus = SnackStatus.SHOWING;
247 0 : _onStatusChanged(currentStatus);
248 0 : if (overlayEntries.isNotEmpty) overlayEntries.first.opaque = opaque;
249 :
250 : break;
251 1 : case AnimationStatus.forward:
252 1 : currentStatus = SnackStatus.IS_APPEARING;
253 3 : _onStatusChanged(currentStatus);
254 : break;
255 1 : case AnimationStatus.reverse:
256 0 : currentStatus = SnackStatus.IS_HIDING;
257 0 : _onStatusChanged(currentStatus);
258 0 : if (overlayEntries.isNotEmpty) overlayEntries.first.opaque = false;
259 : break;
260 1 : case AnimationStatus.dismissed:
261 3 : assert(!overlayEntries.first.opaque);
262 : // We might still be the current route if a subclass is controlling the
263 : // the transition and hits the dismissed status. For example, the iOS
264 : // back gesture drives this animation to the dismissed status before
265 : // popping the navigator.
266 1 : currentStatus = SnackStatus.DISMISSED;
267 3 : _onStatusChanged(currentStatus);
268 :
269 1 : if (!isCurrent) {
270 0 : navigator.finalizeRoute(this);
271 0 : assert(overlayEntries.isEmpty);
272 : }
273 : break;
274 : }
275 1 : changedInternalState();
276 : }
277 :
278 1 : @override
279 : void install() {
280 2 : assert(!_transitionCompleter.isCompleted,
281 0 : 'Cannot install a $runtimeType after disposing it.');
282 2 : _controller = createAnimationController();
283 1 : assert(_controller != null,
284 0 : '$runtimeType.createAnimationController() returned null.');
285 2 : _filterBlurAnimation = createBlurFilterAnimation();
286 2 : _filterColorAnimation = createColorFilterAnimation();
287 2 : _animation = createAnimation();
288 1 : assert(_animation != null, '$runtimeType.createAnimation() returned null.');
289 1 : super.install();
290 : }
291 :
292 1 : @override
293 : TickerFuture didPush() {
294 1 : super.didPush();
295 1 : assert(_controller != null,
296 0 : '$runtimeType.didPush called before calling install() or after calling dispose().');
297 2 : assert(!_transitionCompleter.isCompleted,
298 0 : 'Cannot reuse a $runtimeType after disposing it.');
299 3 : _animation.addStatusListener(_handleStatusChanged);
300 1 : _configureTimer();
301 2 : return _controller.forward();
302 : }
303 :
304 0 : @override
305 : void didReplace(Route<dynamic> oldRoute) {
306 0 : assert(_controller != null,
307 0 : '$runtimeType.didReplace called before calling install() or after calling dispose().');
308 0 : assert(!_transitionCompleter.isCompleted,
309 0 : 'Cannot reuse a $runtimeType after disposing it.');
310 0 : if (oldRoute is SnackRoute) _controller.value = oldRoute._controller.value;
311 0 : _animation.addStatusListener(_handleStatusChanged);
312 0 : super.didReplace(oldRoute);
313 : }
314 :
315 1 : @override
316 : bool didPop(T result) {
317 1 : assert(_controller != null,
318 0 : '$runtimeType.didPop called before calling install() or after calling dispose().');
319 2 : assert(!_transitionCompleter.isCompleted,
320 0 : 'Cannot reuse a $runtimeType after disposing it.');
321 :
322 1 : _result = result;
323 1 : _cancelTimer();
324 :
325 1 : if (_wasDismissedBySwipe) {
326 0 : Timer(Duration(milliseconds: 200), () {
327 0 : _controller.reset();
328 : });
329 :
330 0 : _wasDismissedBySwipe = false;
331 : } else {
332 2 : _controller.reverse();
333 : }
334 :
335 1 : return super.didPop(result);
336 : }
337 :
338 1 : void _configureTimer() {
339 2 : if (snack.duration != null) {
340 1 : if (_timer != null && _timer.isActive) {
341 0 : _timer.cancel();
342 : }
343 5 : _timer = Timer(snack.duration, () {
344 1 : if (this.isCurrent) {
345 2 : navigator.pop();
346 0 : } else if (this.isActive) {
347 0 : navigator.removeRoute(this);
348 : }
349 : });
350 : } else {
351 0 : if (_timer != null) {
352 0 : _timer.cancel();
353 : }
354 : }
355 : }
356 :
357 1 : void _cancelTimer() {
358 3 : if (_timer != null && _timer.isActive) {
359 0 : _timer.cancel();
360 : }
361 : }
362 :
363 : /// Whether this route can perform a transition to the given route.
364 : /// Subclasses can override this method to restrict the set of routes they
365 : /// need to coordinate transitions with.
366 0 : bool canTransitionTo(SnackRoute<dynamic> nextRoute) => true;
367 :
368 : /// Whether this route can perform a transition from the given route.
369 : ///
370 : /// Subclasses can override this method to restrict the set of routes they
371 : /// need to coordinate transitions with.
372 0 : bool canTransitionFrom(SnackRoute<dynamic> previousRoute) => true;
373 :
374 1 : @override
375 : void dispose() {
376 2 : assert(!_transitionCompleter.isCompleted,
377 0 : 'Cannot dispose a $runtimeType twice.');
378 2 : _controller?.dispose();
379 3 : _transitionCompleter.complete(_result);
380 1 : super.dispose();
381 : }
382 :
383 : /// A short description of this route useful for debugging.
384 3 : String get debugLabel => '$runtimeType';
385 :
386 1 : @override
387 3 : String toString() => '$runtimeType(animation: $_controller)';
388 : }
389 :
390 1 : SnackRoute showSnack<T>({@required GetBar snack}) {
391 0 : assert(snack != null);
392 :
393 1 : return SnackRoute<T>(
394 : snack: snack,
395 1 : settings: RouteSettings(name: "snackbar"),
396 : );
397 : }
|