From 59025702db0e0e7f8d2ad9398658d1bdbee85c20 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 23 May 2017 17:48:54 -0700 Subject: [PATCH] AppBar: fix bugs, add docs, add samples (#10223) I added some tests for the bug that I fixed. I added docs for IconButton and AppBar. I added some new constructors for FractionalOffset. --- .../flutter/lib/src/material/app_bar.dart | 61 +++++++-- .../flutter/lib/src/material/icon_button.dart | 10 ++ .../lib/src/painting/fractional_offset.dart | 47 ++++++- packages/flutter/lib/src/widgets/basic.dart | 6 + .../flutter/test/material/app_bar_test.dart | 124 ++++++++++++++++++ .../test/painting/fractional_offset_test.dart | 10 ++ 6 files changed, 242 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 900ca875a4..46989cd6c6 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -119,17 +119,55 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { /// /// An app bar consists of a toolbar and potentially other widgets, such as a /// [TabBar] and a [FlexibleSpaceBar]. App bars typically expose one or more -/// common actions with [IconButton]s which are optionally followed by a -/// [PopupMenuButton] for less common operations. +/// common [actions] with [IconButton]s which are optionally followed by a +/// [PopupMenuButton] for less common operations (sometimes called the "overflow +/// menu"). /// /// App bars are typically used in the [Scaffold.appBar] property, which places /// the app bar as a fixed-height widget at the top of the screen. For a /// scrollable app bar, see [SliverAppBar], which embeds an [AppBar] in a sliver /// for use in a [CustomScrollView]. /// -/// The AppBar displays the toolbar widgets, [leading], [title], and -/// [actions], above the [bottom] (if any). If a [flexibleSpace] widget is -/// specified then it is stacked behind the toolbar and the bottom widget. +/// The AppBar displays the toolbar widgets, [leading], [title], and [actions], +/// above the [bottom] (if any). The [bottom] is usually used for a [TabBar]. If +/// a [flexibleSpace] widget is specified then it is stacked behind the toolbar +/// and the bottom widget. The following diagram shows where each of these slots +/// appears in the toolbar when the writing language is left-to-right (e.g. +/// English): +/// +/// ![The leading widget is in the top left, the actions are in the top right, +/// the title is between them. The bottom is, naturally, at the bottom, and the +/// flexibleSpace is behind all of them.](https://flutter.github.io/assets-for-api-docs/material/app_bar.png) +/// +/// If the [leading] widget is omitted, but the [AppBar] is in a [Scaffold] with +/// a [Drawer], then a button will be inserted to open the drawer. Otherwise, if +/// the nearest [Navigator] has any previous routes, a [BackButton] is inserted +/// instead. +/// +/// ## Sample code +/// +/// ```dart +/// new AppBar( +/// title: new Text('My Fancy Dress'), +/// actions: [ +/// new IconButton( +/// icon: new Icon(Icons.playlist_play), +/// tooltip: 'Air it', +/// onPressed: _airDress, +/// ), +/// new IconButton( +/// icon: new Icon(Icons.playlist_add), +/// tooltip: 'Restitch it', +/// onPressed: _restitchDress, +/// ), +/// new IconButton( +/// icon: new Icon(Icons.playlist_add_check), +/// tooltip: 'Repair it', +/// onPressed: _repairDress, +/// ), +/// ], +/// ) +/// ``` /// /// See also: /// @@ -451,11 +489,17 @@ class _AppBarState extends State { ); } + appBar = new Align( + alignment: FractionalOffset.topCenter, + child: appBar, + ); + if (widget.flexibleSpace != null) { appBar = new Stack( + fit: StackFit.passthrough, children: [ widget.flexibleSpace, - new Positioned(top: 0.0, left: 0.0, right: 0.0, child: appBar), + appBar, ], ); } @@ -463,10 +507,7 @@ class _AppBarState extends State { return new Material( color: widget.backgroundColor ?? themeData.primaryColor, elevation: widget.elevation, - child: new Align( - alignment: FractionalOffset.topCenter, - child: appBar, - ), + child: appBar, ); } } diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 583386b41a..a301190827 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -37,6 +37,16 @@ const double _kMinButtonSize = 48.0; /// requirements in the Material Design specification. The [alignment] controls /// how the icon itself is positioned within the hit region. /// +/// ## Sample code +/// +/// ```dart +/// new IconButton( +/// icon: new Icon(Icons.volume_up), +/// tooltip: 'Increase volume by 10%', +/// onPressed: () { setState(() { _volume *= 1.1; }); }, +/// ) +/// ``` +/// /// See also: /// /// * [Icons], a library of predefined icons. diff --git a/packages/flutter/lib/src/painting/fractional_offset.dart b/packages/flutter/lib/src/painting/fractional_offset.dart index 2529da1a5a..e0b0c725e2 100644 --- a/packages/flutter/lib/src/painting/fractional_offset.dart +++ b/packages/flutter/lib/src/painting/fractional_offset.dart @@ -8,10 +8,14 @@ import 'package:flutter/foundation.dart'; import 'basic_types.dart'; -/// An offset that's expressed as a fraction of a Size. +/// An offset that's expressed as a fraction of a [Size]. /// -/// FractionalOffset(1.0, 0.0) represents the top right of the Size, -/// FractionalOffset(0.0, 1.0) represents the bottom left of the Size, +/// `FractionalOffset(1.0, 0.0)` represents the top right of the [Size]. +/// +/// `FractionalOffset(0.0, 1.0)` represents the bottom left of the [Size]. +/// +/// `FractionalOffset(0.5, 2.0)` represents a point half way across the [Size], +/// below the bottom of the rectangle by the height of the [Size]. @immutable class FractionalOffset { /// Creates a fractional offset. @@ -21,16 +25,47 @@ class FractionalOffset { : assert(dx != null), assert(dy != null); + /// Creates a fractional offset from a specific offset and size. + /// + /// The returned [FractionalOffset] describes the position of the + /// [Offset] in the [Size], as a fraction of the [Size]. + FractionalOffset.fromOffsetAndSize(Offset offset, Size size) : + assert(size != null), + assert(offset != null), + dx = offset.dx / size.width, + dy = offset.dy / size.height; + + /// Creates a fractional offset from a specific offset and rectangle. + /// + /// The offset is assumed to be relative to the same origin as the rectangle. + /// + /// If the offset is relative to the top left of the rectangle, use [new + /// FractionalOffset.fromOffsetAndSize] instead, passing `rect.size`. + /// + /// The returned [FractionalOffset] describes the position of the + /// [Offset] in the [Rect], as a fraction of the [Rect]. + factory FractionalOffset.fromOffsetAndRect(Offset offset, Rect rect) { + return new FractionalOffset.fromOffsetAndSize( + offset - rect.topLeft, + rect.size, + ); + } + /// The distance fraction in the horizontal direction. /// /// A value of 0.0 corresponds to the leftmost edge. A value of 1.0 - /// corresponds to the rightmost edge. + /// corresponds to the rightmost edge. Values are not limited to that range; + /// negative values represent positions to the left of the left edge, and + /// values greater than 1.0 represent positions to the right of the right + /// edge. final double dx; /// The distance fraction in the vertical direction. /// - /// A value of 0.0 corresponds to the topmost edge. A value of 1.0 - /// corresponds to the bottommost edge. + /// A value of 0.0 corresponds to the topmost edge. A value of 1.0 corresponds + /// to the bottommost edge. Values are not limited to that range; negative + /// values represent positions above the top, and values greated than 1.0 + /// represent positions below the bottom. final double dy; /// The top left corner. diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index f299d5e2d3..653b569c3a 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -752,6 +752,12 @@ class Padding extends SingleChildRenderObjectWidget { void updateRenderObject(BuildContext context, RenderPadding renderObject) { renderObject.padding = padding; } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('padding: $padding'); + } } /// A widget that aligns its child within itself and optionally sizes itself diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 38460dab6a..293ffbfc55 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -779,4 +779,128 @@ void main() { ); expect(find.byIcon(Icons.menu), findsOneWidget); }); + + testWidgets('AppBar handles loose children 0', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Center( + child: new AppBar( + leading: new Placeholder(key: key), + title: const Text('Abc'), + actions: [ + const Placeholder(), + const Placeholder(), + const Placeholder(), + ], + ), + ), + ), + ); + expect(tester.renderObject(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); + expect(tester.renderObject(find.byKey(key)).size, const Size(56.0, 56.0)); + }); + + testWidgets('AppBar handles loose children 1', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Center( + child: new AppBar( + leading: new Placeholder(key: key), + title: const Text('Abc'), + actions: [ + const Placeholder(), + const Placeholder(), + const Placeholder(), + ], + flexibleSpace: new DecoratedBox( + decoration: new BoxDecoration( + gradient: new LinearGradient( + begin: const FractionalOffset(0.50, 0.0), + end: const FractionalOffset(0.48, 1.0), + colors: [Colors.blue.shade500, Colors.blue.shade800], + ), + ), + ), + ), + ), + ), + ); + expect(tester.renderObject(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); + expect(tester.renderObject(find.byKey(key)).size, const Size(56.0, 56.0)); + }); + + testWidgets('AppBar handles loose children 2', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Center( + child: new AppBar( + leading: new Placeholder(key: key), + title: const Text('Abc'), + actions: [ + const Placeholder(), + const Placeholder(), + const Placeholder(), + ], + flexibleSpace: new DecoratedBox( + decoration: new BoxDecoration( + gradient: new LinearGradient( + begin: const FractionalOffset(0.50, 0.0), + end: const FractionalOffset(0.48, 1.0), + colors: [Colors.blue.shade500, Colors.blue.shade800], + ), + ), + ), + bottom: new PreferredSize( + preferredSize: const Size(0.0, kToolbarHeight), + child: new Container( + height: 50.0, + padding: const EdgeInsets.all(4.0), + child: const Placeholder( + strokeWidth: 2.0, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ), + ), + ); + expect(tester.renderObject(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); + expect(tester.renderObject(find.byKey(key)).size, const Size(56.0, 56.0)); + }); + + testWidgets('AppBar handles loose children 3', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Center( + child: new AppBar( + leading: new Placeholder(key: key), + title: const Text('Abc'), + actions: [ + const Placeholder(), + const Placeholder(), + const Placeholder(), + ], + bottom: new PreferredSize( + preferredSize: const Size(0.0, kToolbarHeight), + child: new Container( + height: 50.0, + padding: const EdgeInsets.all(4.0), + child: const Placeholder( + strokeWidth: 2.0, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ), + ), + ); + expect(tester.renderObject(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); + expect(tester.renderObject(find.byKey(key)).size, const Size(56.0, 56.0)); + }); } diff --git a/packages/flutter/test/painting/fractional_offset_test.dart b/packages/flutter/test/painting/fractional_offset_test.dart index 1fb5f9cab1..c89d9a11c5 100644 --- a/packages/flutter/test/painting/fractional_offset_test.dart +++ b/packages/flutter/test/painting/fractional_offset_test.dart @@ -26,4 +26,14 @@ void main() { expect(FractionalOffset.lerp(null, b, 0.25), equals(b * 0.25)); expect(FractionalOffset.lerp(a, null, 0.25), equals(a * 0.75)); }); + + test('FractionalOffset.fromOffsetAndSize()', () { + final FractionalOffset a = new FractionalOffset.fromOffsetAndSize(const Offset(100.0, 100.0), const Size(200.0, 400.0)); + expect(a, const FractionalOffset(0.5, 0.25)); + }); + + test('FractionalOffset.fromOffsetAndRect()', () { + final FractionalOffset a = new FractionalOffset.fromOffsetAndRect(const Offset(150.0, 120.0), new Rect.fromLTWH(50.0, 20.0, 200.0, 400.0)); + expect(a, const FractionalOffset(0.5, 0.25)); + }); }