// 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))); }); group('New Floating Action Button Locations', () { testWidgets('startTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _topOffsetY)); }); testWidgets('centerTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _topOffsetY)); }); testWidgets('endTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _topOffsetY)); }); testWidgets('startFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _floatOffsetY)); }); testWidgets('centerFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _floatOffsetY)); }); testWidgets('endFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _floatOffsetY)); }); testWidgets('startDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _dockedOffsetY)); }); testWidgets('centerDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _dockedOffsetY)); }); testWidgets('endDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _dockedOffsetY)); }); testWidgets('miniStartTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniLeftOffsetX, _topOffsetY)); }); testWidgets('miniEndTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniEndTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _topOffsetY)); }); testWidgets('miniStartFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniLeftOffsetX, _miniFloatOffsetY)); }); testWidgets('miniCenterFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniCenterFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _miniFloatOffsetY)); }); testWidgets('miniEndFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniEndFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _miniFloatOffsetY)); }); testWidgets('miniStartDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniLeftOffsetX, _dockedOffsetY)); }); testWidgets('miniEndDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniEndDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _dockedOffsetY)); }); // Test a few RTL cases. testWidgets('endTop, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endTop, textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _topOffsetY)); }); testWidgets('miniStartFloat, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartFloat, textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _miniFloatOffsetY)); }); }); group('Custom Floating Action Button Locations', () { testWidgets('Almost end float', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_AlmostEndFloatFabLocation())); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX - 50, _floatOffsetY)); }); testWidgets('Almost end float, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_AlmostEndFloatFabLocation(), textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX + 50, _floatOffsetY)); }); testWidgets('Quarter end top', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_QuarterEndTopFabLocation())); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX * 0.75 + _leftOffsetX * 0.25, _topOffsetY)); }); testWidgets('Quarter end top, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_QuarterEndTopFabLocation(), textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX * 0.75 + _rightOffsetX * 0.25, _topOffsetY)); }); }); group('Moves involving new locations', () { testWidgets('Moves between new locations and new locations', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _topOffsetY)); await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startFloat)); expect(tester.binding.transientCallbackCount, greaterThan(0)); await tester.pumpAndSettle(); expect(tester.binding.transientCallbackCount, 0); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _floatOffsetY)); await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startDocked)); expect(tester.binding.transientCallbackCount, greaterThan(0)); await tester.pumpAndSettle(); expect(tester.binding.transientCallbackCount, 0); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _dockedOffsetY)); }); testWidgets('Moves between new locations and old locations', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _dockedOffsetY)); await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startDocked)); expect(tester.binding.transientCallbackCount, greaterThan(0)); await tester.pumpAndSettle(); expect(tester.binding.transientCallbackCount, 0); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _dockedOffsetY)); await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerFloat)); expect(tester.binding.transientCallbackCount, greaterThan(0)); await tester.pumpAndSettle(); expect(tester.binding.transientCallbackCount, 0); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _floatOffsetY)); await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerTop)); expect(tester.binding.transientCallbackCount, greaterThan(0)); await tester.pumpAndSettle(); expect(tester.binding.transientCallbackCount, 0); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _topOffsetY)); }); testWidgets('Moves between new locations and old locations with custom animator', (WidgetTester tester) async { final FloatingActionButtonAnimator animator = _LinearMovementFabAnimator(); const Offset begin = Offset(_centerOffsetX, _topOffsetY); const Offset end = Offset(_rightOffsetX - 50, _floatOffsetY); final Duration animationDuration = kFloatingActionButtonSegue * 2; await tester.pumpWidget(_singleFabScaffold( FloatingActionButtonLocation.centerTop, animator: animator, )); expect(find.byType(FloatingActionButton), findsOneWidget); expect(tester.binding.transientCallbackCount, 0); await tester.pumpWidget(_singleFabScaffold( _AlmostEndFloatFabLocation(), animator: animator, )); expect(tester.binding.transientCallbackCount, greaterThan(0)); await tester.pump(animationDuration * 0.25); expect(tester.getCenter(find.byType(FloatingActionButton)), offsetMoreOrLessEquals(begin * 0.75 + end * 0.25)); await tester.pump(animationDuration * 0.25); expect(tester.getCenter(find.byType(FloatingActionButton)), offsetMoreOrLessEquals(begin * 0.5 + end * 0.5)); await tester.pump(animationDuration * 0.25); expect(tester.getCenter(find.byType(FloatingActionButton)), offsetMoreOrLessEquals(begin * 0.25 + end * 0.75)); await tester.pumpAndSettle(); expect(tester.getCenter(find.byType(FloatingActionButton)), end); expect(tester.binding.transientCallbackCount, 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; } } const double _leftOffsetX = 44.0; const double _centerOffsetX = 400.0; const double _rightOffsetX = 756.0; const double _miniLeftOffsetX = _leftOffsetX - kMiniButtonOffsetAdjustment; const double _miniRightOffsetX = _rightOffsetX + kMiniButtonOffsetAdjustment; const double _topOffsetY = 56.0; const double _floatOffsetY = 500.0; const double _dockedOffsetY = 544.0; const double _miniFloatOffsetY = _floatOffsetY + kMiniButtonOffsetAdjustment; Widget _singleFabScaffold( FloatingActionButtonLocation location, { FloatingActionButtonAnimator animator, bool mini = false, TextDirection textDirection = TextDirection.ltr, } ) { return MaterialApp( home: Directionality( textDirection: textDirection, child: Scaffold( appBar: AppBar( title: const Text('FloatingActionButtonLocation Test.'), ), bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), title: Text('Home'), ), BottomNavigationBarItem( icon: Icon(Icons.school), title: Text('School'), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () {}, child: const Icon(Icons.beach_access), mini: mini, ), floatingActionButtonLocation: location, floatingActionButtonAnimator: animator, ), ), ); } // 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); } } class _AlmostEndFloatFabLocation extends StandardFabLocation with FabEndOffsetX, FabFloatOffsetY { @override double getOffsetX (ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { final double directionalAdjustment = scaffoldGeometry.textDirection == TextDirection.ltr ? -50.0 : 50.0; return super.getOffsetX(scaffoldGeometry, adjustment) + directionalAdjustment; } } class _QuarterEndTopFabLocation extends StandardFabLocation with FabEndOffsetX, FabTopOffsetY { @override double getOffsetX (ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) { return super.getOffsetX(scaffoldGeometry, adjustment) * 0.75 + (FloatingActionButtonLocation.startFloat as StandardFabLocation) .getOffsetX(scaffoldGeometry, adjustment) * 0.25; } } class _LinearMovementFabAnimator extends FloatingActionButtonAnimator { @override Offset getOffset({@required Offset begin, @required Offset end, @required double progress}) { return Offset.lerp(begin, end, progress); } @override Animation<double> getScaleAnimation({@required Animation<double> parent}) { return const AlwaysStoppedAnimation<double>(1.0); } @override Animation<double> getRotationAnimation({@required Animation<double> parent}) { return const AlwaysStoppedAnimation<double>(1.0); } }