diff --git a/packages/flutter/lib/src/widgets/banner.dart b/packages/flutter/lib/src/widgets/banner.dart index 66604bdd0a..0e87f52d2c 100644 --- a/packages/flutter/lib/src/widgets/banner.dart +++ b/packages/flutter/lib/src/widgets/banner.dart @@ -7,6 +7,7 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; const double _kOffset = 40.0; // distance to bottom of banner, at a 45 degree angle inwards @@ -294,6 +295,7 @@ class Banner extends StatelessWidget { @override Widget build(BuildContext context) { + assert((textDirection != null && layoutDirection != null) || debugCheckHasDirectionality(context)); return new CustomPaint( foregroundPainter: new BannerPainter( message: message, diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 2f234a2d80..653d83706e 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -3792,11 +3792,10 @@ class RichText extends LeafRenderObjectWidget { @override RenderParagraph createRenderObject(BuildContext context) { - final TextDirection direction = textDirection ?? Directionality.of(context); - assert(direction != null, 'A RichText was created with no textDirection and no ambient Directionality widget.'); + assert(textDirection != null || debugCheckHasDirectionality(context)); return new RenderParagraph(text, textAlign: textAlign, - textDirection: direction, + textDirection: textDirection ?? Directionality.of(context), softWrap: softWrap, overflow: overflow, textScaleFactor: textScaleFactor, @@ -3806,6 +3805,7 @@ class RichText extends LeafRenderObjectWidget { @override void updateRenderObject(BuildContext context, RenderParagraph renderObject) { + assert(textDirection != null || debugCheckHasDirectionality(context)); renderObject ..text = text ..textAlign = textAlign diff --git a/packages/flutter/lib/src/widgets/debug.dart b/packages/flutter/lib/src/widgets/debug.dart index 96cddf2b78..d0d64e99f7 100644 --- a/packages/flutter/lib/src/widgets/debug.dart +++ b/packages/flutter/lib/src/widgets/debug.dart @@ -7,6 +7,7 @@ import 'dart:developer' show Timeline; // to disambiguate reference in dartdocs import 'package:flutter/foundation.dart'; +import 'basic.dart'; import 'framework.dart'; import 'media_query.dart'; import 'table.dart'; @@ -202,6 +203,43 @@ bool debugCheckHasMediaQuery(BuildContext context) { return true; } +/// Asserts that the given context has a [Directionality] ancestor. +/// +/// Used by various widgets to make sure that they are only used in an +/// appropriate context. +/// +/// To invoke this function, use the following pattern, typically in the +/// relevant Widget's build method: +/// +/// ```dart +/// assert(debugCheckHasDirectionality(context)); +/// ``` +/// +/// Does nothing if asserts are disabled. Always returns true. +bool debugCheckHasDirectionality(BuildContext context) { + assert(() { + if (context.widget is! Directionality && context.ancestorWidgetOfExactType(Directionality) == null) { + final Element element = context; + throw new FlutterError( + 'No Directionality widget found.\n' + '${context.widget.runtimeType} widgets require a Directionality widget ancestor.\n' + 'The specific widget that could not find a Directionality ancestor was:\n' + ' ${context.widget}\n' + 'The ownership chain for the affected widget is:\n' + ' ${element.debugGetCreatorChain(10)}\n' + 'Typically, the Directionality widget is introduced by the MaterialApp ' + 'or WidgetsApp widget at the top of your application widget tree. It ' + 'determines the ambient reading direction and is used, for example, to ' + 'determine how to lay out text, how to interpret "start" and "end" ' + 'values, and to resolve EdgeInsetsDirectional, ' + 'FractionalOffsetDirectional, and other *Directional objects.' + ); + } + return true; + }()); + return true; +} + /// Asserts that the `built` widget is not null. /// /// Used when the given `widget` calls a builder function to check that the diff --git a/packages/flutter/lib/src/widgets/icon.dart b/packages/flutter/lib/src/widgets/icon.dart index d652c9f0a2..f2cef83edb 100644 --- a/packages/flutter/lib/src/widgets/icon.dart +++ b/packages/flutter/lib/src/widgets/icon.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; import 'icon_data.dart'; import 'icon_theme.dart'; @@ -84,8 +85,8 @@ class Icon extends StatelessWidget { @override Widget build(BuildContext context) { + assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); - assert(textDirection != null, 'Icon widgets required an ambient Directionality.'); final IconThemeData iconTheme = IconTheme.of(context); diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 5d662aa461..d6084bd92d 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -8,6 +8,7 @@ import 'package:vector_math/vector_math_64.dart'; import 'basic.dart'; import 'container.dart'; +import 'debug.dart'; import 'framework.dart'; import 'text.dart'; import 'ticker_provider.dart'; @@ -486,7 +487,8 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState /// See also: /// /// * [AnimatedPositionedDirectional], which adapts to the ambient -/// [Directionality]. +/// [Directionality] (the same as this widget, but for animating +/// [PositionedDirectional]). class AnimatedPositioned extends ImplicitlyAnimatedWidget { /// Creates a widget that animates its position implicitly. /// @@ -545,14 +547,14 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { /// The child's width. /// - /// Only two out of the three horizontal values (left, right, width) can be - /// set. The third must be null. + /// Only two out of the three horizontal values ([left], [right], [width]) can + /// be set. The third must be null. final double width; /// The child's height. /// - /// Only two out of the three vertical values (top, bottom, height) can be - /// set. The third must be null. + /// Only two out of the three vertical values ([top], [bottom], [height]) can + /// be set. The third must be null. final double height; @override @@ -597,7 +599,7 @@ class _AnimatedPositionedState extends AnimatedWidgetBaseState children = []; if (leading != null) @@ -72,7 +74,6 @@ class NavigationToolbar extends StatelessWidget { children.add(new LayoutId(id: _ToolbarSlot.trailing, child: trailing)); final TextDirection textDirection = Directionality.of(context); - assert(textDirection != null); return new CustomMultiChildLayout( delegate: new _ToolbarLayout( centerMiddle: centerMiddle, diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 6178694b30..7873327770 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; import 'notification_listener.dart'; import 'page_storage.dart'; @@ -451,8 +452,8 @@ class _PageViewState extends State { AxisDirection _getDirection(BuildContext context) { switch (widget.scrollDirection) { case Axis.horizontal: + assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); - assert(textDirection != null); final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection); return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection; case Axis.vertical: diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index ee85cd82c2..e39aaecfd7 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; import 'primary_scroll_controller.dart'; import 'scroll_controller.dart'; @@ -180,8 +181,8 @@ abstract class ScrollView extends StatelessWidget { AxisDirection getDirection(BuildContext context) { switch (scrollDirection) { case Axis.horizontal: + assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); - assert(textDirection != null); final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection); return reverse ? flipAxisDirection(axisDirection) : axisDirection; case Axis.vertical: diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index 361a973642..e2bf2bf3b6 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; import 'primary_scroll_controller.dart'; import 'scroll_controller.dart'; @@ -117,8 +118,8 @@ class SingleChildScrollView extends StatelessWidget { AxisDirection _getDirection(BuildContext context) { switch (scrollDirection) { case Axis.horizontal: + assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); - assert(textDirection != null); final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection); return reverse ? flipAxisDirection(axisDirection) : axisDirection; case Axis.vertical: diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index aaeefaebc0..22f34252c4 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -202,6 +202,7 @@ class Table extends RenderObjectWidget { @override RenderTable createRenderObject(BuildContext context) { + assert(debugCheckHasDirectionality(context)); return new RenderTable( columns: children.isNotEmpty ? children[0].children.length : 0, rows: children.length, @@ -218,6 +219,7 @@ class Table extends RenderObjectWidget { @override void updateRenderObject(BuildContext context, RenderTable renderObject) { + assert(debugCheckHasDirectionality(context)); assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0)); assert(renderObject.rows == children.length); renderObject diff --git a/packages/flutter/test/widgets/animated_positioned_test.dart b/packages/flutter/test/widgets/animated_positioned_test.dart index eb7d4d4e95..e12f45d7ad 100644 --- a/packages/flutter/test/widgets/animated_positioned_test.dart +++ b/packages/flutter/test/widgets/animated_positioned_test.dart @@ -102,7 +102,7 @@ void main() { ); }); - testWidgets('AnimatedPositioned - basics (LTR)', (WidgetTester tester) async { + testWidgets('AnimatedPositionedDirectional - basics (LTR)', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); RenderBox box; @@ -188,7 +188,7 @@ void main() { ); }); - testWidgets('AnimatedPositioned - basics (RTL)', (WidgetTester tester) async { + testWidgets('AnimatedPositionedDirectional - basics (RTL)', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); RenderBox box; @@ -274,7 +274,7 @@ void main() { ); }); - testWidgets('AnimatedPositioned - interrupted animation', (WidgetTester tester) async { + testWidgets('AnimatedPositioned - interrupted animation (VISUAL)', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); RenderBox box; @@ -289,10 +289,10 @@ void main() { top: 0.0, width: 100.0, height: 100.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); box = key.currentContext.findRenderObject(); @@ -313,10 +313,10 @@ void main() { top: 100.0, width: 100.0, height: 100.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); box = key.currentContext.findRenderObject(); @@ -337,10 +337,10 @@ void main() { top: 150.0, width: 100.0, height: 100.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); box = key.currentContext.findRenderObject(); @@ -357,7 +357,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(200.0, 200.0))); }); - testWidgets('AnimatedPositioned - switching variables', (WidgetTester tester) async { + testWidgets('AnimatedPositioned - switching variables (VISUAL)', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); RenderBox box; @@ -372,10 +372,10 @@ void main() { top: 0.0, width: 100.0, height: 100.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); box = key.currentContext.findRenderObject(); @@ -396,10 +396,10 @@ void main() { top: 100.0, right: 100.0, // 700.0 from the left height: 100.0, - duration: const Duration(seconds: 2) - ) - ] - ) + duration: const Duration(seconds: 2), + ), + ], + ), ); box = key.currentContext.findRenderObject(); @@ -416,4 +416,308 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(350.0, 150.0))); }); + testWidgets('AnimatedPositionedDirectional - interrupted animation (LTR)', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + + RenderBox box; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 0.0, + top: 0.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0, 50.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 100.0, + top: 100.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(100.0, 100.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 150.0, + top: 150.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(100.0, 100.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(150.0, 150.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(200.0, 200.0))); + }); + + testWidgets('AnimatedPositionedDirectional - switching variables (LTR)', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + + RenderBox box; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 0.0, + top: 0.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(50.0, 50.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 0.0, + top: 100.0, + end: 100.0, // 700.0 from the start + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(350.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(350.0, 100.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(350.0, 150.0))); + }); + + testWidgets('AnimatedPositionedDirectional - interrupted animation (RTL)', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + + RenderBox box; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 0.0, + top: 0.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(750.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(750.0, 50.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 100.0, + top: 100.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(750.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(700.0, 100.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 150.0, + top: 150.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(700.0, 100.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(650.0, 150.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(600.0, 200.0))); + }); + + testWidgets('AnimatedPositionedDirectional - switching variables (RTL)', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + + RenderBox box; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 0.0, + top: 0.0, + width: 100.0, + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(750.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(750.0, 50.0))); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: new Stack( + children: [ + new AnimatedPositionedDirectional( + child: new Container(key: key), + start: 0.0, + top: 100.0, + end: 100.0, // 700.0 from the start + height: 100.0, + duration: const Duration(seconds: 2), + ), + ], + ), + ), + ); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(450.0, 50.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(450.0, 100.0))); + + await tester.pump(const Duration(seconds: 1)); + + box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(450.0, 150.0))); + }); + } diff --git a/packages/flutter/test/widgets/text_test.dart b/packages/flutter/test/widgets/text_test.dart index 89c752a665..e82b62b146 100644 --- a/packages/flutter/test/widgets/text_test.dart +++ b/packages/flutter/test/widgets/text_test.dart @@ -34,4 +34,11 @@ void main() { expect(text, isNotNull); expect(text.textScaleFactor, 3.0); }); + + testWidgets('Text throws a nice error message if there\'s no Directionality', (WidgetTester tester) async { + await tester.pumpWidget(const Text('Hello')); + final String message = tester.takeException().toString(); + expect(message, contains('Directionality')); + expect(message, contains(' Text ')); + }); }