diff --git a/dev/benchmarks/complex_layout/lib/main.dart b/dev/benchmarks/complex_layout/lib/main.dart index 7c0899f6fa..9ce2923c4d 100644 --- a/dev/benchmarks/complex_layout/lib/main.dart +++ b/dev/benchmarks/complex_layout/lib/main.dart @@ -577,53 +577,41 @@ class GalleryDrawer extends StatelessWidget { child: new ListView( children: [ new FancyDrawerHeader(), - new DrawerItem( - icon: new Icon(Icons.brightness_5), - onPressed: () { _changeTheme(context, true); }, + new ListTile( + leading: new Icon(Icons.brightness_5), + title: new Text('Light'), + onTap: () { _changeTheme(context, true); }, selected: ComplexLayoutApp.of(context).lightTheme, - child: new Row( - children: [ - new Expanded(child: new Text('Light')), - new Radio( - value: true, - groupValue: ComplexLayoutApp.of(context).lightTheme, - onChanged: (bool value) { _changeTheme(context, value); } - ) - ] - ) + trailing: new Radio( + value: true, + groupValue: ComplexLayoutApp.of(context).lightTheme, + onChanged: (bool value) { _changeTheme(context, value); } + ), ), - new DrawerItem( - icon: new Icon(Icons.brightness_7), - onPressed: () { _changeTheme(context, false); }, + new ListTile( + leading: new Icon(Icons.brightness_7), + title: new Text('Dark'), + onTap: () { _changeTheme(context, false); }, selected: !ComplexLayoutApp.of(context).lightTheme, - child: new Row( - children: [ - new Expanded(child: new Text('Dark')), - new Radio( - value: false, - groupValue: ComplexLayoutApp.of(context).lightTheme, - onChanged: (bool value) { _changeTheme(context, value); } - ) - ] - ) + trailing: new Radio( + value: false, + groupValue: ComplexLayoutApp.of(context).lightTheme, + onChanged: (bool value) { _changeTheme(context, value); }, + ), ), new Divider(), - new DrawerItem( - icon: new Icon(Icons.hourglass_empty), + new ListTile( + leading: new Icon(Icons.hourglass_empty), + title: new Text('Animate Slowly'), selected: timeDilation != 1.0, - onPressed: () { ComplexLayoutApp.of(context).toggleAnimationSpeed(); }, - child: new Row( - children: [ - new Expanded(child: new Text('Animate Slowly')), - new Checkbox( - value: timeDilation != 1.0, - onChanged: (bool value) { ComplexLayoutApp.of(context).toggleAnimationSpeed(); } - ) - ] - ) - ) - ] - ) + onTap: () { ComplexLayoutApp.of(context).toggleAnimationSpeed(); }, + trailing: new Checkbox( + value: timeDilation != 1.0, + onChanged: (bool value) { ComplexLayoutApp.of(context).toggleAnimationSpeed(); } + ), + ), + ], + ), ); } } diff --git a/dev/manual_tests/card_collection.dart b/dev/manual_tests/card_collection.dart index fba6a85add..fd2e0e8f3e 100644 --- a/dev/manual_tests/card_collection.dart +++ b/dev/manual_tests/card_collection.dart @@ -107,10 +107,10 @@ class CardCollectionState extends State { buildFontRadioItem("Center-align text", TextAlign.center, _textAlign, _changeTextAlign, icon: Icons.format_align_center, enabled: !_editable), buildFontRadioItem("Right-align text", TextAlign.right, _textAlign, _changeTextAlign, icon: Icons.format_align_right, enabled: !_editable), new Divider(), - new DrawerItem( - icon: new Icon(Icons.dvr), - onPressed: () { debugDumpApp(); debugDumpRenderTree(); }, - child: new Text('Dump App to Console'), + new ListTile( + leading: new Icon(Icons.dvr), + onTap: () { debugDumpApp(); debugDumpRenderTree(); }, + title: new Text('Dump App to Console'), ), ], ), @@ -167,67 +167,51 @@ class CardCollectionState extends State { } Widget buildDrawerCheckbox(String label, bool value, void callback(), { bool enabled: true }) { - return new DrawerItem( - onPressed: enabled ? callback : null, - child: new Row( - children: [ - new Expanded(child: new Text(label)), - new Checkbox( - value: value, - onChanged: enabled ? (_) { callback(); } : null, - ), - ], + return new ListTile( + onTap: enabled ? callback : null, + title: new Text(label), + trailing: new Checkbox( + value: value, + onChanged: enabled ? (_) { callback(); } : null, ), ); } Widget buildDrawerColorRadioItem(String label, MaterialColor itemValue, MaterialColor currentValue, ValueChanged onChanged, { IconData icon, bool enabled: true }) { - return new DrawerItem( - icon: new Icon(icon), - onPressed: enabled ? () { onChanged(itemValue); } : null, - child: new Row( - children: [ - new Expanded(child: new Text(label)), - new Radio( - value: itemValue, - groupValue: currentValue, - onChanged: enabled ? onChanged : null, - ), - ], + return new ListTile( + leading: new Icon(icon), + title: new Text(label), + onTap: enabled ? () { onChanged(itemValue); } : null, + trailing: new Radio( + value: itemValue, + groupValue: currentValue, + onChanged: enabled ? onChanged : null, ), ); } Widget buildDrawerDirectionRadioItem(String label, DismissDirection itemValue, DismissDirection currentValue, ValueChanged onChanged, { IconData icon, bool enabled: true }) { - return new DrawerItem( - icon: new Icon(icon), - onPressed: enabled ? () { onChanged(itemValue); } : null, - child: new Row( - children: [ - new Expanded(child: new Text(label)), - new Radio( - value: itemValue, - groupValue: currentValue, - onChanged: enabled ? onChanged : null, - ), - ], + return new ListTile( + leading: new Icon(icon), + title: new Text(label), + onTap: enabled ? () { onChanged(itemValue); } : null, + trailing: new Radio( + value: itemValue, + groupValue: currentValue, + onChanged: enabled ? onChanged : null, ), ); } Widget buildFontRadioItem(String label, TextAlign itemValue, TextAlign currentValue, ValueChanged onChanged, { IconData icon, bool enabled: true }) { - return new DrawerItem( - icon: new Icon(icon), - onPressed: enabled ? () { onChanged(itemValue); } : null, - child: new Row( - children: [ - new Expanded(child: new Text(label)), - new Radio( - value: itemValue, - groupValue: currentValue, - onChanged: enabled ? onChanged : null, - ), - ], + return new ListTile( + leading: new Icon(icon), + title: new Text(label), + onTap: enabled ? () { onChanged(itemValue); } : null, + trailing: new Radio( + value: itemValue, + groupValue: currentValue, + onChanged: enabled ? onChanged : null, ), ); } diff --git a/dev/manual_tests/page_view.dart b/dev/manual_tests/page_view.dart index c81c262f90..f7cc6b8f48 100644 --- a/dev/manual_tests/page_view.dart +++ b/dev/manual_tests/page_view.dart @@ -85,27 +85,23 @@ class PageViewAppState extends State { child: new ListView( children: [ new DrawerHeader(child: new Center(child: new Text('Options'))), - new DrawerItem( - icon: new Icon(Icons.more_horiz), + new ListTile( + leading: new Icon(Icons.more_horiz), selected: scrollDirection == Axis.horizontal, - child: new Text('Horizontal Layout'), - onPressed: switchScrollDirection, + trailing: new Text('Horizontal Layout'), + onTap: switchScrollDirection, ), - new DrawerItem( - icon: new Icon(Icons.more_vert), + new ListTile( + leading: new Icon(Icons.more_vert), selected: scrollDirection == Axis.vertical, - child: new Text('Vertical Layout'), - onPressed: switchScrollDirection, + trailing: new Text('Vertical Layout'), + onTap: switchScrollDirection, ), - new DrawerItem( - onPressed: toggleItemsWrap, - child: new Row( - children: [ - new Expanded(child: new Text('Scrolling wraps around')), - // TODO(abarth): Actually make this checkbox change this value. - new Checkbox(value: itemsWrap, onChanged: null), - ], - ), + new ListTile( + onTap: toggleItemsWrap, + title: new Text('Scrolling wraps around'), + // TODO(abarth): Actually make this checkbox change this value. + trailing: new Checkbox(value: itemsWrap, onChanged: null), ), ], ), diff --git a/examples/flutter_gallery/lib/demo/material/drawer_demo.dart b/examples/flutter_gallery/lib/demo/material/drawer_demo.dart index 389dd6118c..0ed5761d45 100644 --- a/examples/flutter_gallery/lib/demo/material/drawer_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/drawer_demo.dart @@ -116,10 +116,10 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: _drawerContents.map((String id) { - return new DrawerItem( - icon: new CircleAvatar(child: new Text(id)), - child: new Text('Drawer item $id'), - onPressed: _showNotImplementedMessage, + return new ListTile( + leading: new CircleAvatar(child: new Text(id)), + title: new Text('Drawer item $id'), + onTap: _showNotImplementedMessage, ); }).toList(), ), @@ -133,15 +133,15 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - new DrawerItem( - icon: new Icon(Icons.add), - child: new Text('Add account'), - onPressed: _showNotImplementedMessage, + new ListTile( + leading: new Icon(Icons.add), + title: new Text('Add account'), + onTap: _showNotImplementedMessage, ), - new DrawerItem( - icon: new Icon(Icons.settings), - child: new Text('Manage accounts'), - onPressed: _showNotImplementedMessage, + new ListTile( + leading: new Icon(Icons.settings), + title: new Text('Manage accounts'), + onTap: _showNotImplementedMessage, ), ], ), diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart index e07025d4f3..98057731f0 100644 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ b/examples/flutter_gallery/lib/gallery/drawer.dart @@ -126,96 +126,88 @@ class GalleryDrawer extends StatelessWidget { final TextStyle aboutTextStyle = themeData.textTheme.body2; final TextStyle linkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor); - final Widget lightThemeItem = new DrawerItem( - icon: new Icon(Icons.brightness_5), - onPressed: () { onThemeChanged(true); }, + final Widget lightThemeItem = new ListTile( + leading: new Icon(Icons.brightness_5), + title: new Text('Light'), + trailing: new Radio( + value: true, + groupValue: useLightTheme, + onChanged: onThemeChanged, + ), selected: useLightTheme, - child: new Row( - children: [ - new Expanded(child: new Text('Light')), - new Radio( - value: true, - groupValue: useLightTheme, - onChanged: onThemeChanged - ) - ] - ) + onTap: () { + onThemeChanged(true); + }, ); - final Widget darkThemeItem = new DrawerItem( - icon: new Icon(Icons.brightness_7), - onPressed: () { onThemeChanged(false); }, - selected: useLightTheme, - child: new Row( - children: [ - new Expanded(child: new Text('Dark')), - new Radio( - value: false, - groupValue: useLightTheme, - onChanged: onThemeChanged - ) - ] - ) + final Widget darkThemeItem = new ListTile( + leading: new Icon(Icons.brightness_7), + title: new Text('Dark'), + trailing: new Radio( + value: false, + groupValue: useLightTheme, + onChanged: onThemeChanged + ), + selected: !useLightTheme, + onTap: () { + onThemeChanged(false); + }, ); - final Widget mountainViewItem = new DrawerItem( + final Widget mountainViewItem = new ListTile( // on iOS, we don't want to show an Android phone icon - icon: new Icon(defaultTargetPlatform == TargetPlatform.iOS ? Icons.star : Icons.phone_android), - onPressed: () { onPlatformChanged(TargetPlatform.android); }, + leading: new Icon(defaultTargetPlatform == TargetPlatform.iOS ? Icons.star : Icons.phone_android), + title: new Text('Android'), + trailing: new Radio( + value: TargetPlatform.android, + groupValue: Theme.of(context).platform, + onChanged: onPlatformChanged, + ), selected: Theme.of(context).platform == TargetPlatform.android, - child: new Row( - children: [ - new Expanded(child: new Text('Android')), - new Radio( - value: TargetPlatform.android, - groupValue: Theme.of(context).platform, - onChanged: onPlatformChanged, - ) - ] - ) + onTap: () { + onPlatformChanged(TargetPlatform.android); + }, ); - final Widget cupertinoItem = new DrawerItem( + final Widget cupertinoItem = new ListTile( // on iOS, we don't want to show the iPhone icon - icon: new Icon(defaultTargetPlatform == TargetPlatform.iOS ? Icons.star_border : Icons.phone_iphone), - onPressed: () { onPlatformChanged(TargetPlatform.iOS); }, + leading: new Icon(defaultTargetPlatform == TargetPlatform.iOS ? Icons.star_border : Icons.phone_iphone), + title: new Text('iOS'), + trailing: new Radio( + value: TargetPlatform.iOS, + groupValue: Theme.of(context).platform, + onChanged: onPlatformChanged, + ), selected: Theme.of(context).platform == TargetPlatform.iOS, - child: new Row( - children: [ - new Expanded(child: new Text('iOS')), - new Radio( - value: TargetPlatform.iOS, - groupValue: Theme.of(context).platform, - onChanged: onPlatformChanged, - ) - ] - ) + onTap: () { + onPlatformChanged(TargetPlatform.iOS); + }, ); - final Widget animateSlowlyItem = new DrawerItem( - icon: new Icon(Icons.hourglass_empty), + final Widget animateSlowlyItem = new ListTile( + leading: new Icon(Icons.hourglass_empty), + title: new Text('Animate Slowly'), + trailing: new Checkbox( + value: timeDilation != 1.0, + onChanged: (bool value) { + onTimeDilationChanged(value ? 20.0 : 1.0); + }, + ), selected: timeDilation != 1.0, - onPressed: () { onTimeDilationChanged(timeDilation != 1.0 ? 1.0 : 20.0); }, - child: new Row( - children: [ - new Expanded(child: new Text('Animate Slowly')), - new Checkbox( - value: timeDilation != 1.0, - onChanged: (bool value) { onTimeDilationChanged(value ? 20.0 : 1.0); } - ) - ] - ) + onTap: () { + onTimeDilationChanged(timeDilation != 1.0 ? 1.0 : 20.0); + }, ); - final Widget sendFeedbackItem = new DrawerItem( - icon: new Icon(Icons.report), - onPressed: onSendFeedback ?? () { + final Widget sendFeedbackItem = new ListTile( + leading: new Icon(Icons.report), + title: new Text('Send feedback'), + onTap: onSendFeedback ?? () { UrlLauncher.launch('https://github.com/flutter/flutter/issues/new'); }, - child: new Text('Send feedback'), ); - final Widget aboutItem = new AboutDrawerItem( + final Widget aboutItem = new AboutListTile( icon: const FlutterLogo(), applicationVersion: '2016 Q3 Preview', applicationIcon: const FlutterLogo(), @@ -273,36 +265,36 @@ class GalleryDrawer extends StatelessWidget { ]; if (onShowPerformanceOverlayChanged != null) { - allDrawerItems.insert(8, new DrawerItem( - icon: new Icon(Icons.assessment), - onPressed: () { onShowPerformanceOverlayChanged(!showPerformanceOverlay); }, + allDrawerItems.insert(8, new ListTile( + leading: new Icon(Icons.assessment), + title: new Text('Performance Overlay'), + trailing: new Checkbox( + value: showPerformanceOverlay, + onChanged: (bool value) { + onShowPerformanceOverlayChanged(!showPerformanceOverlay); + }, + ), selected: showPerformanceOverlay, - child: new Row( - children: [ - new Expanded(child: new Text('Performance Overlay')), - new Checkbox( - value: showPerformanceOverlay, - onChanged: (bool value) { onShowPerformanceOverlayChanged(!showPerformanceOverlay); } - ) - ] - ) + onTap: () { + onShowPerformanceOverlayChanged(!showPerformanceOverlay); + }, )); } if (onCheckerboardRasterCacheImagesChanged != null) { - allDrawerItems.insert(8, new DrawerItem( - icon: new Icon(Icons.assessment), - onPressed: () { onCheckerboardRasterCacheImagesChanged(!checkerboardRasterCacheImages); }, + allDrawerItems.insert(8, new ListTile( + leading: new Icon(Icons.assessment), + title: new Text('Checkerboard Raster Cache Images'), + trailing: new Checkbox( + value: checkerboardRasterCacheImages, + onChanged: (bool value) { + onCheckerboardRasterCacheImagesChanged(!checkerboardRasterCacheImages); + }, + ), selected: checkerboardRasterCacheImages, - child: new Row( - children: [ - new Expanded(child: new Text('Checkerboard Raster Cache Images')), - new Checkbox( - value: checkerboardRasterCacheImages, - onChanged: (bool value) { onCheckerboardRasterCacheImagesChanged(!checkerboardRasterCacheImages); } - ) - ] - ) + onTap: () { + onCheckerboardRasterCacheImagesChanged(!checkerboardRasterCacheImages); + }, )); } diff --git a/examples/flutter_gallery/test/smoke_test.dart b/examples/flutter_gallery/test/smoke_test.dart index e477e79363..4812d4b4ce 100644 --- a/examples/flutter_gallery/test/smoke_test.dart +++ b/examples/flutter_gallery/test/smoke_test.dart @@ -131,7 +131,6 @@ Future runSmokeTest(WidgetTester tester) async { await smokeDemo(tester, routeName); tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after leaving route $routeName'); } - expect(errors, 0); final Finder navigationMenuButton = find.byTooltip('Open navigation menu'); @@ -150,6 +149,11 @@ Future runSmokeTest(WidgetTester tester) async { await tester.pump(); await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. + // scroll the 'Send feedback' item into view + await tester.drag(find.text('Light'), const Offset(0.0, -200.0)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // Wait until it's changed. + // send feedback expect(hasFeedback, false); await tester.tap(find.text('Send feedback')); diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index bb2a3fda6f..b93d540099 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -124,19 +124,20 @@ class StockHomeState extends State { child: new ListView( children: [ new DrawerHeader(child: new Center(child: new Text('Stocks'))), - new DrawerItem( - icon: new Icon(Icons.assessment), + new ListTile( + leading: new Icon(Icons.assessment), + title: new Text('Stock List'), selected: true, - child: new Text('Stock List') ), - new DrawerItem( - icon: new Icon(Icons.account_balance), - onPressed: null, - child: new Text('Account Balance') + new ListTile( + leading: new Icon(Icons.account_balance), + title: new Text('Account Balance'), + enabled: false, ), - new DrawerItem( - icon: new Icon(Icons.dvr), - onPressed: () { + new ListTile( + leading: new Icon(Icons.dvr), + title: new Text('Dump App to Console'), + onTap: () { try { debugDumpApp(); debugDumpRenderTree(); @@ -146,38 +147,43 @@ class StockHomeState extends State { debugPrint('Exception while dumping app:\n$e\n$stack'); } }, - child: new Text('Dump App to Console') ), new Divider(), - new DrawerItem( - icon: new Icon(Icons.thumb_up), - onPressed: () => _handleStockModeChange(StockMode.optimistic), - child: new Row( - children: [ - new Expanded(child: new Text('Optimistic')), - new Radio(value: StockMode.optimistic, groupValue: config.configuration.stockMode, onChanged: _handleStockModeChange) - ] - ) + new ListTile( + leading: new Icon(Icons.thumb_up), + title: new Text('Optimistic'), + trailing: new Radio( + value: StockMode.optimistic, + groupValue: config.configuration.stockMode, + onChanged: _handleStockModeChange + ), + onTap: () { + _handleStockModeChange(StockMode.optimistic); + }, ), - new DrawerItem( - icon: new Icon(Icons.thumb_down), - onPressed: () => _handleStockModeChange(StockMode.pessimistic), - child: new Row( - children: [ - new Expanded(child: new Text('Pessimistic')), - new Radio(value: StockMode.pessimistic, groupValue: config.configuration.stockMode, onChanged: _handleStockModeChange) - ] - ) + new ListTile( + leading: new Icon(Icons.thumb_down), + title: new Text('Pessimistic'), + trailing: new Radio( + value: StockMode.pessimistic, + groupValue: config.configuration.stockMode, + onChanged: _handleStockModeChange + ), + onTap: () { + _handleStockModeChange(StockMode.pessimistic); + }, ), new Divider(), - new DrawerItem( - icon: new Icon(Icons.settings), - onPressed: _handleShowSettings, - child: new Text('Settings')), - new DrawerItem( - icon: new Icon(Icons.help), - onPressed: _handleShowAbout, - child: new Text('About')) + new ListTile( + leading: new Icon(Icons.settings), + title: new Text('Settings'), + onTap: _handleShowSettings, + ), + new ListTile( + leading: new Icon(Icons.help), + title: new Text('About'), + onTap: _handleShowAbout, + ), ] ) ); diff --git a/examples/stocks/lib/stock_settings.dart b/examples/stocks/lib/stock_settings.dart index 4b28f30870..1d93aa0829 100644 --- a/examples/stocks/lib/stock_settings.dart +++ b/examples/stocks/lib/stock_settings.dart @@ -103,139 +103,99 @@ class StockSettingsState extends State { Widget buildSettingsPane(BuildContext context) { final List rows = [ - new DrawerItem( - icon: new Icon(Icons.thumb_up), - onPressed: _confirmOptimismChange, - child: new Row( - children: [ - new Expanded(child: new Text('Everything is awesome')), - new Checkbox( - value: config.configuration.stockMode == StockMode.optimistic, - onChanged: (bool value) => _confirmOptimismChange() - ), - ] - ) + new ListTile( + leading: new Icon(Icons.thumb_up), + title: new Text('Everything is awesome'), + onTap: _confirmOptimismChange, + trailing: new Checkbox( + value: config.configuration.stockMode == StockMode.optimistic, + onChanged: (bool value) => _confirmOptimismChange(), + ), ), - new DrawerItem( - icon: new Icon(Icons.backup), - onPressed: () { _handleBackupChanged(!(config.configuration.backupMode == BackupMode.enabled)); }, - child: new Row( - children: [ - new Expanded(child: new Text('Back up stock list to the cloud')), - new Switch( - value: config.configuration.backupMode == BackupMode.enabled, - onChanged: _handleBackupChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.backup), + title: new Text('Back up stock list to the cloud'), + onTap: () { _handleBackupChanged(!(config.configuration.backupMode == BackupMode.enabled)); }, + trailing: new Switch( + value: config.configuration.backupMode == BackupMode.enabled, + onChanged: _handleBackupChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.picture_in_picture), - onPressed: () { _handleShowPerformanceOverlayChanged(!config.configuration.showPerformanceOverlay); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show rendering performance overlay')), - new Switch( - value: config.configuration.showPerformanceOverlay, - onChanged: _handleShowPerformanceOverlayChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.picture_in_picture), + title: new Text('Show rendering performance overlay'), + onTap: () { _handleShowPerformanceOverlayChanged(!config.configuration.showPerformanceOverlay); }, + trailing: new Switch( + value: config.configuration.showPerformanceOverlay, + onChanged: _handleShowPerformanceOverlayChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.accessibility), - onPressed: () { _handleShowSemanticsDebuggerChanged(!config.configuration.showSemanticsDebugger); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show semantics overlay')), - new Switch( - value: config.configuration.showSemanticsDebugger, - onChanged: _handleShowSemanticsDebuggerChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.accessibility), + title: new Text('Show semantics overlay'), + onTap: () { _handleShowSemanticsDebuggerChanged(!config.configuration.showSemanticsDebugger); }, + trailing: new Switch( + value: config.configuration.showSemanticsDebugger, + onChanged: _handleShowSemanticsDebuggerChanged, + ), ), ]; assert(() { // material grid and size construction lines are only available in checked mode rows.addAll([ - new DrawerItem( - icon: new Icon(Icons.border_clear), - onPressed: () { _handleShowGridChanged(!config.configuration.debugShowGrid); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show material grid (for debugging)')), - new Switch( - value: config.configuration.debugShowGrid, - onChanged: _handleShowGridChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.border_clear), + title: new Text('Show material grid (for debugging)'), + onTap: () { _handleShowGridChanged(!config.configuration.debugShowGrid); }, + trailing: new Switch( + value: config.configuration.debugShowGrid, + onChanged: _handleShowGridChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.border_all), - onPressed: () { _handleShowSizesChanged(!config.configuration.debugShowSizes); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show construction lines (for debugging)')), - new Switch( - value: config.configuration.debugShowSizes, - onChanged: _handleShowSizesChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.border_all), + title: new Text('Show construction lines (for debugging)'), + onTap: () { _handleShowSizesChanged(!config.configuration.debugShowSizes); }, + trailing: new Switch( + value: config.configuration.debugShowSizes, + onChanged: _handleShowSizesChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.format_color_text), - onPressed: () { _handleShowBaselinesChanged(!config.configuration.debugShowBaselines); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show baselines (for debugging)')), - new Switch( - value: config.configuration.debugShowBaselines, - onChanged: _handleShowBaselinesChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.format_color_text), + title: new Text('Show baselines (for debugging)'), + onTap: () { _handleShowBaselinesChanged(!config.configuration.debugShowBaselines); }, + trailing: new Switch( + value: config.configuration.debugShowBaselines, + onChanged: _handleShowBaselinesChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.filter_none), - onPressed: () { _handleShowLayersChanged(!config.configuration.debugShowLayers); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show layer boundaries (for debugging)')), - new Switch( - value: config.configuration.debugShowLayers, - onChanged: _handleShowLayersChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.filter_none), + title: new Text('Show layer boundaries (for debugging)'), + onTap: () { _handleShowLayersChanged(!config.configuration.debugShowLayers); }, + trailing: new Switch( + value: config.configuration.debugShowLayers, + onChanged: _handleShowLayersChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.mouse), - onPressed: () { _handleShowPointersChanged(!config.configuration.debugShowPointers); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show pointer hit-testing (for debugging)')), - new Switch( - value: config.configuration.debugShowPointers, - onChanged: _handleShowPointersChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.mouse), + title: new Text('Show pointer hit-testing (for debugging)'), + onTap: () { _handleShowPointersChanged(!config.configuration.debugShowPointers); }, + trailing: new Switch( + value: config.configuration.debugShowPointers, + onChanged: _handleShowPointersChanged, + ), ), - new DrawerItem( - icon: new Icon(Icons.gradient), - onPressed: () { _handleShowRainbowChanged(!config.configuration.debugShowRainbow); }, - child: new Row( - children: [ - new Expanded(child: new Text('Show repaint rainbow (for debugging)')), - new Switch( - value: config.configuration.debugShowRainbow, - onChanged: _handleShowRainbowChanged - ), - ] - ) + new ListTile( + leading: new Icon(Icons.gradient), + title: new Text('Show repaint rainbow (for debugging)'), + onTap: () { _handleShowRainbowChanged(!config.configuration.debugShowRainbow); }, + trailing: new Switch( + value: config.configuration.debugShowRainbow, + onChanged: _handleShowRainbowChanged, + ), ), ]); return true; diff --git a/examples/stocks/test/icon_color_test.dart b/examples/stocks/test/icon_color_test.dart index 1e5fd48f37..e3dea41d95 100644 --- a/examples/stocks/test/icon_color_test.dart +++ b/examples/stocks/test/icon_color_test.dart @@ -38,12 +38,9 @@ Element findElementOfExactWidgetTypeGoingUp(Element node, Type targetType) { final RegExp materialIconAssetNameColorExtractor = new RegExp(r'[^/]+/ic_.+_(white|black)_[0-9]+dp\.png'); void checkIconColor(WidgetTester tester, String label, Color color) { - // The icon is going to be in the same merged semantics box as the text - // regardless of how the menu item is represented, so this is a good - // way to find the menu item. I hope. - final Element semantics = findElementOfExactWidgetTypeGoingUp(tester.element(find.text(label)), MergeSemantics); - expect(semantics, isNotNull); - final Element asset = findElementOfExactWidgetTypeGoingDown(semantics, RichText); + final Element listTile = findElementOfExactWidgetTypeGoingUp(tester.element(find.text(label)), ListTile); + expect(listTile, isNotNull); + final Element asset = findElementOfExactWidgetTypeGoingDown(listTile, RichText); final RichText richText = asset.widget; expect(richText.text.style.color, equals(color)); } diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index b9740dad77..295ec45a44 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -34,7 +34,6 @@ export 'src/material/dialog.dart'; export 'src/material/divider.dart'; export 'src/material/drawer.dart'; export 'src/material/drawer_header.dart'; -export 'src/material/drawer_item.dart'; export 'src/material/dropdown.dart'; export 'src/material/expand_icon.dart'; export 'src/material/expansion_panel.dart'; diff --git a/packages/flutter/lib/src/foundation/licenses.dart b/packages/flutter/lib/src/foundation/licenses.dart index 8c946d9954..8b103912cf 100644 --- a/packages/flutter/lib/src/foundation/licenses.dart +++ b/packages/flutter/lib/src/foundation/licenses.dart @@ -257,8 +257,8 @@ class LicenseEntryWithLineBreaks extends LicenseEntry { /// * [showAboutDialog], which shows a Material-style dialog with information /// about the application, including a button that shows a [LicensePage] that /// uses this API to select licenses to show. -/// * [AboutDrawerItem], which is a widget to put in a [Drawer] which -/// automatically calls [showAboutDialog]. +/// * [AboutListTile], which is a widget that can be added to a [Drawer]. When +/// tapped it calls [showAboutDialog]. class LicenseRegistry { LicenseRegistry._(); diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index f2cdd23ae3..28171b2794 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -11,21 +11,21 @@ import 'package:flutter/foundation.dart'; import 'app_bar.dart'; import 'debug.dart'; import 'dialog.dart'; -import 'drawer_item.dart'; import 'flat_button.dart'; import 'icon.dart'; import 'icon_theme.dart'; import 'icon_theme_data.dart'; +import 'list_tile.dart'; import 'page.dart'; import 'progress_indicator.dart'; import 'scaffold.dart'; import 'scrollbar.dart'; import 'theme.dart'; -/// A [DrawerItem] to show an about box. +/// A [ListTile] that shows an about box. /// -/// Place this in a [Drawer], specifying your preferred application name, -/// version, icon, and copyright in the appropriate fields. +/// This widget is often added to an app's [Drawer]. When tapped it shows +/// an about box dialog with [showAboutDialog]. /// /// The about box will include a button that shows licenses for software used by /// the application. The licenses shown are those returned by the @@ -33,13 +33,13 @@ import 'theme.dart'; /// /// If your application does not have a [Drawer], you should provide an /// affordance to call [showAboutDialog] or (at least) [showLicensePage]. -class AboutDrawerItem extends StatelessWidget { - /// Creates a drawer item for showing an about box. +class AboutListTile extends StatelessWidget { + /// Creates a list tile for showing an about box. /// /// The arguments are all optional. The application name, if omitted, will be /// derived from the nearest [Title] widget. The version, icon, and legalese /// values default to the empty string. - AboutDrawerItem({ + AboutListTile({ Key key, this.icon: const Icon(null), this.child, @@ -109,10 +109,10 @@ class AboutDrawerItem extends StatelessWidget { @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); - return new DrawerItem( - icon: icon, - child: child ?? new Text('About ${applicationName ?? _defaultApplicationName(context)}'), - onPressed: () { + return new ListTile( + leading: icon, + title: child ?? new Text('About ${applicationName ?? _defaultApplicationName(context)}'), + onTap: () { showAboutDialog( context: context, applicationName: applicationName, @@ -131,7 +131,7 @@ class AboutDrawerItem extends StatelessWidget { /// /// The arguments correspond to the properties on [AboutDialog]. /// -/// If the application has a [Drawer], consider using [AboutDrawerItem] instead +/// If the application has a [Drawer], consider using [AboutListTile] instead /// of calling this directly. /// /// If you do not need an about box in your application, you should at least @@ -164,7 +164,7 @@ void showAboutDialog({ /// /// The arguments correspond to the properties on [LicensePage]. /// -/// If the application has a [Drawer], consider using [AboutDrawerItem] instead +/// If the application has a [Drawer], consider using [AboutListTile] instead /// of calling this directly. /// /// The [AboutDialog] shown by [showAboutDialog] includes a button that calls @@ -196,7 +196,7 @@ void showLicensePage({ /// /// To show an [AboutDialog], use [showAboutDialog]. /// -/// If the application has a [Drawer], the [AboutDrawerItem] widget can make the +/// If the application has a [Drawer], the [AboutListTile] widget can make the /// process of showing an about dialog simpler. /// /// The [AboutDialog] shown by [showAboutDialog] includes a button that calls @@ -316,7 +316,7 @@ class AboutDialog extends StatelessWidget { /// /// To show a [LicensePage], use [showLicensePage]. /// -/// The [AboutDialog] shown by [showAboutDialog] and [AboutDrawerItem] includes +/// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes /// a button that calls [showLicensePage]. /// /// The licenses shown on the [LicensePage] are those returned by the diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index 27828fedab..3a6b709e32 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -92,7 +92,7 @@ class ButtonTheme extends InheritedWidget { /// Defaults to 16.0 pixels of horizontal padding. final EdgeInsets padding; - /// The color from the closest instance of this class that encloses the given context. + /// The closest instance of this class that encloses the given context. /// /// Typical usage is as follows: /// diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index a1ed1c04b8..eb0d632404 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'list_tile.dart'; import 'material.dart'; // TODO(eseidel): Draw width should vary based on device size: @@ -30,7 +31,23 @@ const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); /// /// Drawers are typically used with the [Scaffold.drawer] property. The child of /// the drawer is usually a [ListView] whose first child is a [DrawerHeader] -/// that displays status information about the current user. +/// that displays status information about the current user. The remaining +/// drawer children are often constructed with [ListTile]s, often concluding +/// with an [AboutListTile]. +/// +/// An open drawer can be closed by calling [Navigator.pop]. For example +/// a drawer item might close the drawer when tapped: +/// +/// ```dart +/// new ListTile( +/// leading: new Icon(Icons.change_history), +/// title: new Text('Change history'), +/// onTap: () { +/// // change app state... +/// Navigator.pop(context); // close the drawer +/// }, +/// ); +/// ``` /// /// The [AppBar] automatically displays an appropriate [IconButton] to show the /// [Drawer] when a [Drawer] is available in the [Scaffold]. The [Scaffold] @@ -43,9 +60,6 @@ const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); /// * [Scaffold.of], to obtain the current [ScaffoldState], which manages the /// display and animation of the drawer. /// * [ScaffoldState.openDrawer], which displays its [Drawer], if any. -/// * [Navigator.pop], which closes the drawer if it is open. -/// * [DrawerItem], a widget for items in drawers. -/// * [DrawerHeader], a widget for the top part of a drawer. /// * class Drawer extends StatelessWidget { /// Creates a material design drawer. @@ -233,8 +247,7 @@ class DrawerControllerState extends State with SingleTickerPro final ColorTween _color = new ColorTween(begin: Colors.transparent, end: Colors.black54); final GlobalKey _gestureDetectorKey = new GlobalKey(); - @override - Widget build(BuildContext context) { + Widget _buildDrawer(BuildContext context) { if (_controller.status == AnimationStatus.dismissed) { return new Align( alignment: FractionalOffset.centerLeft, @@ -245,7 +258,7 @@ class DrawerControllerState extends State with SingleTickerPro behavior: HitTestBehavior.translucent, excludeFromSemantics: true, child: new Container(width: _kEdgeDragWidth) - ) + ), ); } else { return new GestureDetector( @@ -263,8 +276,8 @@ class DrawerControllerState extends State with SingleTickerPro decoration: new BoxDecoration( backgroundColor: _color.evaluate(_controller) ), - child: new Container() - ) + child: new Container(), + ), ), new Align( alignment: FractionalOffset.centerLeft, @@ -275,14 +288,21 @@ class DrawerControllerState extends State with SingleTickerPro child: new Focus( key: _drawerKey, child: config.child - ) - ) - ) - ) - ] - ) - ) + ), + ), + ), + ), + ], + ), + ), ); } } + @override + Widget build(BuildContext context) { + return new ListTileTheme( + style: ListTileStyle.drawer, + child: _buildDrawer(context), + ); + } } diff --git a/packages/flutter/lib/src/material/drawer_header.dart b/packages/flutter/lib/src/material/drawer_header.dart index a0cd8ede5a..cc50523352 100644 --- a/packages/flutter/lib/src/material/drawer_header.dart +++ b/packages/flutter/lib/src/material/drawer_header.dart @@ -17,14 +17,12 @@ const double _kDrawerHeaderHeight = 160.0 + 1.0; // bottom edge /// Part of the material design [Drawer]. /// /// Requires one of its ancestors to be a [Material] widget. This condition is -/// satisfied by putting the [DrawerItem] in a [Drawer]. +/// satisfied by putting the [DrawerHeader] in a [Drawer]. /// /// See also: /// -/// * [Drawer] /// * [UserAccountsDrawerHeader], a variant of [DrawerHeader] that is /// specialized for showing user accounts. -/// * [DrawerItem] /// * class DrawerHeader extends StatelessWidget { /// Creates a material design drawer header. diff --git a/packages/flutter/lib/src/material/drawer_item.dart b/packages/flutter/lib/src/material/drawer_item.dart deleted file mode 100644 index c7136d9b01..0000000000 --- a/packages/flutter/lib/src/material/drawer_item.dart +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2015 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/widgets.dart'; - -import 'colors.dart'; -import 'constants.dart'; -import 'debug.dart'; -import 'icon.dart'; -import 'icon_theme.dart'; -import 'icon_theme_data.dart'; -import 'image_icon.dart'; -import 'ink_well.dart'; -import 'theme.dart'; - -/// An item in a material design drawer. -/// -/// Part of the material design [Drawer]. -/// -/// Requires one of its ancestors to be a [Material] widget. This condition is -/// satisfied by putting the [DrawerItem] in a [Drawer]. -/// -/// See also: -/// -/// * [Drawer] -/// * [DrawerHeader] -/// * -class DrawerItem extends StatelessWidget { - /// Creates a material design drawer item. - /// - /// Requires one of its ancestors to be a [Material] widget. - const DrawerItem({ - Key key, - this.icon: const Icon(null), - @required this.child, - this.onPressed, - this.selected: false - }) : super(key: key); - - /// The icon to display before the child widget. - /// - /// The size and color of the icon is configured automatically using an - /// [IconTheme] and therefore do not need to be explicitly given in the - /// icon widget. - /// - /// See [Icon], [ImageIcon]. - final Widget icon; - - /// The widget below this widget in the tree. - final Widget child; - - /// Called when the user taps this drawer item. - /// - /// If null, the drawer item is displayed as disabled. - /// - /// To close the [Drawer] when an item is pressed, call [Navigator.pop]. - final VoidCallback onPressed; - - /// Whether this drawer item is currently selected. - /// - /// The currently selected item is highlighted to distinguish it from other - /// drawer items. - final bool selected; - - Color _getIconColor(ThemeData themeData) { - switch (themeData.brightness) { - case Brightness.light: - if (selected) - return themeData.primaryColor; - if (onPressed == null) - return Colors.black26; - return Colors.black45; - case Brightness.dark: - if (selected) - return themeData.accentColor; - if (onPressed == null) - return Colors.white30; - return null; // use default icon theme color unmodified - } - assert(themeData.brightness != null); - return null; - } - - TextStyle _getTextStyle(ThemeData themeData) { - final TextStyle result = themeData.textTheme.body2; - if (selected) { - switch (themeData.brightness) { - case Brightness.light: - return result.copyWith(color: themeData.primaryColor); - case Brightness.dark: - return result.copyWith(color: themeData.accentColor); - } - } - return result; - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - final ThemeData themeData = Theme.of(context); - - final List children = []; - if (icon != null) { - children.add( - new Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: new IconTheme.merge( - context: context, - data: new IconThemeData( - color: _getIconColor(themeData), - size: 24.0 - ), - child: icon - ) - ) - ); - } - if (child != null) { - children.add( - new Expanded( - child: new Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: new AnimatedDefaultTextStyle( - style: _getTextStyle(themeData), - duration: kThemeChangeDuration, - child: child - ) - ) - ) - ); - } - - return new MergeSemantics( - child: new Container( - height: 48.0, - child: new InkWell( - onTap: onPressed, - child: new Row(children: children) - ) - ) - ); - } - -} diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 7cfdb05d8a..8387fe3a7f 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -5,8 +5,11 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; +import 'icon_theme.dart'; +import 'icon_theme_data.dart'; import 'ink_well.dart'; import 'theme.dart'; @@ -28,7 +31,20 @@ enum MaterialListType { twoLine, /// A list tile that contains three lines of text. - threeLine + threeLine, +} + +/// Defines the title font used for [ListTile] descendants of a [ListTileTheme]. +/// +/// List tiles that appear in a [Drawer] use the theme's [TextTheme.body2] +/// text style, which is a little smaller than the theme's [TextTheme.subhead] +/// text style, which is used by default. +enum ListTileStyle { + // Use a title font that's appropriate for a [ListTile] in a list. + list, + + // Use a title font that's appropriate for a [ListTile] that appears in a [Drawer]. + drawer, } /// The vertical extent of the different types of material list tiles. @@ -46,6 +62,64 @@ Map kListTileExtent = const MaterialListType.threeLine: 88.0, }; +/// An inherited widget that defines color and style parameters for [ListTile]s +/// in this widget's subtree. +/// +/// Values specified here are used for [ListTile] properties that are not given +/// an explicit non-null value. +/// +/// The [Drawer] widget specifies a tile theme for its children which sets +/// [style] to [ListTileStyle.drawer]. +class ListTileTheme extends InheritedWidget { + /// Creates an inherited widget that defines color and style parameters for [ListTile]s. + const ListTileTheme({ + Key key, + this.dense: false, + this.style: ListTileStyle.list, + this.selectedColor, + this.iconColor, + this.textColor, + Widget child, + }) : super(key: key, child: child); + + /// If true then [ListTile]s will have the vertically dense layout. + final bool dense; + + /// If specified, [style] defines the font used for [ListTile] titles. + final ListTileStyle style; + + /// If specified, the color used for icons and text when a [ListTile] is selected. + final Color selectedColor; + + /// If specified, the icon color used for enabled [ListTile]s that are not selected. + final Color iconColor; + + /// If specified, the text color used for enabled [ListTile]s that are not selected. + final Color textColor; + + /// The closest instance of this class that encloses the given context. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ListTileTheme theme = ListTileTheme.of(context); + /// ``` + static ListTileTheme of(BuildContext context) { + final ListTileTheme result = context.inheritFromWidgetOfExactType(ListTileTheme); + return result ?? new ListTileTheme(); + } + + @override + bool updateShouldNotify(ListTileTheme oldTheme) { + return dense != oldTheme.dense + || style != oldTheme.style + || selectedColor != oldTheme.selectedColor + || iconColor != oldTheme.iconColor + || textColor != oldTheme.textColor; + } +} + + /// A single fixed-height row that typically contains some text as well as /// a leading or trailing icon. /// @@ -69,14 +143,15 @@ Map kListTileExtent = const /// /// See also: /// -/// * [kListTileExtent], which defines the ListTile sizes. +/// * [ListTileTheme], which defines visual properties for [ListTile]s. /// * [ListView], which can display an arbitrary number of [ListTile]s /// in a scrolling list. -/// * [Card], which can be used with [Column] to show a few [ListTile]s. /// * [CircleAvatar], which shows an icon representing a person and is often /// used as the [leading] element of a ListTile. +/// * [Card], which can be used with [Column] to show a few [ListTile]s. /// * [Divider], which can be used to separate [ListTile]s. /// * [ListTile.divideTiles], a utility for inserting [Divider]s in between [ListTile]s. +/// * [kListTileExtent], which defines the ListTile sizes. /// * class ListTile extends StatelessWidget { /// Creates a list tile. @@ -91,15 +166,20 @@ class ListTile extends StatelessWidget { this.subtitle, this.trailing, this.isThreeLine: false, - this.dense: false, + this.dense, this.enabled: true, this.onTap, - this.onLongPress - }) : super(key: key); + this.onLongPress, + this.selected: false, + }) : super(key: key) { + assert(isThreeLine != null); + assert(enabled != null); + assert(selected != null); + } /// A widget to display before the title. /// - /// Typically a [CircleAvatar] widget. + /// Typically an [Icon] or a [CircleAvatar] widget. final Widget leading; /// The primary content of the list tile. @@ -124,6 +204,8 @@ class ListTile extends StatelessWidget { final bool isThreeLine; /// Whether this list tile is part of a vertically dense list. + /// + /// If this property is null then its value is based on [ListTileTheme.dense]. final bool dense; /// Whether this list tile is interactive. @@ -143,6 +225,12 @@ class ListTile extends StatelessWidget { /// Inoperative if [enabled] is false. final GestureLongPressCallback onLongPress; + /// If this tile is also [enabled] then icons and text are rendered with the same color. + /// + /// By default the selected color is the theme's primary color. The selected color + /// can be overridden with a [ListTileTheme]. + final bool selected; + /// Add a one pixel border in between each tile. If color isn't specified the /// [ThemeData.dividerColor] of the context's [Theme] is used. /// @@ -174,50 +262,103 @@ class ListTile extends StatelessWidget { yield tile; } - TextStyle _primaryTextStyle(BuildContext context) { - final ThemeData theme = Theme.of(context); - final TextStyle style = theme.textTheme.subhead; - if (!enabled) { - final Color color = theme.disabledColor; - return dense ? style.copyWith(fontSize: 13.0, color: color) : style.copyWith(color: color); + Color _iconColor(ThemeData theme, ListTileTheme tileTheme) { + if (!enabled) + return theme.disabledColor; + + if (selected && tileTheme?.selectedColor != null) + return tileTheme.selectedColor; + + if (!selected && tileTheme?.iconColor != null) + return tileTheme.iconColor; + + switch (theme.brightness) { + case Brightness.light: + return selected ? theme.primaryColor : Colors.black45; + case Brightness.dark: + return selected ? theme.accentColor : null; // null - use current icon theme color } - return dense ? style.copyWith(fontSize: 13.0) : style; + assert(theme.brightness != null); + return null; } - TextStyle _secondaryTextStyle(BuildContext context) { - final ThemeData theme = Theme.of(context); - final Color color = theme.textTheme.caption.color; + Color _textColor(ThemeData theme, ListTileTheme tileTheme, Color defaultColor) { + if (!enabled) + return theme.disabledColor; + + if (selected && tileTheme?.selectedColor != null) + return tileTheme.selectedColor; + + if (!selected && tileTheme?.textColor != null) + return tileTheme.textColor; + + if (selected) { + switch (theme.brightness) { + case Brightness.light: + return theme.primaryColor; + case Brightness.dark: + return theme.accentColor; + } + } + return defaultColor; + } + + bool _denseLayout(ListTileTheme tileTheme) { + return dense != null ? dense : (tileTheme?.dense ?? false); + } + + TextStyle _titleTextStyle(ThemeData theme, ListTileTheme tileTheme) { + final TextStyle style = tileTheme?.style == ListTileStyle.drawer + ? theme.textTheme.body2 + : theme.textTheme.subhead; + final Color color = _textColor(theme, tileTheme, style.color); + return _denseLayout(tileTheme) + ? style.copyWith(fontSize: 13.0, color: color) + : style.copyWith(color: color); + } + + TextStyle _subtitleTextStyle(ThemeData theme, ListTileTheme tileTheme) { final TextStyle style = theme.textTheme.body1; - return dense ? style.copyWith(color: color, fontSize: 12.0) : style.copyWith(color: color); + final Color color = _textColor(theme, tileTheme, theme.textTheme.caption.color); + return _denseLayout(tileTheme) + ? style.copyWith(color: color, fontSize: 12.0) + : style.copyWith(color: color); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); + final ThemeData theme = Theme.of(context); + final ListTileTheme tileTheme = ListTileTheme.of(context); + final bool isTwoLine = !isThreeLine && subtitle != null; final bool isOneLine = !isThreeLine && !isTwoLine; double tileHeight; if (isOneLine) - tileHeight = dense ? 48.0 : 56.0; + tileHeight = _denseLayout(tileTheme) ? 48.0 : 56.0; else if (isTwoLine) - tileHeight = dense ? 60.0 : 72.0; + tileHeight = _denseLayout(tileTheme) ? 60.0 : 72.0; else - tileHeight = dense ? 76.0 : 88.0; + tileHeight = _denseLayout(tileTheme) ? 76.0 : 88.0; // Overall, the list tile is a Row() with these children. final List children = []; if (leading != null) { - children.add(new Container( - margin: const EdgeInsets.only(right: 16.0), - width: 40.0, - alignment: FractionalOffset.centerLeft, - child: leading + children.add(new IconTheme.merge( + context: context, + data: new IconThemeData(color: _iconColor(theme, tileTheme)), + child: new Container( + margin: const EdgeInsets.only(right: 16.0), + width: 40.0, + alignment: FractionalOffset.centerLeft, + child: leading + ), )); } final Widget primaryLine = new AnimatedDefaultTextStyle( - style: _primaryTextStyle(context), + style: _titleTextStyle(theme, tileTheme), duration: kThemeChangeDuration, child: title ?? new Container() ); @@ -229,22 +370,22 @@ class ListTile extends StatelessWidget { children: [ primaryLine, new AnimatedDefaultTextStyle( - style: _secondaryTextStyle(context), + style: _subtitleTextStyle(theme, tileTheme), duration: kThemeChangeDuration, - child: subtitle + child: subtitle, ) ] ); } children.add(new Expanded( - child: center + child: center, )); if (trailing != null) { children.add(new Container( margin: const EdgeInsets.only(left: 16.0), alignment: FractionalOffset.centerRight, - child: trailing + child: trailing, )); } @@ -254,9 +395,7 @@ class ListTile extends StatelessWidget { child: new Container( height: tileHeight, padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: new Row( - children: children - ) + child: new Row(children: children), ) ); } diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart index e3c3588de5..9a54a77210 100644 --- a/packages/flutter/test/material/about_test.dart +++ b/packages/flutter/test/material/about_test.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('AboutDrawerItem control test', (WidgetTester tester) async { + testWidgets('AboutListTile control test', (WidgetTester tester) async { await tester.pumpWidget( new MaterialApp( title: 'Pirate app', @@ -20,7 +20,7 @@ void main() { drawer: new Drawer( child: new ListView( children: [ - new AboutDrawerItem( + new AboutListTile( applicationVersion: '0.1.2', applicationIcon: const FlutterLogo(), applicationLegalese: 'I am the very model of a modern major general.', @@ -67,12 +67,12 @@ void main() { testWidgets('About box logic defaults to executable name for app name', (WidgetTester tester) async { await tester.pumpWidget( - new Material(child: new AboutDrawerItem()), + new Material(child: new AboutListTile()), ); expect(find.text('About sky_shell'), findsOneWidget); }); - testWidgets('AboutDrawerItem control test', (WidgetTester tester) async { + testWidgets('AboutListTile control test', (WidgetTester tester) async { final List log = []; Future licenseFuture; diff --git a/packages/flutter/test/material/drawer_test.dart b/packages/flutter/test/material/drawer_test.dart index 37d887b424..980bb04f33 100644 --- a/packages/flutter/test/material/drawer_test.dart +++ b/packages/flutter/test/material/drawer_test.dart @@ -20,9 +20,9 @@ void main() { child: new Text('header') ) ), - new DrawerItem( - icon: new Icon(Icons.archive), - child: new Text('Archive') + new ListTile( + leading: new Icon(Icons.archive), + title: new Text('Archive') ) ] ) diff --git a/packages/flutter/test/material/list_item_test.dart b/packages/flutter/test/material/list_item_test.dart deleted file mode 100644 index 4ed7c3b90e..0000000000 --- a/packages/flutter/test/material/list_item_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2015 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/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('ListTile control test', (WidgetTester tester) async { - await tester.pumpWidget(new MaterialApp( - home: new Material( - child: new Center( - child: new ListTile( - leading: new Icon(Icons.thumb_up), - title: new Text('Title'), - subtitle: new Text('Subtitle'), - trailing: new Icon(Icons.info), - enabled: false, - ), - ), - ), - )); - - expect(find.text('Title'), findsOneWidget); - expect(find.text('Subtitle'), findsOneWidget); - }); - - testWidgets('ListTile control test', (WidgetTester tester) async { - final List titles = [ 'first', 'second', 'third' ]; - - await tester.pumpWidget(new MaterialApp( - home: new Material( - child: new Builder( - builder: (BuildContext context) { - return new ListView( - children: ListTile.divideTiles( - context: context, - tiles: titles.map((String title) => new ListTile(title: new Text(title))), - ).toList(), - ); - }, - ), - ), - )); - - expect(find.text('first'), findsOneWidget); - expect(find.text('second'), findsOneWidget); - expect(find.text('third'), findsOneWidget); - }); -} diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart new file mode 100644 index 0000000000..cb16959da8 --- /dev/null +++ b/packages/flutter/test/material/list_tile_test.dart @@ -0,0 +1,234 @@ +// Copyright 2015 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class TestIcon extends StatefulWidget { + TestIcon({ Key key }) : super(key: key); + + @override + TestIconState createState() => new TestIconState(); +} + +class TestIconState extends State { + IconThemeData iconTheme; + + @override + Widget build(BuildContext context) { + iconTheme = IconTheme.of(context); + return new Icon(Icons.add); + } +} + +class TestText extends StatefulWidget { + TestText(this.text, { Key key }) : super(key: key); + + final String text; + + @override + TestTextState createState() => new TestTextState(); +} + +class TestTextState extends State { + TextStyle textStyle; + + @override + Widget build(BuildContext context) { + textStyle = DefaultTextStyle.of(context).style; + return new Text(config.text); + } +} + +void main() { + testWidgets('ListTile geometry', (WidgetTester tester) async { + // See https://material.io/guidelines/components/lists.html + + bool hasSubtitle; + + Widget buildFrame({ bool dense: false, bool isTwoLine: false, bool isThreeLine: false }) { + hasSubtitle = isTwoLine || isThreeLine; + return new MaterialApp( + home: new Material( + child: new Center( + child: new ListTile( + leading: new Text('leading'), + title: new Text('title'), + subtitle: hasSubtitle ? new Text('subtitle') : null, + trailing: new Text('trailing'), + dense: dense, + isThreeLine: isThreeLine, + ), + ), + ), + ); + } + + void testChildren() { + expect(find.text('leading'), findsOneWidget); + expect(find.text('title'), findsOneWidget); + if (hasSubtitle) + expect(find.text('subtitle'), findsOneWidget); + expect(find.text('trailing'), findsOneWidget); + } + + double left(String text) => tester.getTopLeft(find.text(text)).x; + double right(String text) => tester.getTopRight(find.text(text)).x; + double top(String text) => tester.getTopLeft(find.text(text)).y; + double bottom(String text) => tester.getBottomLeft(find.text(text)).y; + + // 16.0 padding to the left and right of the leading and trailing widgets + void testHorizontalGeometry() { + expect(left('leading'), 16.0); + expect(left('title'), 72.0); + if (hasSubtitle) + expect(left('subtitle'), 72.0); + expect(left('title'), right('leading') + 16.0); + expect(right('trailing'), 800.0 - 16.0); + } + + void testVerticalGeometry(double expectedHeight) { + expect(tester.getSize(find.byType(ListTile)), new Size(800.0, expectedHeight)); + if (hasSubtitle) + expect(top('subtitle'), bottom('title')); + } + + await tester.pumpWidget(buildFrame()); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(56.0); + + await tester.pumpWidget(buildFrame(dense: true)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(48.0); + + await tester.pumpWidget(buildFrame(isTwoLine: true)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(72.0); + + await tester.pumpWidget(buildFrame(isTwoLine: true, dense: true)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(60.0); + + await tester.pumpWidget(buildFrame(isThreeLine: true)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(88.0); + + await tester.pumpWidget(buildFrame(isThreeLine: true, dense: true)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(76.0); + }); + + testWidgets('ListTile.divideTiles', (WidgetTester tester) async { + final List titles = [ 'first', 'second', 'third' ]; + + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new Builder( + builder: (BuildContext context) { + return new ListView( + children: ListTile.divideTiles( + context: context, + tiles: titles.map((String title) => new ListTile(title: new Text(title))), + ).toList(), + ); + }, + ), + ), + )); + + expect(find.text('first'), findsOneWidget); + expect(find.text('second'), findsOneWidget); + expect(find.text('third'), findsOneWidget); + }); + + testWidgets('ListTileTheme', (WidgetTester tester) async { + final Key titleKey = new UniqueKey(); + final Key subtitleKey = new UniqueKey(); + ThemeData theme; + + Widget buildFrame({ + bool enabled: true, + bool dense: false, + bool selected: false, + Color selectedColor, + Color iconColor, + Color textColor, + }) { + return new MaterialApp( + home: new Material( + child: new Center( + child: new ListTileTheme( + dense: dense, + selectedColor: selectedColor, + iconColor: iconColor, + textColor: textColor, + child: new Builder( + builder: (BuildContext context) { + theme = Theme.of(context); + return new ListTile( + enabled: enabled, + selected: selected, + leading: new TestIcon(), + title: new TestText('title', key: titleKey), + subtitle: new TestText('subtitle', key: subtitleKey), + ); + } + ), + ), + ), + ), + ); + } + + const Color green = const Color(0xFF00FF00); + const Color red = const Color(0xFFFF0000); + + Color iconColor() => tester.state(find.byType(TestIcon)).iconTheme.color; + Color textColor(Key key) => tester.state(find.byKey(key)).textStyle.color; + + // A selected ListTile's leading and text get the primary color by default + await(tester.pumpWidget(buildFrame(selected: true))); + await(tester.pump(const Duration(milliseconds: 300))); // DefaultTextStyle changes animate + expect(iconColor(), theme.primaryColor); + expect(textColor(titleKey), theme.primaryColor); + expect(textColor(subtitleKey), theme.primaryColor); + + // A selected ListTile's leading and text get the ListTileTheme's selectedColor + await(tester.pumpWidget(buildFrame(selected: true, selectedColor: green))); + await(tester.pump(const Duration(milliseconds: 300))); // DefaultTextStyle changes animate + expect(iconColor(), green); + expect(textColor(titleKey), green); + expect(textColor(subtitleKey), green); + + // An unselected ListTile's leading icon gets the ListTileTheme's iconColor + // An unselected ListTile's title texts get the ListTileTheme's textColor + await(tester.pumpWidget(buildFrame(iconColor: red, textColor: green))); + await(tester.pump(const Duration(milliseconds: 300))); // DefaultTextStyle changes animate + expect(iconColor(), red); + expect(textColor(titleKey), green); + expect(textColor(subtitleKey), green); + + // If the item is disabled it's rendered with the theme's disabled color. + await(tester.pumpWidget(buildFrame(enabled: false))); + await(tester.pump(const Duration(milliseconds: 300))); // DefaultTextStyle changes animate + expect(iconColor(), theme.disabledColor); + expect(textColor(titleKey), theme.disabledColor); + expect(textColor(subtitleKey), theme.disabledColor); + + // If the item is disabled it's rendered with the theme's disabled color. + // Even if it's selected. + await(tester.pumpWidget(buildFrame(enabled: false, selected: true))); + await(tester.pump(const Duration(milliseconds: 300))); // DefaultTextStyle changes animate + expect(iconColor(), theme.disabledColor); + expect(textColor(titleKey), theme.disabledColor); + expect(textColor(subtitleKey), theme.disabledColor); + }); + +}