From 405ebafe04f02fa0ec3e106e9081a61e6e7363bf Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Tue, 20 Dec 2022 10:49:23 -0800 Subject: [PATCH] Update Material 3 Demo App (#1530) --- .../material_3_demo/lib/component_screen.dart | 1490 +++++++++++++---- .../material_3_demo/lib/elevation_screen.dart | 64 +- experimental/material_3_demo/lib/main.dart | 640 +++++-- .../macos/Runner.xcodeproj/project.pbxproj | 6 +- .../test/color_screen_test.dart | 29 +- .../test/component_screen_test.dart | 172 +- .../test/elevation_screen_test.dart | 39 +- .../test/typography_screen_test.dart | 25 +- 8 files changed, 1885 insertions(+), 580 deletions(-) diff --git a/experimental/material_3_demo/lib/component_screen.dart b/experimental/material_3_demo/lib/component_screen.dart index b14f5bcbe..9dfb1e476 100644 --- a/experimental/material_3_demo/lib/component_screen.dart +++ b/experimental/material_3_demo/lib/component_screen.dart @@ -4,67 +4,50 @@ import 'package:flutter/material.dart'; -class ComponentScreen extends StatelessWidget { - const ComponentScreen({super.key, required this.showNavBottomBar}); +class FirstComponentList extends StatelessWidget { + const FirstComponentList({ + super.key, + required this.showNavBottomBar, + required this.scaffoldKey, + required this.showSecondList, + }); final bool showNavBottomBar; + final GlobalKey scaffoldKey; + final bool showSecondList; @override Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Align( - alignment: Alignment.topCenter, - child: SizedBox( - width: maxWidthConstraint, - child: ListView( - shrinkWrap: true, - children: [ - colDivider, - colDivider, - const Buttons(), - colDivider, - colDivider, - const IconToggleButtons(), - colDivider, - const FloatingActionButtons(), - colDivider, - const Chips(), - colDivider, - const Cards(), - colDivider, - const TextFields(), - colDivider, - const Dialogs(), - colDivider, - const Switches(), - colDivider, - const Checkboxes(), - colDivider, - const Radios(), - colDivider, - const ProgressIndicators(), - colDivider, - showNavBottomBar - ? const NavigationBars( - selectedIndex: 0, - isExampleBar: true, - ) - : Container(), - ], - ), - ), - ), - ), + return ListView( + children: [ + const Actions(), + const Communication(), + const Containment(), + Navigation(scaffoldKey: scaffoldKey), + if (!showSecondList) ...[const Selection(), const TextInputs()], + ], ); } } -const rowDivider = SizedBox(width: 10); +class SecondComponentList extends StatelessWidget { + const SecondComponentList({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + children: const [ + Selection(), + TextInputs(), + ], + ); + } +} + +const rowDivider = SizedBox(width: 20); const colDivider = SizedBox(height: 10); const double cardWidth = 115; -const double maxWidthConstraint = 400; +const double widthConstraint = 450; void Function()? handlePressed( BuildContext context, bool isDisabled, String buttonName) { @@ -87,6 +70,95 @@ void Function()? handlePressed( }; } +class Actions extends StatelessWidget { + const Actions({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Actions', children: [ + Buttons(), + FloatingActionButtons(), + IconToggleButtons(), + SegmentedButtons(), + ]); + } +} + +class Communication extends StatelessWidget { + const Communication({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Communication', children: [ + NavigationBars( + selectedIndex: 1, + isExampleBar: true, + isBadgeExample: true, + ), + ProgressIndicators(), + SnackBarSection(), + ]); + } +} + +class Containment extends StatelessWidget { + const Containment({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Containment', children: [ + BottomSheetSection(), + Cards(), + Dialogs(), + ]); + } +} + +class Navigation extends StatelessWidget { + const Navigation({super.key, required this.scaffoldKey}); + + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + return ComponentGroupDecoration(label: 'Navigation', children: [ + const BottomAppBars(), + const NavigationBars( + selectedIndex: 0, + isExampleBar: true, + ), + NavigationDrawers(scaffoldKey: scaffoldKey), + const Tabs(), + ]); + } +} + +class Selection extends StatelessWidget { + const Selection({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Selection', children: [ + Chips(), + DropdownMenus(), + Radios(), + Checkboxes(), + Sliders(), + Switches(), + ]); + } +} + +class TextInputs extends StatelessWidget { + const TextInputs({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration( + label: 'Text inputs', children: [TextFields()]); + } +} + class Buttons extends StatefulWidget { const Buttons({super.key}); @@ -97,15 +169,18 @@ class Buttons extends StatefulWidget { class _ButtonsState extends State { @override Widget build(BuildContext context) { - return Wrap( - alignment: WrapAlignment.spaceEvenly, - children: const [ - ButtonsWithoutIcon(isDisabled: false), - rowDivider, - ButtonsWithIcon(), - rowDivider, - ButtonsWithoutIcon(isDisabled: true), - ], + return ComponentDecoration( + label: 'Common Buttons', + tooltipMessage: + 'Common buttons include: \nElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton and TextButton', + child: Wrap( + alignment: WrapAlignment.spaceAround, + children: const [ + ButtonsWithoutIcon(isDisabled: false), + ButtonsWithIcon(), + ButtonsWithoutIcon(isDisabled: true), + ], + ), ); } } @@ -202,8 +277,10 @@ class FloatingActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), + return ComponentDecoration( + label: 'Floating action buttons', + tooltipMessage: + 'Floating action buttons include: \nFloatingActionButton.small, FloatingActionButton, FloatingActionButton.extended, and FloatingActionButton.large', child: Wrap( alignment: WrapAlignment.spaceEvenly, crossAxisAlignment: WrapCrossAlignment.center, @@ -239,91 +316,89 @@ class Cards extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), + return ComponentDecoration( + label: 'Cards', + tooltipMessage: 'Card has 3 types: elevated, filled and outlined', child: Wrap( alignment: WrapAlignment.spaceEvenly, children: [ SizedBox( width: cardWidth, - child: Tooltip( - margin: const EdgeInsets.only(top: 20), - message: 'Elevated Card', - child: Card( - child: Container( - padding: const EdgeInsets.all(10), - child: Column( - children: const [ - Align( - alignment: Alignment.topRight, - child: Icon(Icons.more_vert), + child: Card( + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.more_vert), + onPressed: handlePressed(context, false, 'IconButton'), ), - SizedBox(height: 35), - Align( - alignment: Alignment.bottomLeft, - child: Text('Elevated'), - ) - ], - ), + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.bottomLeft, + child: Text('Elevated'), + ) + ], ), ), ), ), SizedBox( width: cardWidth, - child: Tooltip( - margin: const EdgeInsets.only(top: 20), - message: 'Filled Card', - child: Card( - color: Theme.of(context).colorScheme.surfaceVariant, - elevation: 0, - child: Container( - padding: const EdgeInsets.all(10), - child: Column( - children: const [ - Align( - alignment: Alignment.topRight, - child: Icon(Icons.more_vert), + child: Card( + color: Theme.of(context).colorScheme.surfaceVariant, + elevation: 0, + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.more_vert), + onPressed: handlePressed(context, false, 'IconButton'), ), - SizedBox(height: 35), - Align( - alignment: Alignment.bottomLeft, - child: Text('Filled'), - ) - ], - ), + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.bottomLeft, + child: Text('Filled'), + ) + ], ), ), ), ), SizedBox( width: cardWidth, - child: Tooltip( - margin: const EdgeInsets.only(top: 20), - message: 'Outlined Card', - child: Card( - elevation: 0, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.outline, ), - child: Container( - padding: const EdgeInsets.all(10), - child: Column( - children: const [ - Align( - alignment: Alignment.topRight, - child: Icon(Icons.more_vert), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.more_vert), + onPressed: handlePressed(context, false, 'IconButton'), ), - SizedBox(height: 35), - Align( - alignment: Alignment.bottomLeft, - child: Text('Outlined'), - ) - ], - ), + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.bottomLeft, + child: Text('Outlined'), + ) + ], ), ), ), @@ -339,93 +414,50 @@ class TextFields extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.all(10), - child: TextField( - decoration: InputDecoration( - prefixIcon: Icon(Icons.search), - suffixIcon: Icon(Icons.clear), - labelText: 'Filled', - hintText: 'hint text', - helperText: 'supporting text', - filled: true, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - SizedBox( - width: 170, - child: TextField( - decoration: InputDecoration( - prefixIcon: Icon(Icons.search), - suffixIcon: Icon(Icons.clear), - labelText: 'Filled', - hintText: 'hint text', - helperText: 'supporting text', - filled: true, - errorText: 'error text', - ), - ), - ), - SizedBox( - width: 170, - child: TextField( - enabled: false, - decoration: InputDecoration( - prefixIcon: Icon(Icons.search), - suffixIcon: Icon(Icons.clear), - labelText: 'Disabled', - hintText: 'hint text', - helperText: 'supporting text', - filled: true, - ), - ), + return ComponentDecoration( + label: 'Text Fields', + tooltipMessage: + 'Use TextField with different decoration to show text fields', + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: TextField( + decoration: InputDecoration( + prefixIcon: Icon(Icons.search), + suffixIcon: Icon(Icons.clear), + labelText: 'Filled', + hintText: 'hint text', + helperText: 'supporting text', + filled: true, ), - ], - ), - ), - const Padding( - padding: EdgeInsets.all(10), - child: TextField( - decoration: InputDecoration( - prefixIcon: Icon(Icons.search), - suffixIcon: Icon(Icons.clear), - labelText: 'Outlined', - hintText: 'hint text', - helperText: 'supporting text', - border: OutlineInputBorder(), ), ), - ), - Padding( + Padding( padding: const EdgeInsets.all(10), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - SizedBox( - width: 170, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Flexible( + child: SizedBox( + width: 180, child: TextField( decoration: InputDecoration( prefixIcon: Icon(Icons.search), suffixIcon: Icon(Icons.clear), - labelText: 'Outlined', + labelText: 'Filled', hintText: 'hint text', helperText: 'supporting text', - errorText: 'error text', - border: OutlineInputBorder(), filled: true, + errorText: 'error text', ), ), ), - SizedBox( - width: 170, + ), + Flexible( + child: SizedBox( + width: 180, child: TextField( enabled: false, decoration: InputDecoration( @@ -434,13 +466,69 @@ class TextFields extends StatelessWidget { labelText: 'Disabled', hintText: 'hint text', helperText: 'supporting text', - border: OutlineInputBorder(), filled: true, ), ), ), - ])), - ], + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10), + child: TextField( + decoration: InputDecoration( + prefixIcon: Icon(Icons.search), + suffixIcon: Icon(Icons.clear), + labelText: 'Outlined', + hintText: 'hint text', + helperText: 'supporting text', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Flexible( + child: SizedBox( + width: 180, + child: TextField( + decoration: InputDecoration( + prefixIcon: Icon(Icons.search), + suffixIcon: Icon(Icons.clear), + labelText: 'Outlined', + hintText: 'hint text', + helperText: 'supporting text', + errorText: 'error text', + border: OutlineInputBorder(), + filled: true, + ), + ), + ), + ), + Flexible( + child: SizedBox( + width: 180, + child: TextField( + enabled: false, + decoration: InputDecoration( + prefixIcon: Icon(Icons.search), + suffixIcon: Icon(Icons.clear), + labelText: 'Disabled', + hintText: 'hint text', + helperText: 'supporting text', + border: OutlineInputBorder(), + filled: true, + ), + ), + ), + ), + ])), + ], + ), ); } } @@ -476,14 +564,19 @@ class _DialogsState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: TextButton( - child: const Text( - 'Open Dialog', - style: TextStyle(fontWeight: FontWeight.bold), + return Center( + child: ComponentDecoration( + label: 'Dialog', + tooltipMessage: 'Use AlertDialog to show dialogs', + child: UnconstrainedBox( + child: TextButton( + child: const Text( + 'Open Dialog', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () => openDialog(context), + ), ), - onPressed: () => openDialog(context), ), ); } @@ -494,11 +587,15 @@ class Switches extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: const [ - SwitchRow(isEnabled: true), - SwitchRow(isEnabled: false), - ], + return ComponentDecoration( + label: 'Switches', + tooltipMessage: 'Use Switch to show switches', + child: Column( + children: const [ + SwitchRow(isEnabled: true), + SwitchRow(isEnabled: false), + ], + ), ); } } @@ -560,15 +657,19 @@ class Checkboxes extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: const [ - CheckboxRow( - isError: false, - ), - CheckboxRow( - isError: true, - ) - ], + return ComponentDecoration( + label: 'Checkboxes', + tooltipMessage: 'Use Checkbox to show checkboxes', + child: Column( + children: const [ + CheckboxRow( + isError: false, + ), + CheckboxRow( + isError: true, + ) + ], + ), ); } } @@ -590,7 +691,7 @@ class _CheckboxRowState extends State { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Checkbox( isError: widget.isError, @@ -647,28 +748,32 @@ class _RadiosState extends State { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Radio( - value: Value.first, - groupValue: _value, - onChanged: (value) { - setState(() { - _value = value; - }); - }, - ), - Radio( - value: Value.second, - groupValue: _value, - onChanged: (value) { - setState(() { - _value = value; - }); - }, - ), - ], + return ComponentDecoration( + label: 'Radio button', + tooltipMessage: 'Use Radio to show radio buttons', + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Radio( + value: Value.first, + groupValue: _value, + onChanged: (value) { + setState(() { + _value = value; + }); + }, + ), + Radio( + value: Value.second, + groupValue: _value, + onChanged: (value) { + setState(() { + _value = value; + }); + }, + ), + ], + ), ); } } @@ -687,40 +792,45 @@ class _ProgressIndicatorsState extends State { Widget build(BuildContext context) { final double? progressValue = playProgressIndicator ? null : 0.7; - return Column( - children: [ - Row( - children: [ - IconButton( - isSelected: playProgressIndicator, - selectedIcon: const Icon(Icons.pause), - icon: const Icon(Icons.play_arrow), - onPressed: () { - setState(() { - playProgressIndicator = !playProgressIndicator; - }); - }, - ), - Expanded( - child: Row( - children: [ - CircularProgressIndicator( - value: progressValue, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: LinearProgressIndicator( + return ComponentDecoration( + label: 'Progress indicators', + tooltipMessage: + 'There are 2 types of progress indicators: \nCircularProgressIndicator and LinearProgressIndicator', + child: Column( + children: [ + Row( + children: [ + IconButton( + isSelected: playProgressIndicator, + selectedIcon: const Icon(Icons.pause), + icon: const Icon(Icons.play_arrow), + onPressed: () { + setState(() { + playProgressIndicator = !playProgressIndicator; + }); + }, + ), + Expanded( + child: Row( + children: [ + CircularProgressIndicator( value: progressValue, ), - ) - ], + const SizedBox( + width: 10, + ), + Expanded( + child: LinearProgressIndicator( + value: progressValue, + ), + ) + ], + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ); } } @@ -753,22 +863,6 @@ const List appBarDestinations = [ ) ]; -final List navRailDestinations = appBarDestinations - .map( - (destination) => NavigationRailDestination( - icon: Tooltip( - message: destination.label, - child: destination.icon, - ), - selectedIcon: Tooltip( - message: destination.label, - child: destination.selectedIcon, - ), - label: Text(destination.label), - ), - ) - .toList(); - const List exampleBarDestinations = [ NavigationDestination( tooltip: '', @@ -790,24 +884,53 @@ const List exampleBarDestinations = [ ) ]; -class NavigationBars extends StatefulWidget { - final void Function(int)? onSelectItem; - final int selectedIndex; - final bool isExampleBar; +List barWithBadgeDestinations = [ + NavigationDestination( + tooltip: '', + icon: Badge.count(count: 1000, child: const Icon(Icons.mail_outlined)), + label: 'Mail', + selectedIcon: Badge.count(count: 1000, child: const Icon(Icons.mail)), + ), + const NavigationDestination( + tooltip: '', + icon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble_outline)), + label: 'Chat', + selectedIcon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble)), + ), + const NavigationDestination( + tooltip: '', + icon: Badge(child: Icon(Icons.group_outlined)), + label: 'Rooms', + selectedIcon: Badge(child: Icon(Icons.group_rounded)), + ), + NavigationDestination( + tooltip: '', + icon: Badge.count(count: 3, child: const Icon(Icons.videocam_outlined)), + label: 'Meet', + selectedIcon: Badge.count(count: 3, child: const Icon(Icons.videocam)), + ) +]; +class NavigationBars extends StatefulWidget { const NavigationBars({ super.key, this.onSelectItem, required this.selectedIndex, required this.isExampleBar, + this.isBadgeExample, }); + final void Function(int)? onSelectItem; + final int selectedIndex; + final bool isExampleBar; + final bool? isBadgeExample; + @override State createState() => _NavigationBarsState(); } class _NavigationBarsState extends State { - int selectedIndex = 0; + late int selectedIndex; @override void initState() { @@ -816,55 +939,44 @@ class _NavigationBarsState extends State { } @override - Widget build(BuildContext context) { - return NavigationBar( - selectedIndex: selectedIndex, - onDestinationSelected: (index) { - setState(() { - selectedIndex = index; - }); - if (!widget.isExampleBar) widget.onSelectItem!(index); - }, - destinations: - widget.isExampleBar ? exampleBarDestinations : appBarDestinations, - ); - } -} - -class NavigationRailSection extends StatefulWidget { - final void Function(int) onSelectItem; - final int selectedIndex; - - const NavigationRailSection( - {super.key, required this.onSelectItem, required this.selectedIndex}); - - @override - State createState() => _NavigationRailSectionState(); -} - -class _NavigationRailSectionState extends State { - int selectedIndex = 0; - - @override - void initState() { - super.initState(); - selectedIndex = widget.selectedIndex; + void didUpdateWidget(covariant NavigationBars oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedIndex != oldWidget.selectedIndex) { + selectedIndex = widget.selectedIndex; + } } @override Widget build(BuildContext context) { - return NavigationRail( - minWidth: 50, - destinations: navRailDestinations, + bool isBadgeExample = widget.isBadgeExample ?? false; + Widget navigationBar = NavigationBar( selectedIndex: selectedIndex, - useIndicator: true, onDestinationSelected: (index) { setState(() { selectedIndex = index; }); - widget.onSelectItem(index); + if (!widget.isExampleBar) widget.onSelectItem!(index); }, + destinations: widget.isExampleBar && isBadgeExample + ? barWithBadgeDestinations + : widget.isExampleBar + ? exampleBarDestinations + : appBarDestinations, ); + + if (widget.isExampleBar && isBadgeExample) { + navigationBar = ComponentDecoration( + label: 'Badges', + tooltipMessage: 'Use Badge or Badge.count to show badges', + child: navigationBar); + } else if (widget.isExampleBar) { + navigationBar = ComponentDecoration( + label: 'Navigation bar', + tooltipMessage: 'Use NavigationBar to show navigation bars', + child: navigationBar); + } + + return navigationBar; } } @@ -878,13 +990,14 @@ class IconToggleButtons extends StatefulWidget { class _IconToggleButtonsState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(10.0), + return ComponentDecoration( + label: 'Icon buttons', + tooltipMessage: + 'IconButton has 4 types: standard, filled, filled tonal, and outlined', child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( - mainAxisAlignment: MainAxisAlignment.center, // Standard IconButton children: const [ IconToggleButton(isEnabled: true), @@ -893,7 +1006,6 @@ class _IconToggleButtonsState extends State { ], ), Column( - mainAxisAlignment: MainAxisAlignment.center, children: const [ // Filled IconButton IconToggleButton( @@ -908,7 +1020,6 @@ class _IconToggleButtonsState extends State { ], ), Column( - mainAxisAlignment: MainAxisAlignment.center, children: const [ // Filled Tonal IconButton IconToggleButton( @@ -923,7 +1034,6 @@ class _IconToggleButtonsState extends State { ], ), Column( - mainAxisAlignment: MainAxisAlignment.center, children: const [ // Outlined IconButton IconToggleButton( @@ -1073,8 +1183,10 @@ class Chips extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(10.0), + return ComponentDecoration( + label: 'Chips', + tooltipMessage: + 'Use ActionChip, FilterChip, and InputChip to show chips. \nActionChip can also be used for suggestion chip', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -1171,3 +1283,695 @@ class Chips extends StatelessWidget { ); } } + +class SegmentedButtons extends StatelessWidget { + const SegmentedButtons({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Segmented buttons', + tooltipMessage: + 'SegmentedButton has 2 types: single choice and multiple choice', + child: Column( + children: const [ + SingleChoice(), + colDivider, + MultipleChoice(), + ], + ), + ); + } +} + +enum Calendar { day, week, month, year } + +class SingleChoice extends StatefulWidget { + const SingleChoice({super.key}); + + @override + State createState() => _SingleChoiceState(); +} + +class _SingleChoiceState extends State { + Calendar calendarView = Calendar.day; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: const >[ + ButtonSegment( + value: Calendar.day, + label: Text('Day'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Calendar.week, + label: Text('Week'), + icon: Icon(Icons.calendar_view_week)), + ButtonSegment( + value: Calendar.month, + label: Text('Month'), + icon: Icon(Icons.calendar_view_month)), + ButtonSegment( + value: Calendar.year, + label: Text('Year'), + icon: Icon(Icons.calendar_today)), + ], + selected: {calendarView}, + onSelectionChanged: (newSelection) { + setState(() { + // By default there is only a single segment that can be + // selected at one time, so its value is always the first + // item in the selected set. + calendarView = newSelection.first; + }); + }, + ); + } +} + +enum Sizes { extraSmall, small, medium, large, extraLarge } + +class MultipleChoice extends StatefulWidget { + const MultipleChoice({super.key}); + + @override + State createState() => _MultipleChoiceState(); +} + +class _MultipleChoiceState extends State { + Set selection = {Sizes.large, Sizes.extraLarge}; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: const >[ + ButtonSegment(value: Sizes.extraSmall, label: Text('XS')), + ButtonSegment(value: Sizes.small, label: Text('S')), + ButtonSegment(value: Sizes.medium, label: Text('M')), + ButtonSegment( + value: Sizes.large, + label: Text('L'), + ), + ButtonSegment(value: Sizes.extraLarge, label: Text('XL')), + ], + selected: selection, + onSelectionChanged: (newSelection) { + setState(() { + selection = newSelection; + }); + }, + multiSelectionEnabled: true, + ); + } +} + +class SnackBarSection extends StatelessWidget { + const SnackBarSection({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Snackbar', + tooltipMessage: 'Use SnackBar to show snack bars', + child: TextButton( + onPressed: handlePressed(context, false, 'A TextButton'), + child: const Text( + 'Show Snackbar', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ); + } +} + +class BottomSheetSection extends StatelessWidget { + const BottomSheetSection({super.key}); + + @override + Widget build(BuildContext context) { + List buttonList = [ + IconButton(onPressed: () {}, icon: const Icon(Icons.share_outlined)), + IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + IconButton(onPressed: () {}, icon: const Icon(Icons.delete_outline)), + IconButton(onPressed: () {}, icon: const Icon(Icons.archive_outlined)), + IconButton(onPressed: () {}, icon: const Icon(Icons.settings_outlined)), + IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)), + ]; + List labelList = const [ + Text('Share'), + Text('Add to'), + Text('Trash'), + Text('Move to\n archive'), + Text('Settings'), + Text('Favorite') + ]; + + buttonList = List.generate( + buttonList.length, + (index) => Padding( + padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + buttonList[index], + labelList[index], + ], + ), + )); + + return ComponentDecoration( + label: 'Bottom sheet', + tooltipMessage: + 'Use showModalBottomSheet to show a modal bottom sheet', + child: TextButton( + child: const Text( + 'Show Modal bottom sheet', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) { + return SizedBox( + height: 250, + child: Column( + children: [ + Expanded( + flex: 3, + child: ListView( + scrollDirection: Axis.horizontal, + children: buttonList, + ), + ), + const Divider( + indent: 20, + endIndent: 20, + thickness: 2, + ), + Expanded( + flex: 2, + child: Center( + child: FilledButton.tonal( + child: const Text('Close BottomSheet'), + onPressed: () => Navigator.pop(context), + ), + )) + ], + ), + ); + }, + ); + }, + ), + ); + } +} + +class BottomAppBars extends StatelessWidget { + const BottomAppBars({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Bottom app bar', + tooltipMessage: 'Use BottomAppBar to show bottom app bars below', + child: Column( + children: [ + SizedBox( + height: 80, + child: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () {}, + elevation: 0.0, + child: const Icon(Icons.add), + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.endContained, + bottomNavigationBar: BottomAppBar( + elevation: 0.0, + child: Row( + children: [ + IconButton( + tooltip: 'Open popup menu', + icon: const Icon(Icons.more_vert), + onPressed: () { + final SnackBar snackBar = SnackBar( + content: const Text('Yay! A SnackBar!'), + action: SnackBarAction( + label: 'Undo', + onPressed: () {}, + ), + ); + + // Find the ScaffoldMessenger in the widget tree + // and use it to show a SnackBar. + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, + ), + IconButton( + tooltip: 'Search', + icon: const Icon(Icons.search), + onPressed: () {}, + ), + IconButton( + tooltip: 'Favorite', + icon: const Icon(Icons.favorite), + onPressed: () {}, + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 20.0, + ), + SizedBox( + height: 80, + child: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () {}, + elevation: 0.0, + child: const Icon(Icons.add), + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.endContained, + bottomNavigationBar: BottomAppBar( + child: Row( + children: [ + IconButton( + tooltip: 'Open popup menu', + icon: const Icon(Icons.more_vert), + onPressed: () { + final SnackBar snackBar = SnackBar( + content: const Text('Yay! A SnackBar!'), + action: SnackBarAction( + label: 'Undo', + onPressed: () {}, + ), + ); + + // Find the ScaffoldMessenger in the widget tree + // and use it to show a SnackBar. + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, + ), + IconButton( + tooltip: 'Search', + icon: const Icon(Icons.search), + onPressed: () {}, + ), + IconButton( + tooltip: 'Favorite', + icon: const Icon(Icons.favorite), + onPressed: () {}, + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class NavigationDrawers extends StatelessWidget { + const NavigationDrawers({super.key, required this.scaffoldKey}); + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Navigation drawer', + tooltipMessage: + 'Use NavigationDrawer to show navigation drawer or end drawer', + child: UnconstrainedBox( + child: TextButton( + child: const Text('Open End Drawer', + style: TextStyle(fontWeight: FontWeight.bold)), + onPressed: () { + scaffoldKey.currentState!.openEndDrawer(); + }, + ), + ), + ); + } +} + +class NavigationDrawerSection extends StatefulWidget { + const NavigationDrawerSection({super.key}); + + @override + State createState() => + _NavigationDrawerSectionState(); +} + +class _NavigationDrawerSectionState extends State { + int navDrawerIndex = 0; + + @override + Widget build(BuildContext context) { + return NavigationDrawer( + onDestinationSelected: (selectedIndex) { + setState(() { + navDrawerIndex = selectedIndex; + }); + }, + selectedIndex: navDrawerIndex, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), + child: Text( + 'Mail', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ...destinations.map((destination) { + return NavigationDrawerDestination( + label: Text(destination.label), + icon: destination.icon, + selectedIcon: destination.selectedIcon, + ); + }), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 28), + child: Divider(), + ), + Padding( + padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), + child: Text( + 'Labels', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ...labelDestinations.map((destination) { + return NavigationDrawerDestination( + label: Text(destination.label), + icon: destination.icon, + selectedIcon: destination.selectedIcon, + ); + }), + ], + ); + } +} + +class ExampleDestination { + const ExampleDestination(this.label, this.icon, this.selectedIcon); + + final String label; + final Widget icon; + final Widget selectedIcon; +} + +const List destinations = [ + ExampleDestination('Inbox', Icon(Icons.inbox_outlined), Icon(Icons.inbox)), + ExampleDestination('Outbox', Icon(Icons.send_outlined), Icon(Icons.send)), + ExampleDestination( + 'Favorites', Icon(Icons.favorite_outline), Icon(Icons.favorite)), + ExampleDestination('Trash', Icon(Icons.delete_outline), Icon(Icons.delete)), +]; + +const List labelDestinations = [ + ExampleDestination( + 'Family', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), + ExampleDestination( + 'School', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), + ExampleDestination('Work', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), +]; + +class Tabs extends StatefulWidget { + const Tabs({super.key}); + + @override + State createState() => _TabsState(); +} + +class _TabsState extends State with TickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Tabs', + tooltipMessage: 'Use TabBar to show tabs', + child: SizedBox( + height: 80, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab( + icon: Icon(Icons.videocam_outlined), + text: 'Video', + iconMargin: EdgeInsets.only(bottom: 0.0), + ), + Tab( + icon: Icon(Icons.photo_outlined), + text: 'Photos', + iconMargin: EdgeInsets.only(bottom: 0.0), + ), + Tab( + icon: Icon(Icons.audiotrack_sharp), + text: 'Audio', + iconMargin: EdgeInsets.only(bottom: 0.0), + ), + ], + ), + ), + ), + ), + ); + } +} + +class DropdownMenus extends StatefulWidget { + const DropdownMenus({super.key}); + + @override + State createState() => _DropdownMenusState(); +} + +class _DropdownMenusState extends State { + final TextEditingController colorController = TextEditingController(); + final TextEditingController iconController = TextEditingController(); + IconLabel? selectedIcon = IconLabel.smile; + ColorLabel? selectedColor; + + @override + Widget build(BuildContext context) { + final List> colorEntries = + >[]; + for (final ColorLabel color in ColorLabel.values) { + colorEntries.add(DropdownMenuEntry( + value: color, label: color.label, enabled: color.label != 'Grey')); + } + + final List> iconEntries = + >[]; + for (final IconLabel icon in IconLabel.values) { + iconEntries + .add(DropdownMenuEntry(value: icon, label: icon.label)); + } + + return ComponentDecoration( + label: 'Dropdown menus', + tooltipMessage: 'Use DropdownMenu to show dropdown menus', + child: Wrap( + alignment: WrapAlignment.spaceAround, + runAlignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: 20, + children: [ + DropdownMenu( + controller: colorController, + label: const Text('Color'), + enableFilter: true, + dropdownMenuEntries: colorEntries, + inputDecorationTheme: const InputDecorationTheme(filled: true), + onSelected: (color) { + setState(() { + selectedColor = color; + }); + }, + ), + DropdownMenu( + initialSelection: IconLabel.smile, + controller: iconController, + leadingIcon: const Icon(Icons.search), + label: const Text('Icon'), + dropdownMenuEntries: iconEntries, + onSelected: (icon) { + setState(() { + selectedIcon = icon; + }); + }, + ), + Icon( + selectedIcon?.icon, + color: selectedColor?.color ?? Colors.grey.withOpacity(0.5), + ) + ], + ), + ); + } +} + +enum ColorLabel { + blue('Blue', Colors.blue), + pink('Pink', Colors.pink), + green('Green', Colors.green), + yellow('Yellow', Colors.yellow), + grey('Grey', Colors.grey); + + const ColorLabel(this.label, this.color); + final String label; + final Color color; +} + +enum IconLabel { + smile('Smile', Icons.sentiment_satisfied_outlined), + cloud( + 'Cloud', + Icons.cloud_outlined, + ), + brush('Brush', Icons.brush_outlined), + heart('Heart', Icons.favorite); + + const IconLabel(this.label, this.icon); + final String label; + final IconData icon; +} + +class Sliders extends StatefulWidget { + const Sliders({super.key}); + + @override + State createState() => _SlidersState(); +} + +class _SlidersState extends State { + double sliderValue0 = 30.0; + double sliderValue1 = 20.0; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Sliders', + tooltipMessage: 'Use Slider to show sliders', + child: Column( + children: [ + Slider( + max: 100, + value: sliderValue0, + onChanged: (value) { + setState(() { + sliderValue0 = value; + }); + }, + ), + Slider( + max: 100, + divisions: 5, + value: sliderValue1, + label: sliderValue1.round().toString(), + onChanged: (value) { + setState(() { + sliderValue1 = value; + }); + }, + ), + ], + )); + } +} + +class ComponentDecoration extends StatelessWidget { + const ComponentDecoration({ + super.key, + required this.label, + required this.child, + this.tooltipMessage = '', + }); + + final String label; + final Widget child; + final String? tooltipMessage; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, style: Theme.of(context).textTheme.titleSmall), + Tooltip( + message: tooltipMessage, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 5.0), + child: Icon(Icons.info_outline, size: 16)), + ), + ], + ), + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: widthConstraint), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: child, + ), + ), + ), + ], + ), + ); + } +} + +class ComponentGroupDecoration extends StatelessWidget { + const ComponentGroupDecoration( + {super.key, required this.label, required this.children}); + + final String label; + final List children; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Center( + child: Column( + children: [ + Text(label, style: Theme.of(context).textTheme.titleLarge), + ...children + ], + ), + ), + ), + ); + } +} diff --git a/experimental/material_3_demo/lib/elevation_screen.dart b/experimental/material_3_demo/lib/elevation_screen.dart index 7e235b7cc..15c5c4bd3 100644 --- a/experimental/material_3_demo/lib/elevation_screen.dart +++ b/experimental/material_3_demo/lib/elevation_screen.dart @@ -12,35 +12,45 @@ class ElevationScreen extends StatelessWidget { Color shadowColor = Theme.of(context).colorScheme.shadow; Color surfaceTint = Theme.of(context).colorScheme.primary; return Expanded( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 20, 16.0, 0), - child: Text( - 'Surface Tint only', - style: Theme.of(context).textTheme.titleLarge, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 20, 16.0, 0), + child: Text( + 'Surface Tint Color Only', + style: Theme.of(context).textTheme.titleLarge, + ), ), ), ElevationGrid(surfaceTintColor: surfaceTint), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0), - child: Text( - 'Surface Tint and Shadow', - style: Theme.of(context).textTheme.titleLarge, - ), + SliverList( + delegate: SliverChildListDelegate([ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0), + child: Text( + 'Surface Tint Color and Shadow Color', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ]), ), ElevationGrid( shadowColor: shadowColor, surfaceTintColor: surfaceTint, ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0), - child: Text( - 'Shadow only', - style: Theme.of(context).textTheme.titleLarge, - ), + SliverList( + delegate: SliverChildListDelegate([ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0), + child: Text( + 'Shadow Color Only', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ]), ), ElevationGrid(shadowColor: shadowColor), ], @@ -72,18 +82,16 @@ class ElevationGrid extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( + return SliverPadding( padding: const EdgeInsets.all(8), - child: LayoutBuilder(builder: (context, constraints) { - if (constraints.maxWidth < narrowScreenWidthThreshold) { - return GridView.count( - shrinkWrap: true, + sliver: SliverLayoutBuilder(builder: (context, constraints) { + if (constraints.crossAxisExtent < narrowScreenWidthThreshold) { + return SliverGrid.count( crossAxisCount: 3, children: elevationCards(shadowColor, surfaceTintColor), ); } else { - return GridView.count( - shrinkWrap: true, + return SliverGrid.count( crossAxisCount: 6, children: elevationCards(shadowColor, surfaceTintColor), ); diff --git a/experimental/material_3_demo/lib/main.dart b/experimental/material_3_demo/lib/main.dart index 4504642a3..750040b1a 100644 --- a/experimental/material_3_demo/lib/main.dart +++ b/experimental/material_3_demo/lib/main.dart @@ -9,7 +9,8 @@ import 'elevation_screen.dart'; import 'typography_screen.dart'; void main() { - runApp(const Material3Demo()); + runApp(const MaterialApp( + debugShowCheckedModeBanner: false, home: Material3Demo())); } class Material3Demo extends StatefulWidget { @@ -23,25 +24,23 @@ class Material3Demo extends StatefulWidget { // screenWidthThreshold; otherwise, NavigationBar is used for navigation. const double narrowScreenWidthThreshold = 450; -const Color m3BaseColor = Color(0xff6750a4); -const List colorOptions = [ - m3BaseColor, - Colors.blue, - Colors.teal, - Colors.green, - Colors.yellow, - Colors.orange, - Colors.pink -]; -const List colorText = [ - 'M3 Baseline', - 'Blue', - 'Teal', - 'Green', - 'Yellow', - 'Orange', - 'Pink', -]; +const double transitionLength = 500; + +enum ColorSeed { + baseColor('M3 Baseline', Color(0xff6750a4)), + indigo('Indigo', Colors.indigo), + blue('Blue', Colors.blue), + teal('Teal', Colors.teal), + green('Green', Colors.green), + yellow('Yellow', Colors.yellow), + orange('Orange', Colors.orange), + deepOrange('Deep Orange', Colors.deepOrange), + pink('Pink', Colors.pink); + + const ColorSeed(this.label, this.color); + final String label; + final Color color; +} enum ScreenSelected { component(0), @@ -53,10 +52,17 @@ enum ScreenSelected { final int value; } -class _Material3DemoState extends State { +class _Material3DemoState extends State + with SingleTickerProviderStateMixin { + final GlobalKey scaffoldKey = GlobalKey(); + late final AnimationController controller; + late final CurvedAnimation railAnimation; + bool controllerInitialized = false; + bool showMediumSizeLayout = false; + bool showLargeSizeLayout = false; bool useMaterial3 = true; bool useLightMode = true; - int colorSelected = 0; + ColorSeed colorSelected = ColorSeed.baseColor; int screenIndex = ScreenSelected.component.value; late ThemeData themeData; @@ -64,12 +70,61 @@ class _Material3DemoState extends State { @override initState() { super.initState(); - themeData = updateThemes(colorSelected, useMaterial3, useLightMode); + themeData = updateThemes(colorSelected.color, useMaterial3, useLightMode); + + controller = AnimationController( + duration: Duration(milliseconds: transitionLength.toInt() * 2), + value: 0, + vsync: this, + ); + railAnimation = CurvedAnimation( + parent: controller, + curve: const Interval(0.5, 1.0), + ); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); } - ThemeData updateThemes(int colorIndex, bool useMaterial3, bool useLightMode) { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final double width = MediaQuery.of(context).size.width; + final AnimationStatus status = controller.status; + if (width > 1000) { + if (width > 1500) { + showMediumSizeLayout = false; + showLargeSizeLayout = true; + } else { + showMediumSizeLayout = true; + showLargeSizeLayout = false; + } + if (status != AnimationStatus.forward && + status != AnimationStatus.completed) { + controller.forward(); + } + } else { + showMediumSizeLayout = false; + showLargeSizeLayout = false; + if (status != AnimationStatus.reverse && + status != AnimationStatus.dismissed) { + controller.reverse(); + } + } + if (!controllerInitialized) { + controllerInitialized = true; + controller.value = width > 1000 ? 1 : 0; + } + } + + ThemeData updateThemes( + Color colorSelected, bool useMaterial3, bool useLightMode) { return ThemeData( - colorSchemeSeed: colorOptions[colorSelected], + colorSchemeSeed: colorSelected, useMaterial3: useMaterial3, brightness: useLightMode ? Brightness.light : Brightness.dark); } @@ -83,21 +138,21 @@ class _Material3DemoState extends State { void handleBrightnessChange() { setState(() { useLightMode = !useLightMode; - themeData = updateThemes(colorSelected, useMaterial3, useLightMode); + themeData = updateThemes(colorSelected.color, useMaterial3, useLightMode); }); } void handleMaterialVersionChange() { setState(() { useMaterial3 = !useMaterial3; - themeData = updateThemes(colorSelected, useMaterial3, useLightMode); + themeData = updateThemes(colorSelected.color, useMaterial3, useLightMode); }); } void handleColorSelect(int value) { setState(() { - colorSelected = value; - themeData = updateThemes(colorSelected, useMaterial3, useLightMode); + colorSelected = ColorSeed.values[value]; + themeData = updateThemes(colorSelected.color, useMaterial3, useLightMode); }); } @@ -105,7 +160,15 @@ class _Material3DemoState extends State { ScreenSelected screenSelected, bool showNavBarExample) { switch (screenSelected) { case ScreenSelected.component: - return ComponentScreen(showNavBottomBar: showNavBarExample); + return Expanded( + child: OneTwoTransition( + animation: railAnimation, + one: FirstComponentList( + showNavBottomBar: showNavBarExample, + scaffoldKey: scaffoldKey, + showSecondList: + showMediumSizeLayout || showLargeSizeLayout), + two: const SecondComponentList())); case ScreenSelected.color: return const ColorPalettesScreen(); case ScreenSelected.typography: @@ -113,62 +176,140 @@ class _Material3DemoState extends State { case ScreenSelected.elevation: return const ElevationScreen(); default: - return ComponentScreen(showNavBottomBar: showNavBarExample); + return FirstComponentList( + showNavBottomBar: showNavBarExample, + scaffoldKey: scaffoldKey, + showSecondList: showMediumSizeLayout || showLargeSizeLayout); } } - PreferredSizeWidget createAppBar() { - return AppBar( - title: useMaterial3 ? const Text('Material 3') : const Text('Material 2'), - actions: [ - IconButton( + Widget brightnessButton({bool showTooltipBelow = true}) => Tooltip( + preferBelow: showTooltipBelow, + message: 'Toggle brightness', + child: IconButton( icon: useLightMode ? const Icon(Icons.wb_sunny_outlined) : const Icon(Icons.wb_sunny), onPressed: handleBrightnessChange, - tooltip: 'Toggle brightness', ), - IconButton( + ); + + Widget material3Button({bool showTooltipBelow = true}) => Tooltip( + preferBelow: showTooltipBelow, + message: 'Switch to Material ${useMaterial3 ? 2 : 3}', + child: IconButton( icon: useMaterial3 ? const Icon(Icons.filter_3) : const Icon(Icons.filter_2), onPressed: handleMaterialVersionChange, - tooltip: 'Switch to Material ${useMaterial3 ? 2 : 3}', ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - itemBuilder: (context) { - return List.generate(colorOptions.length, (index) { - return PopupMenuItem( - value: index, - child: Wrap( - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: Icon( - index == colorSelected - ? Icons.color_lens - : Icons.color_lens_outlined, - color: colorOptions[index], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 20), - child: Text(colorText[index]), + ); + + Widget colorSeedButton(Icon icon) => PopupMenuButton( + icon: icon, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + itemBuilder: (context) { + return List.generate(ColorSeed.values.length, (index) { + ColorSeed currentColor = ColorSeed.values[index]; + + return PopupMenuItem( + value: index, + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Icon( + currentColor == colorSelected + ? Icons.color_lens + : Icons.color_lens_outlined, + color: currentColor.color, ), - ], - ), - ); - }); - }, - onSelected: handleColorSelect, - ), - ], + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text(currentColor.label), + ), + ], + ), + ); + }); + }, + onSelected: handleColorSelect, + ); + + PreferredSizeWidget createAppBar() { + return AppBar( + title: useMaterial3 ? const Text('Material 3') : const Text('Material 2'), + actions: !showMediumSizeLayout && !showLargeSizeLayout + ? [ + brightnessButton(), + material3Button(), + colorSeedButton(const Icon(Icons.more_vert)), + ] + : [Container()], ); } + Widget _expandedTrailingActions() => Container( + constraints: const BoxConstraints.tightFor(width: 250), + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const Text('Brightness'), + Expanded(child: Container()), + Switch( + value: useLightMode, + onChanged: (_) { + handleBrightnessChange(); + }) + ], + ), + Row( + children: [ + useMaterial3 + ? const Text('Material 3') + : const Text('Material 2'), + Expanded(child: Container()), + Switch( + value: useMaterial3, + onChanged: (_) { + handleMaterialVersionChange(); + }) + ], + ), + const Divider(), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200.0), + child: GridView.count( + crossAxisCount: 3, + children: List.generate( + ColorSeed.values.length, + (i) => IconButton( + icon: const Icon(Icons.circle), + color: ColorSeed.values[i].color, + onPressed: () { + handleColorSelect(i); + }, + )), + ), + ), + ], + ), + ); + + Widget _trailingActions() => Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible(child: brightnessButton(showTooltipBelow: false)), + Flexible(child: material3Button(showTooltipBelow: false)), + Flexible(child: colorSeedButton(const Icon(Icons.more_horiz))), + ], + ); + @override Widget build(BuildContext context) { return MaterialApp( @@ -176,40 +317,337 @@ class _Material3DemoState extends State { title: 'Material 3', themeMode: useLightMode ? ThemeMode.light : ThemeMode.dark, theme: themeData, - home: LayoutBuilder(builder: (context, constraints) { - if (constraints.maxWidth < narrowScreenWidthThreshold) { - return Scaffold( - appBar: createAppBar(), - body: Row(children: [ - createScreenFor(ScreenSelected.values[screenIndex], false), - ]), - bottomNavigationBar: NavigationBars( - onSelectItem: handleScreenChanged, - selectedIndex: screenIndex, - isExampleBar: false, - ), - ); - } else { - return Scaffold( - appBar: createAppBar(), - body: SafeArea( - bottom: false, - top: false, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: NavigationRailSection( - onSelectItem: handleScreenChanged, - selectedIndex: screenIndex)), - const VerticalDivider(thickness: 1, width: 1), - createScreenFor(ScreenSelected.values[screenIndex], true), - ], + home: AnimatedBuilder( + animation: controller, + builder: (context, child) { + return NavigationTransition( + scaffoldKey: scaffoldKey, + animationController: controller, + railAnimation: railAnimation, + appBar: createAppBar(), + body: createScreenFor( + ScreenSelected.values[screenIndex], controller.value == 1), + navigationRail: NavigationRail( + extended: showLargeSizeLayout, + destinations: navRailDestinations, + selectedIndex: screenIndex, + onDestinationSelected: (index) { + setState(() { + screenIndex = index; + handleScreenChanged(screenIndex); + }); + }, + trailing: Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: showLargeSizeLayout + ? _expandedTrailingActions() + : _trailingActions(), + ), + ), + ), + navigationBar: NavigationBars( + onSelectItem: (index) { + setState(() { + screenIndex = index; + handleScreenChanged(screenIndex); + }); + }, + selectedIndex: screenIndex, + isExampleBar: false, ), + ); + }), + ); + } +} + +class NavigationTransition extends StatefulWidget { + const NavigationTransition( + {super.key, + required this.scaffoldKey, + required this.animationController, + required this.railAnimation, + required this.navigationRail, + required this.navigationBar, + required this.appBar, + required this.body}); + + final GlobalKey scaffoldKey; + final AnimationController animationController; + final CurvedAnimation railAnimation; + final Widget navigationRail; + final Widget navigationBar; + final PreferredSizeWidget appBar; + final Widget body; + + @override + State createState() => _NavigationTransitionState(); +} + +class _NavigationTransitionState extends State { + late final AnimationController controller; + late final CurvedAnimation railAnimation; + late final ReverseAnimation barAnimation; + bool controllerInitialized = false; + bool showDivider = false; + + @override + void initState() { + super.initState(); + + controller = widget.animationController; + railAnimation = widget.railAnimation; + + barAnimation = ReverseAnimation( + CurvedAnimation( + parent: controller, + curve: const Interval(0.0, 0.5), + ), + ); + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + key: widget.scaffoldKey, + appBar: widget.appBar, + body: Row( + children: [ + RailTransition( + animation: railAnimation, + backgroundColor: colorScheme.surface, + child: widget.navigationRail, + ), + railAnimation.isDismissed + ? const SizedBox() + : const VerticalDivider(width: 1), + widget.body, + ], + ), + bottomNavigationBar: BarTransition( + animation: barAnimation, + backgroundColor: colorScheme.surface, + child: widget.navigationBar, + ), + endDrawer: const NavigationDrawerSection(), + ); + } +} + +final List navRailDestinations = appBarDestinations + .map( + (destination) => NavigationRailDestination( + icon: Tooltip( + message: destination.label, + child: destination.icon, + ), + selectedIcon: Tooltip( + message: destination.label, + child: destination.selectedIcon, + ), + label: Text(destination.label), + ), + ) + .toList(); + +class SizeAnimation extends CurvedAnimation { + SizeAnimation(Animation parent) + : super( + parent: parent, + curve: const Interval( + 0.2, + 0.8, + curve: Curves.easeInOutCubicEmphasized, + ), + reverseCurve: Interval( + 0, + 0.2, + curve: Curves.easeInOutCubicEmphasized.flipped, + ), + ); +} + +class OffsetAnimation extends CurvedAnimation { + OffsetAnimation(Animation parent) + : super( + parent: parent, + curve: const Interval( + 0.4, + 1.0, + curve: Curves.easeInOutCubicEmphasized, + ), + reverseCurve: Interval( + 0, + 0.2, + curve: Curves.easeInOutCubicEmphasized.flipped, + ), + ); +} + +class RailTransition extends StatefulWidget { + const RailTransition( + {super.key, + required this.animation, + required this.backgroundColor, + required this.child}); + + final Animation animation; + final Widget child; + final Color backgroundColor; + + @override + State createState() => _RailTransition(); +} + +class _RailTransition extends State { + late Animation offsetAnimation; + late Animation widthAnimation; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + // The animations are only rebuilt by this method when the text + // direction changes because this widget only depends on Directionality. + final bool ltr = Directionality.of(context) == TextDirection.ltr; + + widthAnimation = Tween( + begin: 0, + end: 1, + ).animate(SizeAnimation(widget.animation)); + + offsetAnimation = Tween( + begin: ltr ? const Offset(-1, 0) : const Offset(1, 0), + end: Offset.zero, + ).animate(OffsetAnimation(widget.animation)); + } + + @override + Widget build(BuildContext context) { + return ClipRect( + child: DecoratedBox( + decoration: BoxDecoration(color: widget.backgroundColor), + child: Align( + alignment: Alignment.topLeft, + widthFactor: widthAnimation.value, + child: FractionalTranslation( + translation: offsetAnimation.value, + child: widget.child, + ), + ), + ), + ); + } +} + +class BarTransition extends StatefulWidget { + const BarTransition( + {super.key, + required this.animation, + required this.backgroundColor, + required this.child}); + + final Animation animation; + final Color backgroundColor; + final Widget child; + + @override + State createState() => _BarTransition(); +} + +class _BarTransition extends State { + late final Animation offsetAnimation; + late final Animation heightAnimation; + + @override + void initState() { + super.initState(); + + offsetAnimation = Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(OffsetAnimation(widget.animation)); + + heightAnimation = Tween( + begin: 0, + end: 1, + ).animate(SizeAnimation(widget.animation)); + } + + @override + Widget build(BuildContext context) { + return ClipRect( + child: DecoratedBox( + decoration: BoxDecoration(color: widget.backgroundColor), + child: Align( + alignment: Alignment.topLeft, + heightFactor: heightAnimation.value, + child: FractionalTranslation( + translation: offsetAnimation.value, + child: widget.child, + ), + ), + ), + ); + } +} + +class OneTwoTransition extends StatefulWidget { + const OneTwoTransition({ + super.key, + required this.animation, + required this.one, + required this.two, + }); + + final Animation animation; + final Widget one; + final Widget two; + + @override + State createState() => _OneTwoTransitionState(); +} + +class _OneTwoTransitionState extends State { + late final Animation offsetAnimation; + late final Animation widthAnimation; + + @override + void initState() { + super.initState(); + + offsetAnimation = Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(OffsetAnimation(widget.animation)); + + widthAnimation = Tween( + begin: 0, + end: 1000, + ).animate(SizeAnimation(widget.animation)); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + flex: 1000, + child: widget.one, + ), + if (widthAnimation.value.toInt() > 0) ...[ + Flexible( + flex: widthAnimation.value.toInt(), + child: FractionalTranslation( + translation: offsetAnimation.value, + child: widget.two, ), - ); - } - }), + ) + ], + ], ); } } diff --git a/experimental/material_3_demo/macos/Runner.xcodeproj/project.pbxproj b/experimental/material_3_demo/macos/Runner.xcodeproj/project.pbxproj index 761c8ce3c..34b8722c1 100644 --- a/experimental/material_3_demo/macos/Runner.xcodeproj/project.pbxproj +++ b/experimental/material_3_demo/macos/Runner.xcodeproj/project.pbxproj @@ -345,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -424,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -471,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/experimental/material_3_demo/test/color_screen_test.dart b/experimental/material_3_demo/test/color_screen_test.dart index f28d126f7..92a21673d 100644 --- a/experimental/material_3_demo/test/color_screen_test.dart +++ b/experimental/material_3_demo/test/color_screen_test.dart @@ -16,17 +16,25 @@ void main() { 'on NavigationBar', (tester) async { widgetSetup(tester, 449); addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - await tester.pumpWidget(const Material3Demo()); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); expect(find.text('Light Theme'), findsNothing); expect(find.text('Dark Theme'), findsNothing); expect(find.byType(NavigationBar), findsOneWidget); - Finder colorIconOnBar = find.byIcon(Icons.format_paint_outlined); + Finder colorIconOnBar = find.descendant( + of: find.byType(NavigationBar), + matching: find.widgetWithIcon( + NavigationDestination, Icons.format_paint_outlined)); expect(colorIconOnBar, findsOneWidget); await tester.tap(colorIconOnBar); await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(colorIconOnBar, findsNothing); - expect(find.byIcon(Icons.format_paint), findsOneWidget); + + Finder selectedColorIconOnBar = find.descendant( + of: find.byType(NavigationBar), + matching: + find.widgetWithIcon(NavigationDestination, Icons.format_paint)); + expect(selectedColorIconOnBar, findsOneWidget); expect(find.text('Light Theme'), findsOneWidget); expect(find.text('Dark Theme'), findsOneWidget); }); @@ -34,18 +42,25 @@ void main() { testWidgets( 'Color palettes screen shows correctly when color icon is clicked ' 'on NavigationRail', (tester) async { - widgetSetup(tester, 450); // NavigationRail shows only when width is >= 450. + widgetSetup( + tester, 1200); // NavigationRail shows only when width is > 1000. addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - await tester.pumpWidget(const Material3Demo()); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + await tester.pumpAndSettle(); expect(find.text('Light Theme'), findsNothing); expect(find.text('Dark Theme'), findsNothing); expect(find.byType(NavigationRail), findsOneWidget); - Finder colorIconOnRail = find.byIcon(Icons.format_paint_outlined); + Finder colorIconOnRail = find.descendant( + of: find.byType(NavigationRail), + matching: find.byIcon(Icons.format_paint_outlined)); expect(colorIconOnRail, findsOneWidget); await tester.tap(colorIconOnRail); await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(colorIconOnRail, findsNothing); - expect(find.byIcon(Icons.format_paint), findsOneWidget); + Finder selectedColorIconOnRail = find.descendant( + of: find.byType(NavigationRail), + matching: find.byIcon(Icons.format_paint)); + expect(selectedColorIconOnRail, findsOneWidget); expect(find.text('Light Theme'), findsOneWidget); expect(find.text('Dark Theme'), findsOneWidget); }); diff --git a/experimental/material_3_demo/test/component_screen_test.dart b/experimental/material_3_demo/test/component_screen_test.dart index 91508a48b..d174ec292 100644 --- a/experimental/material_3_demo/test/component_screen_test.dart +++ b/experimental/material_3_demo/test/component_screen_test.dart @@ -10,15 +10,15 @@ import 'package:material_3_demo/main.dart'; void main() { testWidgets('Default main page shows all M3 components', (tester) async { - widgetSetup(tester, 800, windowHeight: 3500); - await tester.pumpWidget(const Material3Demo()); + widgetSetup(tester, 800, windowHeight: 7000); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); // Elements on the app bar expect(find.text('Material 3'), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.wb_sunny_outlined), - findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.filter_3), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.more_vert), findsOneWidget); + expect( + find.widgetWithIcon(AppBar, Icons.wb_sunny_outlined), findsOneWidget); + expect(find.widgetWithIcon(AppBar, Icons.filter_3), findsOneWidget); + expect(find.widgetWithIcon(AppBar, Icons.more_vert), findsOneWidget); // Elements on the component screen // Buttons @@ -27,13 +27,14 @@ void main() { expect(find.widgetWithText(FilledButton, 'Filled Tonal'), findsNWidgets(2)); expect(find.widgetWithText(OutlinedButton, 'Outlined'), findsNWidgets(2)); expect(find.widgetWithText(TextButton, 'Text'), findsNWidgets(2)); - expect(find.text('Icon'), findsNWidgets(5)); + expect(find.widgetWithText(Buttons, 'Icon'), findsNWidgets(5)); // IconButtons expect(find.byType(IconToggleButton), findsNWidgets(8)); // FABs - expect(find.byType(FloatingActionButton), findsNWidgets(4)); + expect(find.byType(FloatingActionButton), + findsNWidgets(6)); // 2 more shows up in the bottom app bar. expect(find.widgetWithText(FloatingActionButton, 'Create'), findsOneWidget); // Chips @@ -43,9 +44,9 @@ void main() { expect(find.byType(InputChip), findsNWidgets(4)); // Cards - expect(find.widgetWithText(Card, 'Filled'), findsOneWidget); - expect(find.widgetWithText(Card, 'Filled'), findsOneWidget); - expect(find.widgetWithText(Card, 'Outlined'), findsOneWidget); + expect(find.widgetWithText(Cards, 'Elevated'), findsOneWidget); + expect(find.widgetWithText(Cards, 'Filled'), findsOneWidget); + expect(find.widgetWithText(Cards, 'Outlined'), findsOneWidget); // TextFields expect(find.widgetWithText(TextField, 'Disabled'), findsNWidgets(2)); @@ -76,68 +77,57 @@ void main() { }); testWidgets( - 'NavigationRail doesn\'t show when width value is small than 450 ' + 'NavigationRail doesn\'t show when width value is small than 1000 ' '(in Portrait mode or narrow screen)', (tester) async { - widgetSetup(tester, 449); - await tester.pumpWidget(const Material3Demo()); + widgetSetup(tester, 999, windowHeight: 7000); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + await tester.pumpAndSettle(); - // When screen width is less than 450, NavigationBar will show. At the same - // time, the NavigationRail and the NavigationBar example will NOT show. - expect(find.byType(NavigationBars), findsOneWidget); + // When screen width is less than 1000, NavigationBar will show. At the same + // time, the NavigationBar example still show up in the navigation group. + expect(find.byType(NavigationBars), + findsNWidgets(3)); // The real navBar, badges example and navBar example expect(find.widgetWithText(NavigationBar, 'Components'), findsOneWidget); expect(find.widgetWithText(NavigationBar, 'Color'), findsOneWidget); expect(find.widgetWithText(NavigationBar, 'Typography'), findsOneWidget); expect(find.widgetWithText(NavigationBar, 'Elevation'), findsOneWidget); - expect(find.byType(NavigationRailSection), findsNothing); - expect(find.widgetWithText(NavigationBar, 'Explore'), findsNothing); - expect(find.widgetWithText(NavigationBar, 'Pets'), findsNothing); - expect(find.widgetWithText(NavigationBar, 'Account'), findsNothing); + expect(find.widgetWithText(NavigationBar, 'Explore'), findsOneWidget); + expect(find.widgetWithText(NavigationBar, 'Pets'), findsOneWidget); + expect(find.widgetWithText(NavigationBar, 'Account'), findsOneWidget); }); testWidgets( 'NavigationRail shows when width value is greater than or equal ' - 'to 450 (in Landscape mode or wider screen)', (tester) async { - widgetSetup(tester, 450); - await tester.pumpWidget(const Material3Demo()); - - // When screen width is greater than or equal to 450, NavigationRail and - // NavigationBar example will show. At the same time, the NavigationBar - // will NOT show. - expect(find.byType(NavigationRailSection), findsOneWidget); + 'to 1000 (in Landscape mode or wider screen)', (tester) async { + widgetSetup(tester, 1001, windowHeight: 3000); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + await tester.pumpAndSettle(); + + // When screen width is greater than or equal to 1000, NavigationRail will show. + // At the same time, the NavigationBar will NOT show. + expect(find.byType(NavigationRail), findsOneWidget); expect(find.byType(Tooltip, skipOffstage: false), findsWidgets); - expect(find.widgetWithText(NavigationRailSection, 'Components'), - findsOneWidget); - expect(find.widgetWithText(NavigationRailSection, 'Color'), findsOneWidget); - expect(find.widgetWithText(NavigationRailSection, 'Typography'), - findsOneWidget); - expect(find.widgetWithText(NavigationRailSection, 'Elevation'), - findsOneWidget); - - final navbarExample = find.byType(NavigationBars); - await tester.scrollUntilVisible( - scrollable: find.byType(Scrollable).first, - navbarExample, - 500.0, - ); - expect(find.byType(NavigationBars), findsOneWidget); + expect(find.widgetWithText(NavigationRail, 'Components'), findsOneWidget); + expect(find.widgetWithText(NavigationRail, 'Color'), findsOneWidget); + expect(find.widgetWithText(NavigationRail, 'Typography'), findsOneWidget); + expect(find.widgetWithText(NavigationRail, 'Elevation'), findsOneWidget); + expect(find.widgetWithText(NavigationBar, 'Explore'), findsOneWidget); expect(find.widgetWithText(NavigationBar, 'Pets'), findsOneWidget); expect(find.widgetWithText(NavigationBar, 'Account'), findsOneWidget); - expect(find.widgetWithText(NavigationBar, 'Components'), findsNothing); - expect(find.widgetWithText(NavigationBar, 'Colors'), findsNothing); - expect(find.widgetWithText(NavigationBar, 'Typography'), findsNothing); - expect(find.widgetWithText(NavigationBar, 'Elevation'), findsNothing); + // the Navigation bar should be out of screen. + final RenderBox box = + tester.renderObject(find.widgetWithText(NavigationBar, 'Components')); + expect(box.localToGlobal(Offset.zero), const Offset(0.0, 3080.0)); }); testWidgets( 'Material version switches between Material3 and Material2 when' 'the version icon is clicked', (tester) async { - widgetSetup(tester, 450, windowHeight: 3000); - await tester.pumpWidget(const Material3Demo()); - Finder m3Icon = find.widgetWithIcon(IconButton, Icons.filter_3); - Finder m2Icon = find.widgetWithIcon(IconButton, Icons.filter_2); + widgetSetup(tester, 450, windowHeight: 7000); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); BuildContext defaultElevatedButton = tester.firstElement(find.byType(ElevatedButton)); BuildContext defaultIconButton = @@ -157,8 +147,8 @@ void main() { await tester.tap(dismiss); await tester.pumpAndSettle(const Duration(microseconds: 500)); - expect(m3Icon, findsOneWidget); - expect(m2Icon, findsNothing); + expect(find.widgetWithIcon(AppBar, Icons.filter_3), findsOneWidget); + expect(find.widgetWithIcon(AppBar, Icons.filter_2), findsNothing); expect(find.text('Material 3'), findsOneWidget); expect(Theme.of(defaultElevatedButton).useMaterial3, true); expect(Theme.of(defaultIconButton).useMaterial3, true); @@ -166,7 +156,10 @@ void main() { expect(Theme.of(defaultCard).useMaterial3, true); expect(Theme.of(defaultChip).useMaterial3, true); - await tester.tap(m3Icon); + Finder appbarM3Icon = find.descendant( + of: find.byType(AppBar), + matching: find.widgetWithIcon(IconButton, Icons.filter_3)); + await tester.tap(appbarM3Icon); await tester.pumpAndSettle(const Duration(microseconds: 500)); BuildContext updatedElevatedButton = tester.firstElement(find.byType(ElevatedButton)); @@ -187,8 +180,8 @@ void main() { await tester.tap(updatedDismiss); await tester.pumpAndSettle(const Duration(microseconds: 500)); - expect(m3Icon, findsNothing); - expect(m2Icon, findsOneWidget); + expect(find.widgetWithIcon(AppBar, Icons.filter_2), findsOneWidget); + expect(find.widgetWithIcon(AppBar, Icons.filter_3), findsNothing); expect(find.text('Material 2'), findsOneWidget); expect(Theme.of(updatedElevatedButton).useMaterial3, false); expect(Theme.of(updatedIconButton).useMaterial3, false); @@ -200,17 +193,32 @@ void main() { testWidgets( 'Other screens become Material2 mode after changing mode from ' 'main screen', (tester) async { - await tester.pumpWidget(const Material3Demo()); - await tester.tap(find.widgetWithIcon(IconButton, Icons.filter_3)); - await tester.tap(find.byIcon(Icons.format_paint_outlined)); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + Finder appbarM3Icon = find.descendant( + of: find.byType(AppBar), + matching: find.widgetWithIcon(IconButton, Icons.filter_3)); + await tester.tap(appbarM3Icon); + Finder secondScreenIcon = find.descendant( + of: find.byType(NavigationBar), + matching: find.widgetWithIcon( + NavigationDestination, Icons.format_paint_outlined)); + await tester.tap(secondScreenIcon); await tester.pumpAndSettle(const Duration(microseconds: 500)); BuildContext lightThemeText = tester.element(find.text('Light Theme')); expect(Theme.of(lightThemeText).useMaterial3, false); - await tester.tap(find.byIcon(Icons.text_snippet_outlined)); + Finder thirdScreenIcon = find.descendant( + of: find.byType(NavigationBar), + matching: find.widgetWithIcon( + NavigationDestination, Icons.text_snippet_outlined)); + await tester.tap(thirdScreenIcon); await tester.pumpAndSettle(const Duration(microseconds: 500)); BuildContext displayLargeText = tester.element(find.text('Display Large')); expect(Theme.of(displayLargeText).useMaterial3, false); - await tester.tap(find.byIcon(Icons.invert_colors_on_outlined)); + Finder fourthScreenIcon = find.descendant( + of: find.byType(NavigationBar), + matching: find.widgetWithIcon( + NavigationDestination, Icons.invert_colors_on_outlined)); + await tester.tap(fourthScreenIcon); await tester.pumpAndSettle(const Duration(microseconds: 500)); BuildContext material = tester.firstElement(find.byType(Material)); expect(Theme.of(material).useMaterial3, false); @@ -219,12 +227,17 @@ void main() { testWidgets( 'Brightness mode switches between dark and light when' 'the brightness icon is clicked', (tester) async { - await tester.pumpWidget(const Material3Demo()); - Finder lightIcon = find.widgetWithIcon(IconButton, Icons.wb_sunny_outlined); - Finder darkIcon = find.widgetWithIcon(IconButton, Icons.wb_sunny); - BuildContext appBar = tester.element(find.byType(AppBar)); - BuildContext body = tester.element(find.byType(Scaffold)); - BuildContext navigationRail = tester.element(find.byType(NavigationRail)); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + Finder lightIcon = find.descendant( + of: find.byType(AppBar), + matching: find.widgetWithIcon(IconButton, Icons.wb_sunny_outlined)); + Finder darkIcon = find.descendant( + of: find.byType(AppBar), + matching: find.widgetWithIcon(IconButton, Icons.wb_sunny)); + BuildContext appBar = tester.element(find.byType(AppBar).first); + BuildContext body = tester.firstElement(find.byType(Scaffold).first); + BuildContext navigationRail = tester.element( + find.widgetWithIcon(NavigationRail, Icons.format_paint_outlined)); expect(lightIcon, findsOneWidget); expect(darkIcon, findsNothing); expect(Theme.of(appBar).brightness, Brightness.light); @@ -233,8 +246,8 @@ void main() { await tester.tap(lightIcon); await tester.pumpAndSettle(const Duration(microseconds: 500)); - BuildContext appBar2 = tester.element(find.byType(AppBar)); - BuildContext body2 = tester.element(find.byType(Scaffold)); + BuildContext appBar2 = tester.element(find.byType(AppBar).first); + BuildContext body2 = tester.element(find.byType(Scaffold).first); BuildContext navigationRail2 = tester.element(find.byType(NavigationRail)); expect(lightIcon, findsNothing); expect(darkIcon, findsOneWidget); @@ -245,10 +258,15 @@ void main() { testWidgets('Color theme changes when a color is selected from menu', (tester) async { - await tester.pumpWidget(const Material3Demo()); - Finder menuIcon = find.widgetWithIcon(IconButton, Icons.more_vert); - BuildContext appBar = tester.element(find.byType(AppBar)); - BuildContext body = tester.element(find.byType(Scaffold)); + Color m3BaseColor = const Color(0xff6750a4); + await tester.pumpWidget(Container()); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + await tester.pump(); + Finder menuIcon = find.descendant( + of: find.byType(AppBar), + matching: find.widgetWithIcon(IconButton, Icons.more_vert)); + BuildContext appBar = tester.element(find.byType(AppBar).first); + BuildContext body = tester.element(find.byType(Scaffold).first); BuildContext navigationRail = tester.element(find.byType(NavigationRail)); expect(Theme.of(appBar).primaryColor, m3BaseColor); @@ -256,11 +274,11 @@ void main() { expect(Theme.of(navigationRail).primaryColor, m3BaseColor); await tester.tap(menuIcon); await tester.pumpAndSettle(); - await tester.tap(find.text('Blue')); + await tester.tap(find.text('Blue').last); await tester.pumpAndSettle(); - BuildContext appBar2 = tester.element(find.byType(AppBar)); - BuildContext body2 = tester.element(find.byType(Scaffold)); + BuildContext appBar2 = tester.element(find.byType(AppBar).first); + BuildContext body2 = tester.element(find.byType(Scaffold).first); BuildContext navigationRail2 = tester.element(find.byType(NavigationRail)); ThemeData expectedTheme = ThemeData(colorSchemeSeed: Colors.blue); expect(Theme.of(appBar2).primaryColor, expectedTheme.primaryColor); diff --git a/experimental/material_3_demo/test/elevation_screen_test.dart b/experimental/material_3_demo/test/elevation_screen_test.dart index ee31f381d..14f3cac2c 100644 --- a/experimental/material_3_demo/test/elevation_screen_test.dart +++ b/experimental/material_3_demo/test/elevation_screen_test.dart @@ -16,43 +16,54 @@ void main() { 'selected on NavigationBar', (tester) async { widgetSetup(tester, 449); addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - await tester.pumpWidget(const Material3Demo()); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); - expect(find.text('Surface Tint only'), findsNothing); + expect(find.text('Surface Tint Color Only'), findsNothing); expect(find.byType(NavigationBar), findsOneWidget); - Finder tintIconOnBar = find.byIcon(Icons.invert_colors_on_outlined); + Finder tintIconOnBar = find.descendant( + of: find.byType(NavigationBar), + matching: find.widgetWithIcon( + NavigationDestination, Icons.invert_colors_on_outlined)); expect(tintIconOnBar, findsOneWidget); await tester.tap(tintIconOnBar); await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(tintIconOnBar, findsNothing); - expect(find.byIcon(Icons.opacity), findsOneWidget); - expect(find.text('Surface Tint only'), findsOneWidget); + Finder selectedTintIconOnBar = find.descendant( + of: find.byType(NavigationBar), + matching: find.widgetWithIcon(NavigationDestination, Icons.opacity)); + expect(selectedTintIconOnBar, findsOneWidget); + expect(find.text('Surface Tint Color Only'), findsOneWidget); }); testWidgets( 'Surface Tones screen shows correctly when the corresponding icon is ' 'selected on NavigationRail', (tester) async { - widgetSetup(tester, 450); // NavigationRail shows only when width is >= 450. + widgetSetup( + tester, 1200); // NavigationRail shows only when width is > 1000. addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - await tester.pumpWidget(const Material3Demo()); - expect(find.text('Surface Tint only'), findsNothing); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); + expect(find.text('Surface Tint Color Only'), findsNothing); expect(find.byType(NavigationRail), findsOneWidget); - Finder tintIconOnRail = find.byIcon(Icons.invert_colors_on_outlined); + Finder tintIconOnRail = find.descendant( + of: find.byType(NavigationRail), + matching: find.byIcon(Icons.invert_colors_on_outlined)); expect(tintIconOnRail, findsOneWidget); await tester.tap(tintIconOnRail); await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(tintIconOnRail, findsNothing); - expect(find.byIcon(Icons.opacity), findsOneWidget); - expect(find.text('Surface Tint only'), findsOneWidget); + Finder selectedTintIconOnRail = find.descendant( + of: find.byType(NavigationRail), matching: find.byIcon(Icons.opacity)); + expect(selectedTintIconOnRail, findsOneWidget); + expect(find.text('Surface Tint Color Only'), findsOneWidget); }); testWidgets('Surface Tones screen shows correct content', (tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold(body: Row(children: const [ElevationScreen()])), )); - expect(find.text('Surface Tint only'), findsOneWidget); - expect(find.text('Surface Tint and Shadow'), findsOneWidget); - expect(find.text('Shadow only'), findsOneWidget); + expect(find.text('Surface Tint Color Only'), findsOneWidget); + expect(find.text('Surface Tint Color and Shadow Color'), findsOneWidget); + expect(find.text('Shadow Color Only'), findsOneWidget); expect(find.byType(ElevationGrid), findsNWidgets(3)); expect(find.byType(ElevationCard), findsNWidgets(18)); }); diff --git a/experimental/material_3_demo/test/typography_screen_test.dart b/experimental/material_3_demo/test/typography_screen_test.dart index a6dd95469..257a37e5b 100644 --- a/experimental/material_3_demo/test/typography_screen_test.dart +++ b/experimental/material_3_demo/test/typography_screen_test.dart @@ -16,33 +16,44 @@ void main() { 'selected on NavigationBar', (tester) async { widgetSetup(tester, 449); addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - await tester.pumpWidget(const Material3Demo()); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); expect(find.text('Display Large'), findsNothing); expect(find.byType(NavigationBar), findsOneWidget); - Finder textIconOnBar = find.byIcon(Icons.text_snippet_outlined); + Finder textIconOnBar = find.descendant( + of: find.byType(NavigationBar), + matching: find.byIcon(Icons.text_snippet_outlined)); expect(textIconOnBar, findsOneWidget); await tester.tap(textIconOnBar); await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(textIconOnBar, findsNothing); - expect(find.byIcon(Icons.text_snippet), findsOneWidget); + Finder selectedTextIconOnBar = find.descendant( + of: find.byType(NavigationBar), + matching: find.byIcon(Icons.text_snippet)); + expect(selectedTextIconOnBar, findsOneWidget); expect(find.text('Display Large'), findsOneWidget); }); testWidgets( 'Typography screen shows correctly when the corresponding icon is ' 'selected on NavigationRail', (tester) async { - widgetSetup(tester, 450); // NavigationRail shows only when width is >= 450. + widgetSetup( + tester, 1200); // NavigationRail shows only when width is > 1000. addTearDown(tester.binding.window.clearPhysicalSizeTestValue); - await tester.pumpWidget(const Material3Demo()); + await tester.pumpWidget(const MaterialApp(home: Material3Demo())); expect(find.text('Display Large'), findsNothing); expect(find.byType(NavigationRail), findsOneWidget); - Finder textIconOnRail = find.byIcon(Icons.text_snippet_outlined); + Finder textIconOnRail = find.descendant( + of: find.byType(NavigationRail), + matching: find.byIcon(Icons.text_snippet_outlined)); expect(textIconOnRail, findsOneWidget); await tester.tap(textIconOnRail); await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(textIconOnRail, findsNothing); - expect(find.byIcon(Icons.text_snippet), findsOneWidget); + Finder selectedTextIconOnRail = find.descendant( + of: find.byType(NavigationRail), + matching: find.byIcon(Icons.text_snippet)); + expect(selectedTextIconOnRail, findsOneWidget); expect(find.text('Display Large'), findsOneWidget); });