Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
samples/experimental/material_3_demo/lib/component_screen.dart
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
2548 lines (2354 sloc)
72.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2021 The Flutter team. 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/rendering.dart'; | |
import 'package:flutter/services.dart'; | |
const rowDivider = SizedBox(width: 20); | |
const colDivider = SizedBox(height: 10); | |
const tinySpacing = 3.0; | |
const smallSpacing = 10.0; | |
const double cardWidth = 115; | |
const double widthConstraint = 450; | |
class FirstComponentList extends StatelessWidget { | |
const FirstComponentList({ | |
super.key, | |
required this.showNavBottomBar, | |
required this.scaffoldKey, | |
required this.showSecondList, | |
}); | |
final bool showNavBottomBar; | |
final GlobalKey<ScaffoldState> scaffoldKey; | |
final bool showSecondList; | |
@override | |
Widget build(BuildContext context) { | |
List<Widget> children = [ | |
const Actions(), | |
colDivider, | |
const Communication(), | |
colDivider, | |
const Containment(), | |
if (!showSecondList) ...[ | |
colDivider, | |
Navigation(scaffoldKey: scaffoldKey), | |
colDivider, | |
const Selection(), | |
colDivider, | |
const TextInputs() | |
], | |
]; | |
List<double?> heights = List.filled(children.length, null); | |
// Fully traverse this list before moving on. | |
return FocusTraversalGroup( | |
child: CustomScrollView( | |
slivers: [ | |
SliverPadding( | |
padding: showSecondList | |
? const EdgeInsetsDirectional.only(end: smallSpacing) | |
: EdgeInsets.zero, | |
sliver: SliverList( | |
delegate: BuildSlivers( | |
heights: heights, | |
builder: (context, index) { | |
return _CacheHeight( | |
heights: heights, | |
index: index, | |
child: children[index], | |
); | |
}, | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class SecondComponentList extends StatelessWidget { | |
const SecondComponentList({ | |
super.key, | |
required this.scaffoldKey, | |
}); | |
final GlobalKey<ScaffoldState> scaffoldKey; | |
@override | |
Widget build(BuildContext context) { | |
List<Widget> children = [ | |
Navigation(scaffoldKey: scaffoldKey), | |
colDivider, | |
const Selection(), | |
colDivider, | |
const TextInputs(), | |
]; | |
List<double?> heights = List.filled(children.length, null); | |
// Fully traverse this list before moving on. | |
return FocusTraversalGroup( | |
child: CustomScrollView( | |
slivers: [ | |
SliverPadding( | |
padding: const EdgeInsetsDirectional.only(end: smallSpacing), | |
sliver: SliverList( | |
delegate: BuildSlivers( | |
heights: heights, | |
builder: (context, index) { | |
return _CacheHeight( | |
heights: heights, | |
index: index, | |
child: children[index], | |
); | |
}, | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
// If the content of a CustomScrollView does not change, then it's | |
// safe to cache the heights of each item as they are laid out. The | |
// sum of the cached heights are returned by an override of | |
// `SliverChildDelegate.estimateMaxScrollOffset`. The default version | |
// of this method bases its estimate on the average height of the | |
// visible items. The override ensures that the scrollbar thumb's | |
// size, which depends on the max scroll offset, will shrink smoothly | |
// as the contents of the list are exposed for the first time, and | |
// then remain fixed. | |
class _CacheHeight extends SingleChildRenderObjectWidget { | |
const _CacheHeight({ | |
super.child, | |
required this.heights, | |
required this.index, | |
}); | |
final List<double?> heights; | |
final int index; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return _RenderCacheHeight( | |
heights: heights, | |
index: index, | |
); | |
} | |
@override | |
void updateRenderObject( | |
BuildContext context, _RenderCacheHeight renderObject) { | |
renderObject | |
..heights = heights | |
..index = index; | |
} | |
} | |
class _RenderCacheHeight extends RenderProxyBox { | |
_RenderCacheHeight({ | |
required List<double?> heights, | |
required int index, | |
}) : _heights = heights, | |
_index = index, | |
super(); | |
List<double?> _heights; | |
List<double?> get heights => _heights; | |
set heights(List<double?> value) { | |
if (value == _heights) { | |
return; | |
} | |
_heights = value; | |
markNeedsLayout(); | |
} | |
int _index; | |
int get index => _index; | |
set index(int value) { | |
if (value == index) { | |
return; | |
} | |
_index = value; | |
markNeedsLayout(); | |
} | |
@override | |
void performLayout() { | |
super.performLayout(); | |
heights[index] = size.height; | |
} | |
} | |
// The heights information is used to override the `estimateMaxScrollOffset` and | |
// provide a more accurate estimation for the max scroll offset. | |
class BuildSlivers extends SliverChildBuilderDelegate { | |
BuildSlivers({ | |
required NullableIndexedWidgetBuilder builder, | |
required this.heights, | |
}) : super(builder, childCount: heights.length); | |
final List<double?> heights; | |
@override | |
double? estimateMaxScrollOffset(int firstIndex, int lastIndex, | |
double leadingScrollOffset, double trailingScrollOffset) { | |
return heights.reduce((sum, height) => (sum ?? 0) + (height ?? 0))!; | |
} | |
} | |
class Actions extends StatelessWidget { | |
const Actions({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentGroupDecoration(label: 'Actions', children: <Widget>[ | |
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(), | |
Dividers(), | |
// TODO: Add Lists, https://github.com/flutter/flutter/issues/114006 | |
// TODO: Add Side sheets, https://github.com/flutter/flutter/issues/119328 | |
]); | |
} | |
} | |
class Navigation extends StatelessWidget { | |
const Navigation({super.key, required this.scaffoldKey}); | |
final GlobalKey<ScaffoldState> scaffoldKey; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentGroupDecoration(label: 'Navigation', children: [ | |
const BottomAppBars(), | |
const NavigationBars( | |
selectedIndex: 0, | |
isExampleBar: true, | |
), | |
NavigationDrawers(scaffoldKey: scaffoldKey), | |
const NavigationRails(), | |
const Tabs(), | |
const SearchAnchors(), | |
const TopAppBars(), | |
]); | |
} | |
} | |
class Selection extends StatelessWidget { | |
const Selection({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentGroupDecoration(label: 'Selection', children: [ | |
Checkboxes(), | |
Chips(), | |
DatePickers(), | |
Menus(), | |
Radios(), | |
Sliders(), | |
Switches(), | |
TimePickers(), | |
]); | |
} | |
} | |
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}); | |
@override | |
State<Buttons> createState() => _ButtonsState(); | |
} | |
class _ButtonsState extends State<Buttons> { | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentDecoration( | |
label: 'Common buttons', | |
tooltipMessage: | |
'Use ElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton, or TextButton', | |
child: SingleChildScrollView( | |
scrollDirection: Axis.horizontal, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: <Widget>[ | |
ButtonsWithoutIcon(isDisabled: false), | |
ButtonsWithIcon(), | |
ButtonsWithoutIcon(isDisabled: true), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ButtonsWithoutIcon extends StatelessWidget { | |
final bool isDisabled; | |
const ButtonsWithoutIcon({super.key, required this.isDisabled}); | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 5.0), | |
child: IntrinsicWidth( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: <Widget>[ | |
ElevatedButton( | |
onPressed: isDisabled ? null : () {}, | |
child: const Text('Elevated'), | |
), | |
colDivider, | |
FilledButton( | |
onPressed: isDisabled ? null : () {}, | |
child: const Text('Filled'), | |
), | |
colDivider, | |
FilledButton.tonal( | |
onPressed: isDisabled ? null : () {}, | |
child: const Text('Filled tonal'), | |
), | |
colDivider, | |
OutlinedButton( | |
onPressed: isDisabled ? null : () {}, | |
child: const Text('Outlined'), | |
), | |
colDivider, | |
TextButton( | |
onPressed: isDisabled ? null : () {}, | |
child: const Text('Text'), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ButtonsWithIcon extends StatelessWidget { | |
const ButtonsWithIcon({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 10.0), | |
child: IntrinsicWidth( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: <Widget>[ | |
ElevatedButton.icon( | |
onPressed: () {}, | |
icon: const Icon(Icons.add), | |
label: const Text('Icon'), | |
), | |
colDivider, | |
FilledButton.icon( | |
onPressed: () {}, | |
label: const Text('Icon'), | |
icon: const Icon(Icons.add), | |
), | |
colDivider, | |
FilledButton.tonalIcon( | |
onPressed: () {}, | |
label: const Text('Icon'), | |
icon: const Icon(Icons.add), | |
), | |
colDivider, | |
OutlinedButton.icon( | |
onPressed: () {}, | |
icon: const Icon(Icons.add), | |
label: const Text('Icon'), | |
), | |
colDivider, | |
TextButton.icon( | |
onPressed: () {}, | |
icon: const Icon(Icons.add), | |
label: const Text('Icon'), | |
) | |
], | |
), | |
), | |
); | |
} | |
} | |
class FloatingActionButtons extends StatelessWidget { | |
const FloatingActionButtons({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Floating action buttons', | |
tooltipMessage: | |
'Use FloatingActionButton or FloatingActionButton.extended', | |
child: Wrap( | |
crossAxisAlignment: WrapCrossAlignment.center, | |
runSpacing: smallSpacing, | |
spacing: smallSpacing, | |
children: [ | |
FloatingActionButton.small( | |
onPressed: () {}, | |
tooltip: 'Small', | |
child: const Icon(Icons.add), | |
), | |
FloatingActionButton.extended( | |
onPressed: () {}, | |
tooltip: 'Extended', | |
icon: const Icon(Icons.add), | |
label: const Text('Create'), | |
), | |
FloatingActionButton( | |
onPressed: () {}, | |
tooltip: 'Standard', | |
child: const Icon(Icons.add), | |
), | |
FloatingActionButton.large( | |
onPressed: () {}, | |
tooltip: 'Large', | |
child: const Icon(Icons.add), | |
), | |
], | |
), | |
); | |
} | |
} | |
class Cards extends StatelessWidget { | |
const Cards({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Cards', | |
tooltipMessage: 'Use Card', | |
child: Wrap( | |
alignment: WrapAlignment.spaceEvenly, | |
children: [ | |
SizedBox( | |
width: cardWidth, | |
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: () {}, | |
), | |
), | |
const SizedBox(height: 20), | |
const Align( | |
alignment: Alignment.bottomLeft, | |
child: Text('Elevated'), | |
) | |
], | |
), | |
), | |
), | |
), | |
SizedBox( | |
width: cardWidth, | |
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: () {}, | |
), | |
), | |
const SizedBox(height: 20), | |
const Align( | |
alignment: Alignment.bottomLeft, | |
child: Text('Filled'), | |
) | |
], | |
), | |
), | |
), | |
), | |
SizedBox( | |
width: cardWidth, | |
child: Card( | |
elevation: 0, | |
shape: RoundedRectangleBorder( | |
side: BorderSide( | |
color: Theme.of(context).colorScheme.outline, | |
), | |
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: () {}, | |
), | |
), | |
const SizedBox(height: 20), | |
const Align( | |
alignment: Alignment.bottomLeft, | |
child: Text('Outlined'), | |
) | |
], | |
), | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class _ClearButton extends StatelessWidget { | |
const _ClearButton({required this.controller}); | |
final TextEditingController controller; | |
@override | |
Widget build(BuildContext context) => IconButton( | |
icon: const Icon(Icons.clear), | |
onPressed: () => controller.clear(), | |
); | |
} | |
class TextFields extends StatefulWidget { | |
const TextFields({super.key}); | |
@override | |
State<TextFields> createState() => _TextFieldsState(); | |
} | |
class _TextFieldsState extends State<TextFields> { | |
final TextEditingController _controllerFilled = TextEditingController(); | |
final TextEditingController _controllerOutlined = TextEditingController(); | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Text fields', | |
tooltipMessage: 'Use TextField with different InputDecoration', | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.all(smallSpacing), | |
child: TextField( | |
controller: _controllerFilled, | |
decoration: InputDecoration( | |
prefixIcon: const Icon(Icons.search), | |
suffixIcon: _ClearButton(controller: _controllerFilled), | |
labelText: 'Filled', | |
hintText: 'hint text', | |
helperText: 'supporting text', | |
filled: true, | |
), | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(smallSpacing), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Flexible( | |
child: SizedBox( | |
width: 200, | |
child: TextField( | |
maxLength: 10, | |
maxLengthEnforcement: MaxLengthEnforcement.none, | |
controller: _controllerFilled, | |
decoration: InputDecoration( | |
prefixIcon: const Icon(Icons.search), | |
suffixIcon: _ClearButton(controller: _controllerFilled), | |
labelText: 'Filled', | |
hintText: 'hint text', | |
helperText: 'supporting text', | |
filled: true, | |
errorText: 'error text', | |
), | |
), | |
), | |
), | |
const SizedBox(width: smallSpacing), | |
Flexible( | |
child: SizedBox( | |
width: 200, | |
child: TextField( | |
controller: _controllerFilled, | |
enabled: false, | |
decoration: InputDecoration( | |
prefixIcon: const Icon(Icons.search), | |
suffixIcon: _ClearButton(controller: _controllerFilled), | |
labelText: 'Disabled', | |
hintText: 'hint text', | |
helperText: 'supporting text', | |
filled: true, | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(smallSpacing), | |
child: TextField( | |
controller: _controllerOutlined, | |
decoration: InputDecoration( | |
prefixIcon: const Icon(Icons.search), | |
suffixIcon: _ClearButton(controller: _controllerOutlined), | |
labelText: 'Outlined', | |
hintText: 'hint text', | |
helperText: 'supporting text', | |
border: const OutlineInputBorder(), | |
), | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(smallSpacing), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Flexible( | |
child: SizedBox( | |
width: 200, | |
child: TextField( | |
controller: _controllerOutlined, | |
decoration: InputDecoration( | |
prefixIcon: const Icon(Icons.search), | |
suffixIcon: | |
_ClearButton(controller: _controllerOutlined), | |
labelText: 'Outlined', | |
hintText: 'hint text', | |
helperText: 'supporting text', | |
errorText: 'error text', | |
border: const OutlineInputBorder(), | |
filled: true, | |
), | |
), | |
), | |
), | |
const SizedBox(width: smallSpacing), | |
Flexible( | |
child: SizedBox( | |
width: 200, | |
child: TextField( | |
controller: _controllerOutlined, | |
enabled: false, | |
decoration: InputDecoration( | |
prefixIcon: const Icon(Icons.search), | |
suffixIcon: | |
_ClearButton(controller: _controllerOutlined), | |
labelText: 'Disabled', | |
hintText: 'hint text', | |
helperText: 'supporting text', | |
border: const OutlineInputBorder(), | |
filled: true, | |
), | |
), | |
), | |
), | |
])), | |
], | |
), | |
); | |
} | |
} | |
class Dialogs extends StatefulWidget { | |
const Dialogs({super.key}); | |
@override | |
State<Dialogs> createState() => _DialogsState(); | |
} | |
class _DialogsState extends State<Dialogs> { | |
void openDialog(BuildContext context) { | |
showDialog<void>( | |
context: context, | |
builder: (context) => AlertDialog( | |
title: const Text('What is a dialog?'), | |
content: const Text( | |
'A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made.'), | |
actions: <Widget>[ | |
TextButton( | |
child: const Text('Okay'), | |
onPressed: () => Navigator.of(context).pop(), | |
), | |
FilledButton( | |
child: const Text('Dismiss'), | |
onPressed: () => Navigator.of(context).pop(), | |
), | |
], | |
), | |
); | |
} | |
void openFullscreenDialog(BuildContext context) { | |
showDialog<void>( | |
context: context, | |
builder: (context) => Dialog.fullscreen( | |
child: Padding( | |
padding: const EdgeInsets.all(20.0), | |
child: Scaffold( | |
appBar: AppBar( | |
title: const Text('Full-screen dialog'), | |
centerTitle: false, | |
leading: IconButton( | |
icon: const Icon(Icons.close), | |
onPressed: () => Navigator.of(context).pop(), | |
), | |
actions: [ | |
TextButton( | |
child: const Text('Close'), | |
onPressed: () => Navigator.of(context).pop(), | |
), | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Dialog', | |
tooltipMessage: | |
'Use showDialog with Dialog.fullscreen, AlertDialog, or SimpleDialog', | |
child: Wrap( | |
alignment: WrapAlignment.spaceBetween, | |
children: [ | |
TextButton( | |
child: const Text( | |
'Show dialog', | |
style: TextStyle(fontWeight: FontWeight.bold), | |
), | |
onPressed: () => openDialog(context), | |
), | |
TextButton( | |
child: const Text( | |
'Show full-screen dialog', | |
style: TextStyle(fontWeight: FontWeight.bold), | |
), | |
onPressed: () => openFullscreenDialog(context), | |
), | |
], | |
), | |
); | |
} | |
} | |
class Dividers extends StatelessWidget { | |
const Dividers({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentDecoration( | |
label: 'Dividers', | |
tooltipMessage: 'Use Divider or VerticalDivider', | |
child: Column( | |
children: <Widget>[ | |
Divider(key: Key('divider')), | |
], | |
), | |
); | |
} | |
} | |
class Switches extends StatelessWidget { | |
const Switches({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentDecoration( | |
label: 'Switches', | |
tooltipMessage: 'Use SwitchListTile or Switch', | |
child: Column( | |
children: <Widget>[ | |
SwitchRow(isEnabled: true), | |
SwitchRow(isEnabled: false), | |
], | |
), | |
); | |
} | |
} | |
class SwitchRow extends StatefulWidget { | |
const SwitchRow({super.key, required this.isEnabled}); | |
final bool isEnabled; | |
@override | |
State<SwitchRow> createState() => _SwitchRowState(); | |
} | |
class _SwitchRowState extends State<SwitchRow> { | |
bool value0 = false; | |
bool value1 = true; | |
final MaterialStateProperty<Icon?> thumbIcon = | |
MaterialStateProperty.resolveWith<Icon?>((states) { | |
if (states.contains(MaterialState.selected)) { | |
return const Icon(Icons.check); | |
} | |
return const Icon(Icons.close); | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Row( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
// TODO: use SwitchListTile when thumbIcon is available https://github.com/flutter/flutter/issues/118616 | |
Switch( | |
value: value0, | |
onChanged: widget.isEnabled | |
? (value) { | |
setState(() { | |
value0 = value; | |
}); | |
} | |
: null, | |
), | |
Switch( | |
thumbIcon: thumbIcon, | |
value: value1, | |
onChanged: widget.isEnabled | |
? (value) { | |
setState(() { | |
value1 = value; | |
}); | |
} | |
: null, | |
), | |
], | |
); | |
} | |
} | |
class Checkboxes extends StatefulWidget { | |
const Checkboxes({super.key}); | |
@override | |
State<Checkboxes> createState() => _CheckboxesState(); | |
} | |
class _CheckboxesState extends State<Checkboxes> { | |
bool? isChecked0 = true; | |
bool? isChecked1; | |
bool? isChecked2 = false; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Checkboxes', | |
tooltipMessage: 'Use CheckboxListTile or Checkbox', | |
child: Column( | |
children: <Widget>[ | |
CheckboxListTile( | |
tristate: true, | |
value: isChecked0, | |
title: const Text('Option 1'), | |
onChanged: (value) { | |
setState(() { | |
isChecked0 = value; | |
}); | |
}, | |
), | |
CheckboxListTile( | |
tristate: true, | |
value: isChecked1, | |
title: const Text('Option 2'), | |
onChanged: (value) { | |
setState(() { | |
isChecked1 = value; | |
}); | |
}, | |
), | |
CheckboxListTile( | |
tristate: true, | |
value: isChecked2, | |
title: const Text('Option 3'), | |
// TODO: showcase error state https://github.com/flutter/flutter/issues/118616 | |
onChanged: (value) { | |
setState(() { | |
isChecked2 = value; | |
}); | |
}, | |
), | |
const CheckboxListTile( | |
tristate: true, | |
title: Text('Option 4'), | |
value: true, | |
onChanged: null, | |
), | |
], | |
), | |
); | |
} | |
} | |
enum Value { first, second } | |
class Radios extends StatefulWidget { | |
const Radios({super.key}); | |
@override | |
State<Radios> createState() => _RadiosState(); | |
} | |
enum Options { option1, option2, option3 } | |
class _RadiosState extends State<Radios> { | |
Options? _selectedOption = Options.option1; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Radio buttons', | |
tooltipMessage: 'Use RadioListTile<T> or Radio<T>', | |
child: Column( | |
children: <Widget>[ | |
RadioListTile<Options>( | |
title: const Text('Option 1'), | |
value: Options.option1, | |
groupValue: _selectedOption, | |
onChanged: (value) { | |
setState(() { | |
_selectedOption = value; | |
}); | |
}, | |
), | |
RadioListTile<Options>( | |
title: const Text('Option 2'), | |
value: Options.option2, | |
groupValue: _selectedOption, | |
onChanged: (value) { | |
setState(() { | |
_selectedOption = value; | |
}); | |
}, | |
), | |
RadioListTile<Options>( | |
title: const Text('Option 3'), | |
value: Options.option3, | |
groupValue: _selectedOption, | |
onChanged: null, | |
), | |
], | |
), | |
); | |
} | |
} | |
class ProgressIndicators extends StatefulWidget { | |
const ProgressIndicators({super.key}); | |
@override | |
State<ProgressIndicators> createState() => _ProgressIndicatorsState(); | |
} | |
class _ProgressIndicatorsState extends State<ProgressIndicators> { | |
bool playProgressIndicator = false; | |
@override | |
Widget build(BuildContext context) { | |
final double? progressValue = playProgressIndicator ? null : 0.7; | |
return ComponentDecoration( | |
label: 'Progress indicators', | |
tooltipMessage: | |
'Use CircularProgressIndicator or LinearProgressIndicator', | |
child: Column( | |
children: <Widget>[ | |
Row( | |
children: [ | |
IconButton( | |
isSelected: playProgressIndicator, | |
selectedIcon: const Icon(Icons.pause), | |
icon: const Icon(Icons.play_arrow), | |
onPressed: () { | |
setState(() { | |
playProgressIndicator = !playProgressIndicator; | |
}); | |
}, | |
), | |
Expanded( | |
child: Row( | |
children: <Widget>[ | |
rowDivider, | |
CircularProgressIndicator( | |
value: progressValue, | |
), | |
rowDivider, | |
Expanded( | |
child: LinearProgressIndicator( | |
value: progressValue, | |
), | |
), | |
rowDivider, | |
], | |
), | |
), | |
], | |
), | |
], | |
), | |
); | |
} | |
} | |
const List<NavigationDestination> appBarDestinations = [ | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.widgets_outlined), | |
label: 'Components', | |
selectedIcon: Icon(Icons.widgets), | |
), | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.format_paint_outlined), | |
label: 'Color', | |
selectedIcon: Icon(Icons.format_paint), | |
), | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.text_snippet_outlined), | |
label: 'Typography', | |
selectedIcon: Icon(Icons.text_snippet), | |
), | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.invert_colors_on_outlined), | |
label: 'Elevation', | |
selectedIcon: Icon(Icons.opacity), | |
) | |
]; | |
const List<Widget> exampleBarDestinations = [ | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.explore_outlined), | |
label: 'Explore', | |
selectedIcon: Icon(Icons.explore), | |
), | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.pets_outlined), | |
label: 'Pets', | |
selectedIcon: Icon(Icons.pets), | |
), | |
NavigationDestination( | |
tooltip: '', | |
icon: Icon(Icons.account_box_outlined), | |
label: 'Account', | |
selectedIcon: Icon(Icons.account_box), | |
) | |
]; | |
List<Widget> 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 = false, | |
}); | |
final void Function(int)? onSelectItem; | |
final int selectedIndex; | |
final bool isExampleBar; | |
final bool isBadgeExample; | |
@override | |
State<NavigationBars> createState() => _NavigationBarsState(); | |
} | |
class _NavigationBarsState extends State<NavigationBars> { | |
late int selectedIndex; | |
@override | |
void initState() { | |
super.initState(); | |
selectedIndex = widget.selectedIndex; | |
} | |
@override | |
void didUpdateWidget(covariant NavigationBars oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.selectedIndex != oldWidget.selectedIndex) { | |
selectedIndex = widget.selectedIndex; | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
// App NavigationBar should get first focus. | |
Widget navigationBar = Focus( | |
autofocus: !(widget.isExampleBar || widget.isBadgeExample), | |
child: NavigationBar( | |
selectedIndex: selectedIndex, | |
onDestinationSelected: (index) { | |
setState(() { | |
selectedIndex = index; | |
}); | |
if (!widget.isExampleBar) widget.onSelectItem!(index); | |
}, | |
destinations: widget.isExampleBar && widget.isBadgeExample | |
? barWithBadgeDestinations | |
: widget.isExampleBar | |
? exampleBarDestinations | |
: appBarDestinations, | |
), | |
); | |
if (widget.isExampleBar && widget.isBadgeExample) { | |
navigationBar = ComponentDecoration( | |
label: 'Badges', | |
tooltipMessage: 'Use Badge or Badge.count', | |
child: navigationBar); | |
} else if (widget.isExampleBar) { | |
navigationBar = ComponentDecoration( | |
label: 'Navigation bar', | |
tooltipMessage: 'Use NavigationBar', | |
child: navigationBar); | |
} | |
return navigationBar; | |
} | |
} | |
class IconToggleButtons extends StatefulWidget { | |
const IconToggleButtons({super.key}); | |
@override | |
State<IconToggleButtons> createState() => _IconToggleButtonsState(); | |
} | |
class _IconToggleButtonsState extends State<IconToggleButtons> { | |
bool standardSelected = false; | |
bool filledSelected = false; | |
bool tonalSelected = false; | |
bool outlinedSelected = false; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Icon buttons', | |
tooltipMessage: | |
'Use IconButton, IconButton.filled, IconButton.filledTonal, and IconButton.outlined', | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: <Widget>[ | |
Column( | |
// Standard IconButton | |
children: <Widget>[ | |
IconButton( | |
isSelected: standardSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: () { | |
setState(() { | |
standardSelected = !standardSelected; | |
}); | |
}, | |
), | |
colDivider, | |
IconButton( | |
isSelected: standardSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: null, | |
), | |
], | |
), | |
Column( | |
children: <Widget>[ | |
// Filled IconButton | |
IconButton.filled( | |
isSelected: filledSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: () { | |
setState(() { | |
filledSelected = !filledSelected; | |
}); | |
}, | |
), | |
colDivider, | |
IconButton.filled( | |
isSelected: filledSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: null, | |
), | |
], | |
), | |
Column( | |
children: <Widget>[ | |
// Filled Tonal IconButton | |
IconButton.filledTonal( | |
isSelected: tonalSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: () { | |
setState(() { | |
tonalSelected = !tonalSelected; | |
}); | |
}, | |
), | |
colDivider, | |
IconButton.filledTonal( | |
isSelected: tonalSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: null, | |
), | |
], | |
), | |
Column( | |
children: <Widget>[ | |
// Outlined IconButton | |
IconButton.outlined( | |
isSelected: outlinedSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: () { | |
setState(() { | |
outlinedSelected = !outlinedSelected; | |
}); | |
}, | |
), | |
colDivider, | |
IconButton.outlined( | |
isSelected: outlinedSelected, | |
icon: const Icon(Icons.settings_outlined), | |
selectedIcon: const Icon(Icons.settings), | |
onPressed: null, | |
), | |
], | |
), | |
], | |
), | |
); | |
} | |
} | |
class Chips extends StatefulWidget { | |
const Chips({super.key}); | |
@override | |
State<Chips> createState() => _ChipsState(); | |
} | |
class _ChipsState extends State<Chips> { | |
bool isFiltered = true; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Chips', | |
tooltipMessage: | |
'Use ActionChip, FilterChip, or InputChip. \nActionChip can also be used for suggestion chip', | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: <Widget>[ | |
Wrap( | |
spacing: smallSpacing, | |
runSpacing: smallSpacing, | |
children: <Widget>[ | |
ActionChip( | |
label: const Text('Assist'), | |
avatar: const Icon(Icons.event), | |
onPressed: () {}, | |
), | |
FilterChip( | |
label: const Text('Filter'), | |
selected: isFiltered, | |
onSelected: (selected) { | |
setState(() => isFiltered = selected); | |
}, | |
), | |
InputChip( | |
label: const Text('Input'), | |
onPressed: () {}, | |
onDeleted: () {}, | |
), | |
ActionChip( | |
label: const Text('Suggestion'), | |
onPressed: () {}, | |
), | |
], | |
), | |
colDivider, | |
Wrap( | |
spacing: smallSpacing, | |
runSpacing: smallSpacing, | |
children: <Widget>[ | |
const ActionChip( | |
label: Text('Assist'), | |
avatar: Icon(Icons.event), | |
), | |
FilterChip( | |
label: const Text('Filter'), | |
selected: isFiltered, | |
onSelected: null, | |
), | |
InputChip( | |
label: const Text('Input'), | |
onDeleted: () {}, | |
isEnabled: false, | |
), | |
const ActionChip( | |
label: Text('Suggestion'), | |
), | |
], | |
), | |
], | |
), | |
); | |
} | |
} | |
class DatePickers extends StatefulWidget { | |
const DatePickers({super.key}); | |
@override | |
State<DatePickers> createState() => _DatePickersState(); | |
} | |
class _DatePickersState extends State<DatePickers> { | |
DateTime? selectedDate; | |
final DateTime _firstDate = DateTime(DateTime.now().year - 2); | |
final DateTime _lastDate = DateTime(DateTime.now().year + 1); | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Date picker', | |
tooltipMessage: 'Use showDatePicker', | |
child: TextButton( | |
onPressed: () async { | |
DateTime? date = await showDatePicker( | |
context: context, | |
initialDate: selectedDate ?? DateTime.now(), | |
firstDate: _firstDate, | |
lastDate: _lastDate, | |
); | |
setState(() { | |
selectedDate = date; | |
if (selectedDate != null) { | |
ScaffoldMessenger.of(context).showSnackBar(SnackBar( | |
content: Text( | |
'Selected Date: ${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'), | |
)); | |
} | |
}); | |
}, | |
child: const Text( | |
'Show date picker', | |
style: TextStyle(fontWeight: FontWeight.bold), | |
), | |
), | |
); | |
} | |
} | |
class TimePickers extends StatefulWidget { | |
const TimePickers({super.key}); | |
@override | |
State<TimePickers> createState() => _TimePickersState(); | |
} | |
class _TimePickersState extends State<TimePickers> { | |
TimeOfDay? selectedTime; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Time picker', | |
tooltipMessage: 'Use showTimePicker', | |
child: TextButton( | |
onPressed: () async { | |
final TimeOfDay? time = await showTimePicker( | |
context: context, | |
initialTime: selectedTime ?? TimeOfDay.now(), | |
builder: (context, child) { | |
return MediaQuery( | |
data: MediaQuery.of(context).copyWith( | |
alwaysUse24HourFormat: true, | |
), | |
child: child!, | |
); | |
}, | |
); | |
setState(() { | |
selectedTime = time; | |
if (selectedTime != null) { | |
ScaffoldMessenger.of(context).showSnackBar(SnackBar( | |
content: | |
Text('Selected time: ${selectedTime!.format(context)}'), | |
)); | |
} | |
}); | |
}, | |
child: const Text( | |
'Show time picker', | |
style: TextStyle(fontWeight: FontWeight.bold), | |
), | |
), | |
); | |
} | |
} | |
class SegmentedButtons extends StatelessWidget { | |
const SegmentedButtons({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentDecoration( | |
label: 'Segmented buttons', | |
tooltipMessage: 'Use SegmentedButton<T>', | |
child: Column( | |
children: <Widget>[ | |
SingleChoice(), | |
colDivider, | |
MultipleChoice(), | |
], | |
), | |
); | |
} | |
} | |
enum Calendar { day, week, month, year } | |
class SingleChoice extends StatefulWidget { | |
const SingleChoice({super.key}); | |
@override | |
State<SingleChoice> createState() => _SingleChoiceState(); | |
} | |
class _SingleChoiceState extends State<SingleChoice> { | |
Calendar calendarView = Calendar.day; | |
@override | |
Widget build(BuildContext context) { | |
return SegmentedButton<Calendar>( | |
segments: const <ButtonSegment<Calendar>>[ | |
ButtonSegment<Calendar>( | |
value: Calendar.day, | |
label: Text('Day'), | |
icon: Icon(Icons.calendar_view_day)), | |
ButtonSegment<Calendar>( | |
value: Calendar.week, | |
label: Text('Week'), | |
icon: Icon(Icons.calendar_view_week)), | |
ButtonSegment<Calendar>( | |
value: Calendar.month, | |
label: Text('Month'), | |
icon: Icon(Icons.calendar_view_month)), | |
ButtonSegment<Calendar>( | |
value: Calendar.year, | |
label: Text('Year'), | |
icon: Icon(Icons.calendar_today)), | |
], | |
selected: <Calendar>{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<MultipleChoice> createState() => _MultipleChoiceState(); | |
} | |
class _MultipleChoiceState extends State<MultipleChoice> { | |
Set<Sizes> selection = <Sizes>{Sizes.large, Sizes.extraLarge}; | |
@override | |
Widget build(BuildContext context) { | |
return SegmentedButton<Sizes>( | |
segments: const <ButtonSegment<Sizes>>[ | |
ButtonSegment<Sizes>(value: Sizes.extraSmall, label: Text('XS')), | |
ButtonSegment<Sizes>(value: Sizes.small, label: Text('S')), | |
ButtonSegment<Sizes>(value: Sizes.medium, label: Text('M')), | |
ButtonSegment<Sizes>( | |
value: Sizes.large, | |
label: Text('L'), | |
), | |
ButtonSegment<Sizes>(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 ScaffoldMessenger.of(context).showSnackBar with SnackBar', | |
child: TextButton( | |
onPressed: () { | |
final snackBar = SnackBar( | |
behavior: SnackBarBehavior.floating, | |
width: 400.0, | |
content: const Text('This is a snackbar'), | |
action: SnackBarAction( | |
label: 'Close', | |
onPressed: () {}, | |
), | |
); | |
ScaffoldMessenger.of(context).hideCurrentSnackBar(); | |
ScaffoldMessenger.of(context).showSnackBar(snackBar); | |
}, | |
child: const Text( | |
'Show snackbar', | |
style: TextStyle(fontWeight: FontWeight.bold), | |
), | |
), | |
); | |
} | |
} | |
class BottomSheetSection extends StatefulWidget { | |
const BottomSheetSection({super.key}); | |
@override | |
State<BottomSheetSection> createState() => _BottomSheetSectionState(); | |
} | |
class _BottomSheetSectionState extends State<BottomSheetSection> { | |
bool isNonModalBottomSheetOpen = false; | |
PersistentBottomSheetController<void>? _nonModalBottomSheetController; | |
@override | |
Widget build(BuildContext context) { | |
List<Widget> buttonList = <Widget>[ | |
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<Text> labelList = const <Text>[ | |
Text('Share'), | |
Text('Add to'), | |
Text('Trash'), | |
Text('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<T> or showBottomSheet<T>', | |
child: Wrap( | |
alignment: WrapAlignment.spaceEvenly, | |
children: [ | |
TextButton( | |
child: const Text( | |
'Show modal bottom sheet', | |
style: TextStyle(fontWeight: FontWeight.bold), | |
), | |
onPressed: () { | |
showModalBottomSheet<void>( | |
showDragHandle: true, | |
context: context, | |
// TODO: Remove when this is in the framework https://github.com/flutter/flutter/issues/118619 | |
constraints: const BoxConstraints(maxWidth: 640), | |
builder: (context) { | |
return SizedBox( | |
height: 150, | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 32.0), | |
child: ListView( | |
shrinkWrap: true, | |
scrollDirection: Axis.horizontal, | |
children: buttonList, | |
), | |
), | |
); | |
}, | |
); | |
}, | |
), | |
TextButton( | |
child: Text( | |
isNonModalBottomSheetOpen | |
? 'Hide bottom sheet' | |
: 'Show bottom sheet', | |
style: const TextStyle(fontWeight: FontWeight.bold), | |
), | |
onPressed: () { | |
if (isNonModalBottomSheetOpen) { | |
_nonModalBottomSheetController?.close(); | |
setState(() { | |
isNonModalBottomSheetOpen = false; | |
}); | |
return; | |
} else { | |
setState(() { | |
isNonModalBottomSheetOpen = true; | |
}); | |
} | |
_nonModalBottomSheetController = showBottomSheet<void>( | |
elevation: 8.0, | |
context: context, | |
// TODO: Remove when this is in the framework https://github.com/flutter/flutter/issues/118619 | |
constraints: const BoxConstraints(maxWidth: 640), | |
builder: (context) { | |
return SizedBox( | |
height: 150, | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 32.0), | |
child: ListView( | |
shrinkWrap: true, | |
scrollDirection: Axis.horizontal, | |
children: buttonList, | |
), | |
), | |
); | |
}, | |
); | |
}, | |
), | |
], | |
), | |
); | |
} | |
} | |
class BottomAppBars extends StatelessWidget { | |
const BottomAppBars({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Bottom app bar', | |
tooltipMessage: 'Use BottomAppBar', | |
child: Column( | |
children: [ | |
SizedBox( | |
height: 80, | |
child: Scaffold( | |
floatingActionButton: FloatingActionButton( | |
onPressed: () {}, | |
elevation: 0.0, | |
child: const Icon(Icons.add), | |
), | |
floatingActionButtonLocation: | |
FloatingActionButtonLocation.endContained, | |
bottomNavigationBar: BottomAppBar( | |
child: Row( | |
children: <Widget>[ | |
const IconButtonAnchorExample(), | |
IconButton( | |
tooltip: 'Search', | |
icon: const Icon(Icons.search), | |
onPressed: () {}, | |
), | |
IconButton( | |
tooltip: 'Favorite', | |
icon: const Icon(Icons.favorite), | |
onPressed: () {}, | |
), | |
], | |
), | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class IconButtonAnchorExample extends StatelessWidget { | |
const IconButtonAnchorExample({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MenuAnchor( | |
builder: (context, controller, child) { | |
return IconButton( | |
onPressed: () { | |
if (controller.isOpen) { | |
controller.close(); | |
} else { | |
controller.open(); | |
} | |
}, | |
icon: const Icon(Icons.more_vert), | |
); | |
}, | |
menuChildren: [ | |
MenuItemButton( | |
child: const Text('Menu 1'), | |
onPressed: () {}, | |
), | |
MenuItemButton( | |
child: const Text('Menu 2'), | |
onPressed: () {}, | |
), | |
SubmenuButton( | |
menuChildren: <Widget>[ | |
MenuItemButton( | |
onPressed: () {}, | |
child: const Text('Menu 3.1'), | |
), | |
MenuItemButton( | |
onPressed: () {}, | |
child: const Text('Menu 3.2'), | |
), | |
MenuItemButton( | |
onPressed: () {}, | |
child: const Text('Menu 3.3'), | |
), | |
], | |
child: const Text('Menu 3'), | |
), | |
], | |
); | |
} | |
} | |
class ButtonAnchorExample extends StatelessWidget { | |
const ButtonAnchorExample({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MenuAnchor( | |
builder: (context, controller, child) { | |
return FilledButton.tonal( | |
onPressed: () { | |
if (controller.isOpen) { | |
controller.close(); | |
} else { | |
controller.open(); | |
} | |
}, | |
child: const Text('Show menu'), | |
); | |
}, | |
menuChildren: [ | |
MenuItemButton( | |
leadingIcon: const Icon(Icons.people_alt_outlined), | |
child: const Text('Item 1'), | |
onPressed: () {}, | |
), | |
MenuItemButton( | |
leadingIcon: const Icon(Icons.remove_red_eye_outlined), | |
child: const Text('Item 2'), | |
onPressed: () {}, | |
), | |
MenuItemButton( | |
leadingIcon: const Icon(Icons.refresh), | |
onPressed: () {}, | |
child: const Text('Item 3'), | |
), | |
], | |
); | |
} | |
} | |
class NavigationDrawers extends StatelessWidget { | |
const NavigationDrawers({super.key, required this.scaffoldKey}); | |
final GlobalKey<ScaffoldState> scaffoldKey; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Navigation drawer', | |
tooltipMessage: | |
'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer', | |
child: Column( | |
children: [ | |
const SizedBox(height: 520, child: NavigationDrawerSection()), | |
colDivider, | |
colDivider, | |
TextButton( | |
child: const Text('Show modal navigation drawer', | |
style: TextStyle(fontWeight: FontWeight.bold)), | |
onPressed: () { | |
scaffoldKey.currentState!.openEndDrawer(); | |
}, | |
), | |
], | |
), | |
); | |
} | |
} | |
class NavigationDrawerSection extends StatefulWidget { | |
const NavigationDrawerSection({super.key}); | |
@override | |
State<NavigationDrawerSection> createState() => | |
_NavigationDrawerSectionState(); | |
} | |
class _NavigationDrawerSectionState extends State<NavigationDrawerSection> { | |
int navDrawerIndex = 0; | |
@override | |
Widget build(BuildContext context) { | |
return NavigationDrawer( | |
onDestinationSelected: (selectedIndex) { | |
setState(() { | |
navDrawerIndex = selectedIndex; | |
}); | |
}, | |
selectedIndex: navDrawerIndex, | |
children: <Widget>[ | |
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 Divider(indent: 28, endIndent: 28), | |
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<ExampleDestination> destinations = <ExampleDestination>[ | |
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<ExampleDestination> labelDestinations = <ExampleDestination>[ | |
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 NavigationRails extends StatelessWidget { | |
const NavigationRails({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const ComponentDecoration( | |
label: 'Navigation rail', | |
tooltipMessage: 'Use NavigationRail', | |
child: IntrinsicWidth( | |
child: SizedBox(height: 420, child: NavigationRailSection())), | |
); | |
} | |
} | |
class NavigationRailSection extends StatefulWidget { | |
const NavigationRailSection({super.key}); | |
@override | |
State<NavigationRailSection> createState() => _NavigationRailSectionState(); | |
} | |
class _NavigationRailSectionState extends State<NavigationRailSection> { | |
int navRailIndex = 0; | |
@override | |
Widget build(BuildContext context) { | |
return NavigationRail( | |
onDestinationSelected: (selectedIndex) { | |
setState(() { | |
navRailIndex = selectedIndex; | |
}); | |
}, | |
elevation: 4, | |
leading: FloatingActionButton( | |
child: const Icon(Icons.create), onPressed: () {}), | |
groupAlignment: 0.0, | |
selectedIndex: navRailIndex, | |
labelType: NavigationRailLabelType.selected, | |
destinations: <NavigationRailDestination>[ | |
...destinations.map((destination) { | |
return NavigationRailDestination( | |
label: Text(destination.label), | |
icon: destination.icon, | |
selectedIcon: destination.selectedIcon, | |
); | |
}), | |
], | |
); | |
} | |
} | |
class Tabs extends StatefulWidget { | |
const Tabs({super.key}); | |
@override | |
State<Tabs> createState() => _TabsState(); | |
} | |
class _TabsState extends State<Tabs> 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', | |
child: SizedBox( | |
height: 80, | |
child: Scaffold( | |
appBar: AppBar( | |
bottom: TabBar( | |
controller: _tabController, | |
tabs: const <Widget>[ | |
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), | |
), | |
], | |
), | |
// TODO: Showcase secondary tab bar https://github.com/flutter/flutter/issues/111962 | |
), | |
), | |
), | |
); | |
} | |
} | |
class TopAppBars extends StatelessWidget { | |
const TopAppBars({super.key}); | |
static final actions = [ | |
IconButton(icon: const Icon(Icons.attach_file), onPressed: () {}), | |
IconButton(icon: const Icon(Icons.event), onPressed: () {}), | |
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), | |
]; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Top app bars', | |
tooltipMessage: | |
'Use AppBar, SliverAppBar, SliverAppBar.medium, or SliverAppBar.large', | |
child: Column( | |
children: [ | |
AppBar( | |
title: const Text('Center-aligned'), | |
leading: const BackButton(), | |
actions: [ | |
IconButton( | |
iconSize: 32, | |
icon: const Icon(Icons.account_circle_outlined), | |
onPressed: () {}, | |
), | |
], | |
centerTitle: true, | |
), | |
colDivider, | |
AppBar( | |
title: const Text('Small'), | |
leading: const BackButton(), | |
actions: actions, | |
centerTitle: false, | |
), | |
colDivider, | |
SizedBox( | |
height: 100, | |
child: CustomScrollView( | |
slivers: [ | |
SliverAppBar.medium( | |
title: const Text('Medium'), | |
leading: const BackButton(), | |
actions: actions, | |
), | |
const SliverFillRemaining(), | |
], | |
), | |
), | |
colDivider, | |
SizedBox( | |
height: 130, | |
child: CustomScrollView( | |
slivers: [ | |
SliverAppBar.large( | |
title: const Text('Large'), | |
leading: const BackButton(), | |
actions: actions, | |
), | |
const SliverFillRemaining(), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class Menus extends StatefulWidget { | |
const Menus({super.key}); | |
@override | |
State<Menus> createState() => _MenusState(); | |
} | |
class _MenusState extends State<Menus> { | |
final TextEditingController colorController = TextEditingController(); | |
final TextEditingController iconController = TextEditingController(); | |
IconLabel? selectedIcon = IconLabel.smile; | |
ColorLabel? selectedColor; | |
@override | |
Widget build(BuildContext context) { | |
final List<DropdownMenuEntry<ColorLabel>> colorEntries = | |
<DropdownMenuEntry<ColorLabel>>[]; | |
for (final ColorLabel color in ColorLabel.values) { | |
colorEntries.add(DropdownMenuEntry<ColorLabel>( | |
value: color, label: color.label, enabled: color.label != 'Grey')); | |
} | |
final List<DropdownMenuEntry<IconLabel>> iconEntries = | |
<DropdownMenuEntry<IconLabel>>[]; | |
for (final IconLabel icon in IconLabel.values) { | |
iconEntries | |
.add(DropdownMenuEntry<IconLabel>(value: icon, label: icon.label)); | |
} | |
return ComponentDecoration( | |
label: 'Menus', | |
tooltipMessage: 'Use MenuAnchor or DropdownMenu<T>', | |
child: Column( | |
children: [ | |
const Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
ButtonAnchorExample(), | |
rowDivider, | |
IconButtonAnchorExample(), | |
], | |
), | |
colDivider, | |
Wrap( | |
alignment: WrapAlignment.spaceAround, | |
runAlignment: WrapAlignment.center, | |
crossAxisAlignment: WrapCrossAlignment.center, | |
spacing: smallSpacing, | |
runSpacing: smallSpacing, | |
children: [ | |
DropdownMenu<ColorLabel>( | |
controller: colorController, | |
label: const Text('Color'), | |
enableFilter: true, | |
dropdownMenuEntries: colorEntries, | |
inputDecorationTheme: const InputDecorationTheme(filled: true), | |
onSelected: (color) { | |
setState(() { | |
selectedColor = color; | |
}); | |
}, | |
), | |
DropdownMenu<IconLabel>( | |
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<Sliders> createState() => _SlidersState(); | |
} | |
class _SlidersState extends State<Sliders> { | |
double sliderValue0 = 30.0; | |
double sliderValue1 = 20.0; | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Sliders', | |
tooltipMessage: 'Use Slider or RangeSlider', | |
child: Column( | |
children: <Widget>[ | |
Slider( | |
max: 100, | |
value: sliderValue0, | |
onChanged: (value) { | |
setState(() { | |
sliderValue0 = value; | |
}); | |
}, | |
), | |
const SizedBox(height: 20), | |
Slider( | |
max: 100, | |
divisions: 5, | |
value: sliderValue1, | |
label: sliderValue1.round().toString(), | |
onChanged: (value) { | |
setState(() { | |
sliderValue1 = value; | |
}); | |
}, | |
), | |
], | |
)); | |
} | |
} | |
class SearchAnchors extends StatefulWidget { | |
const SearchAnchors({super.key}); | |
@override | |
State<SearchAnchors> createState() => _SearchAnchorsState(); | |
} | |
class _SearchAnchorsState extends State<SearchAnchors> { | |
String? selectedColor; | |
List<ColorItem> searchHistory = <ColorItem>[]; | |
Iterable<Widget> getHistoryList(SearchController controller) { | |
return searchHistory.map((color) => ListTile( | |
leading: const Icon(Icons.history), | |
title: Text(color.label), | |
trailing: IconButton( | |
icon: const Icon(Icons.call_missed), | |
onPressed: () { | |
controller.text = color.label; | |
controller.selection = | |
TextSelection.collapsed(offset: controller.text.length); | |
}), | |
onTap: () { | |
controller.closeView(color.label); | |
handleSelection(color); | |
}, | |
)); | |
} | |
Iterable<Widget> getSuggestions(SearchController controller) { | |
final String input = controller.value.text; | |
return ColorItem.values | |
.where((color) => color.label.contains(input)) | |
.map((filteredColor) => ListTile( | |
leading: CircleAvatar(backgroundColor: filteredColor.color), | |
title: Text(filteredColor.label), | |
trailing: IconButton( | |
icon: const Icon(Icons.call_missed), | |
onPressed: () { | |
controller.text = filteredColor.label; | |
controller.selection = | |
TextSelection.collapsed(offset: controller.text.length); | |
}), | |
onTap: () { | |
controller.closeView(filteredColor.label); | |
handleSelection(filteredColor); | |
}, | |
)); | |
} | |
void handleSelection(ColorItem color) { | |
setState(() { | |
selectedColor = color.label; | |
if (searchHistory.length >= 5) { | |
searchHistory.removeLast(); | |
} | |
searchHistory.insert(0, color); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return ComponentDecoration( | |
label: 'Search', | |
tooltipMessage: 'Use SearchAnchor or SearchAnchor.bar', | |
child: Column( | |
children: <Widget>[ | |
SearchAnchor.bar( | |
barHintText: 'Search colors', | |
suggestionsBuilder: (context, controller) { | |
if (controller.text.isEmpty) { | |
if (searchHistory.isNotEmpty) { | |
return getHistoryList(controller); | |
} | |
return <Widget>[ | |
const Center( | |
child: Text('No search history.', | |
style: TextStyle(color: Colors.grey)), | |
) | |
]; | |
} | |
return getSuggestions(controller); | |
}, | |
), | |
const SizedBox(height: 20), | |
if (selectedColor == null) | |
const Text('Select a color') | |
else | |
Text('Last selected color is $selectedColor') | |
], | |
), | |
); | |
} | |
} | |
class ComponentDecoration extends StatefulWidget { | |
const ComponentDecoration({ | |
super.key, | |
required this.label, | |
required this.child, | |
this.tooltipMessage = '', | |
}); | |
final String label; | |
final Widget child; | |
final String? tooltipMessage; | |
@override | |
State<ComponentDecoration> createState() => _ComponentDecorationState(); | |
} | |
class _ComponentDecorationState extends State<ComponentDecoration> { | |
final focusNode = FocusNode(); | |
@override | |
Widget build(BuildContext context) { | |
return RepaintBoundary( | |
child: Padding( | |
padding: const EdgeInsets.symmetric(vertical: smallSpacing), | |
child: Column( | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text(widget.label, | |
style: Theme.of(context).textTheme.titleSmall), | |
Tooltip( | |
message: widget.tooltipMessage, | |
child: const Padding( | |
padding: EdgeInsets.symmetric(horizontal: 5.0), | |
child: Icon(Icons.info_outline, size: 16)), | |
), | |
], | |
), | |
ConstrainedBox( | |
constraints: | |
const BoxConstraints.tightFor(width: widthConstraint), | |
// Tapping within the a component card should request focus | |
// for that component's children. | |
child: Focus( | |
focusNode: focusNode, | |
canRequestFocus: true, | |
child: GestureDetector( | |
onTapDown: (_) { | |
focusNode.requestFocus(); | |
}, | |
behavior: HitTestBehavior.opaque, | |
child: Card( | |
elevation: 0, | |
shape: RoundedRectangleBorder( | |
side: BorderSide( | |
color: Theme.of(context).colorScheme.outlineVariant, | |
), | |
borderRadius: const BorderRadius.all(Radius.circular(12)), | |
), | |
child: Padding( | |
padding: const EdgeInsets.symmetric( | |
horizontal: 5.0, vertical: 20.0), | |
child: Center( | |
child: widget.child, | |
), | |
), | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ComponentGroupDecoration extends StatelessWidget { | |
const ComponentGroupDecoration( | |
{super.key, required this.label, required this.children}); | |
final String label; | |
final List<Widget> children; | |
@override | |
Widget build(BuildContext context) { | |
// Fully traverse this component group before moving on | |
return FocusTraversalGroup( | |
child: Card( | |
margin: EdgeInsets.zero, | |
elevation: 0, | |
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), | |
child: Padding( | |
padding: const EdgeInsets.symmetric(vertical: 20.0), | |
child: Center( | |
child: Column( | |
children: [ | |
Text(label, style: Theme.of(context).textTheme.titleLarge), | |
colDivider, | |
...children | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
enum ColorItem { | |
red('red', Colors.red), | |
orange('orange', Colors.orange), | |
yellow('yellow', Colors.yellow), | |
green('green', Colors.green), | |
blue('blue', Colors.blue), | |
indigo('indigo', Colors.indigo), | |
violet('violet', Color(0xFF8F00FF)), | |
purple('purple', Colors.purple), | |
pink('pink', Colors.pink), | |
silver('silver', Color(0xFF808080)), | |
gold('gold', Color(0xFFFFD700)), | |
beige('beige', Color(0xFFF5F5DC)), | |
brown('brown', Colors.brown), | |
grey('grey', Colors.grey), | |
black('black', Colors.black), | |
white('white', Colors.white); | |
const ColorItem(this.label, this.color); | |
final String label; | |
final Color color; | |
} |