// Copyright 2018 The Chromium 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 '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); }); testWidgets('interrupts in-progress animations without jumps', (WidgetTester tester) async { final _GeometryListener geometryListener = new _GeometryListener(); ScaffoldGeometry geometry; _GeometryListenerState listenerState; Size previousRect; // The maximum amounts we expect the fab width and height to change during one step of a transition. const double maxDeltaWidth = 12.0; const double maxDeltaHeight = 12.0; // 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."); expect(deltaHeight.abs(), lessThanOrEqualTo(maxDeltaHeight), reason: "The Floating Action Button's width should not change faster than $maxDeltaHeight per animation step."); } previousRect = currentRect; } // We'll listen to the Scaffold's geometry for any 'jumps' to a size of 1 to detect changes in the size and rotation of the fab. // Creating a scaffold with the fab at endFloat await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener)); listenerState = tester.state(find.byType(_GeometryListener)); listenerState.geometryListenable.addListener(check); // Moving the fab to centerFloat' await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener)); await tester.pumpAndSettle(); // Moving the fab to the top start after finishing the previous motion await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation(), listener: geometryListener)); // Interrupting motion to move to the end float 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)); }); } class _GeometryListener extends StatefulWidget { @override State createState() => new _GeometryListenerState(); } class _GeometryListenerState extends State<_GeometryListener> { @override Widget build(BuildContext context) { return new 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 = new _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: const Text('1'), ), FloatingActionButtonLocation location, _GeometryListener listener, TextDirection textDirection: TextDirection.ltr, EdgeInsets viewInsets: const EdgeInsets.only(bottom: 200.0), Widget bab, }) { return new Directionality( textDirection: textDirection, child: new MediaQuery( data: new MediaQueryData(viewInsets: viewInsets), child: new Scaffold( appBar: new 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 new Offset(fabX, fabY); } }