1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Basic floating action button locations', () {
testWidgets('still animates motion when the floating action button is null', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(fab: null, location: null));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.endFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.centerFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
});
testWidgets('moves fab from center to end and back', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('moves to and from custom-defined positions', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation()));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation()));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
expect(tester.binding.transientCallbackCount, 0);
});
group('interrupts in-progress animations without jumps', () {
_GeometryListener geometryListener;
ScaffoldGeometry geometry;
_GeometryListenerState listenerState;
Size previousRect;
Iterable<double> previousRotations;
// The maximum amounts we expect the fab width and height to change
// during one step of a transition.
const double maxDeltaWidth = 12.5;
const double maxDeltaHeight = 12.5;
// The maximum amounts we expect the fab icon to rotate during one step
// of a transition.
const double maxDeltaRotation = 0.09;
// We'll listen to the Scaffold's geometry for any 'jumps' to detect
// changes in the size and rotation of the fab.
void setupListener(WidgetTester tester) {
// Measure the delta in width and height of the fab, and check that it never grows
// by more than the expected maximum deltas.
void check() {
geometry = listenerState.cache.value;
final Size currentRect = geometry.floatingActionButtonArea?.size;
// Measure the delta in width and height of the rect, and check that
// it never grows by more than a safe amount.
if (previousRect != null && currentRect != null) {
final double deltaWidth = currentRect.width - previousRect.width;
final double deltaHeight = currentRect.height - previousRect.height;
expect(
deltaWidth.abs(),
lessThanOrEqualTo(maxDeltaWidth),
reason: "The Floating Action Button's width should not change "
'faster than $maxDeltaWidth per animation step.\n'
'Prevous rect: $previousRect, current rect: $currentRect',
);
expect(
deltaHeight.abs(),
lessThanOrEqualTo(maxDeltaHeight),
reason: "The Floating Action Button's width should not change "
'faster than $maxDeltaHeight per animation step.\n'
'Prevous rect: $previousRect, current rect: $currentRect',
);
}
previousRect = currentRect;
// Measure the delta in rotation.
// Check that it never grows by more than a safe amount.
//
// Note that there may be multiple transitions all active at
// the same time. We are concerned only with the closest one.
final Iterable<RotationTransition> rotationTransitions = tester.widgetList(
find.byType(RotationTransition),
);
final Iterable<double> currentRotations = rotationTransitions.map(
(RotationTransition t) => t.turns.value);
if (previousRotations != null && previousRotations.isNotEmpty
&& currentRotations != null && currentRotations.isNotEmpty
&& previousRect != null && currentRect != null) {
final List<double> deltas = <double>[];
for (final double currentRotation in currentRotations) {
double minDelta;
for (final double previousRotation in previousRotations) {
final double delta = (previousRotation - currentRotation).abs();
minDelta ??= delta;
minDelta = min(delta, minDelta);
}
deltas.add(minDelta);
}
if (deltas.where((double delta) => delta < maxDeltaRotation).isEmpty) {
fail("The Floating Action Button's rotation should not change "
'faster than $maxDeltaRotation per animation step.\n'
'Detected deltas were: $deltas\n'
'Previous values: $previousRotations, current values: $currentRotations\n'
'Prevous rect: $previousRect, current rect: $currentRect',);
}
}
previousRotations = currentRotations;
}
listenerState = tester.state(find.byType(_GeometryListener));
listenerState.geometryListenable.addListener(check);
}
setUp(() {
// We create the geometry listener here, but it can only be set up
// after it is pumped into the widget tree and a tester is
// available.
geometryListener = _GeometryListener();
geometry = null;
listenerState = null;
previousRect = null;
previousRotations = null;
});
testWidgets('moving the fab to centerFloat', (WidgetTester tester) async {
// Create a scaffold with the fab at endFloat
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
setupListener(tester);
// Move the fab to centerFloat'
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
testWidgets('interrupting motion towards the StartTop location.', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
setupListener(tester);
// Move the fab to the top start after creating the fab.
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation(), listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Interrupt motion to move to the end float
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
testWidgets('interrupting entrance to remove the fab.', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
setupListener(tester);
// Animate the fab in.
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Remove the fab.
await tester.pumpWidget(
buildFrame(
fab: null,
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
await tester.pumpAndSettle();
});
testWidgets('interrupting entrance of a new fab.', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
fab: null,
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
setupListener(tester);
// Bring in a new fab.
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Interrupt motion to move the fab.
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
await tester.pumpAndSettle();
});
});
});
testWidgets('Docked floating action button locations', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
// Scaffold 800x600, FAB is 56x56, BAB is 800x100, FAB's center is
// at the top of the BAB.
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.centerDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 500.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0));
});
testWidgets('Docked floating action button locations: no BAB, small BAB', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
viewInsets: EdgeInsets.zero,
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 16.0),
viewInsets: EdgeInsets.zero,
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0));
});
testWidgets('Mini-start-top floating action button location', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(onPressed: () { }, mini: true),
floatingActionButtonLocation: FloatingActionButtonLocation.miniStartTop,
body: Column(
children: const <Widget>[
ListTile(
leading: CircleAvatar(),
),
],
),
),
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)).dx, tester.getCenter(find.byType(CircleAvatar)).dx);
expect(tester.getCenter(find.byType(FloatingActionButton)).dy, kToolbarHeight);
});
testWidgets('Start-top floating action button location LTR', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(16.0, 28.0, 56.0, 56.0)));
});
testWidgets('End-top floating action button location RTL', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
),
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(16.0, 28.0, 56.0, 56.0)));
});
testWidgets('Start-top floating action button location RTL', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
),
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0)));
});
testWidgets('End-top floating action button location LTR', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0)));
});
}
class _GeometryListener extends StatefulWidget {
@override
State createState() => _GeometryListenerState();
}
class _GeometryListenerState extends State<_GeometryListener> {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: cache
);
}
int numNotifications = 0;
ValueListenable<ScaffoldGeometry> geometryListenable;
_GeometryCachePainter cache;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable)
return;
if (geometryListenable != null)
geometryListenable.removeListener(onGeometryChanged);
geometryListenable = newListenable;
geometryListenable.addListener(onGeometryChanged);
cache = _GeometryCachePainter(geometryListenable);
}
void onGeometryChanged() {
numNotifications += 1;
}
}
// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class _GeometryCachePainter extends CustomPainter {
_GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
ScaffoldGeometry value;
@override
void paint(Canvas canvas, Size size) {
value = geometryListenable.value;
}
@override
bool shouldRepaint(_GeometryCachePainter oldDelegate) {
return true;
}
}
Widget buildFrame({
FloatingActionButton fab = const FloatingActionButton(
onPressed: null,
child: Text('1'),
),
FloatingActionButtonLocation location,
_GeometryListener listener,
TextDirection textDirection = TextDirection.ltr,
EdgeInsets viewInsets = const EdgeInsets.only(bottom: 200.0),
Widget bab,
}) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: textDirection,
child: MediaQuery(
data: MediaQueryData(viewInsets: viewInsets),
child: Scaffold(
appBar: AppBar(title: const Text('FabLocation Test')),
floatingActionButtonLocation: location,
floatingActionButton: fab,
bottomNavigationBar: bab,
body: listener,
),
),
));
}
class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _StartTopFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
double fabX;
assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.right;
fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - startPadding;
break;
case TextDirection.ltr:
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.left;
fabX = startPadding;
break;
}
final double fabY = scaffoldGeometry.contentTop - (scaffoldGeometry.floatingActionButtonSize.height / 2.0);
return Offset(fabX, fabY);
}
}