diff --git a/lib/color_palettes_screen.dart b/lib/color_palettes_screen.dart new file mode 100644 index 0000000..db399f8 --- /dev/null +++ b/lib/color_palettes_screen.dart @@ -0,0 +1,339 @@ +// 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/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const Widget divider = SizedBox(height: 10); + +// If screen content width is greater or equal to this value, the light and dark +// color schemes will be displayed in a column. Otherwise, they will +// be displayed in a row. +const double narrowScreenWidthThreshold = 400; + +class ColorPalettesScreen extends StatelessWidget { + const ColorPalettesScreen({super.key}); + + @override + Widget build(BuildContext context) { + Color selectedColor = Theme.of(context).primaryColor; + ThemeData lightTheme = ThemeData( + colorSchemeSeed: selectedColor, + brightness: Brightness.light, + ); + ThemeData darkTheme = ThemeData( + colorSchemeSeed: selectedColor, + brightness: Brightness.dark, + ); + + Widget schemeLabel(String brightness) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Text( + brightness, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ); + } + + Widget schemeView(ThemeData theme) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: ColorSchemeView( + colorScheme: theme.colorScheme, + ), + ); + } + + Widget dynamicColorNotice() => RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodySmall, + children: [ + const TextSpan( + text: 'To create color schemes based on a ' + 'platform\'s implementation of dynamic color, ' + 'use the '), + TextSpan( + text: 'dynamic_color', + style: const TextStyle(decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer() + ..onTap = () async { + final url = Uri.parse( + 'https://pub.dev/packages/dynamic_color', + ); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + }, + ), + const TextSpan(text: ' package.'), + ], + ), + ); + + return Expanded( + child: LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < narrowScreenWidthThreshold) { + return SingleChildScrollView( + child: Column( + children: [ + dynamicColorNotice(), + divider, + schemeLabel('Light ColorScheme'), + schemeView(lightTheme), + divider, + divider, + schemeLabel('Dark ColorScheme'), + schemeView(darkTheme), + ], + ), + ); + } else { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 5), + child: Column( + children: [ + dynamicColorNotice(), + Row( + children: [ + Expanded( + child: Column( + children: [ + schemeLabel('Light ColorScheme'), + schemeView(lightTheme), + ], + ), + ), + Expanded( + child: Column( + children: [ + schemeLabel('Dark ColorScheme'), + schemeView(darkTheme), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + }), + ); + } +} + +class ColorSchemeView extends StatelessWidget { + const ColorSchemeView({super.key, required this.colorScheme}); + + final ColorScheme colorScheme; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ColorGroup(children: [ + ColorChip( + label: 'primary', + color: colorScheme.primary, + onColor: colorScheme.onPrimary, + ), + ColorChip( + label: 'onPrimary', + color: colorScheme.onPrimary, + onColor: colorScheme.primary), + ColorChip( + label: 'primaryContainer', + color: colorScheme.primaryContainer, + onColor: colorScheme.onPrimaryContainer, + ), + ColorChip( + label: 'onPrimaryContainer', + color: colorScheme.onPrimaryContainer, + onColor: colorScheme.primaryContainer, + ), + ]), + divider, + ColorGroup(children: [ + ColorChip( + label: 'secondary', + color: colorScheme.secondary, + onColor: colorScheme.onSecondary, + ), + ColorChip( + label: 'onSecondary', + color: colorScheme.onSecondary, + onColor: colorScheme.secondary, + ), + ColorChip( + label: 'secondaryContainer', + color: colorScheme.secondaryContainer, + onColor: colorScheme.onSecondaryContainer, + ), + ColorChip( + label: 'onSecondaryContainer', + color: colorScheme.onSecondaryContainer, + onColor: colorScheme.secondaryContainer), + ]), + divider, + ColorGroup( + children: [ + ColorChip( + label: 'tertiary', + color: colorScheme.tertiary, + onColor: colorScheme.onTertiary), + ColorChip( + label: 'onTertiary', + color: colorScheme.onTertiary, + onColor: colorScheme.tertiary), + ColorChip( + label: 'tertiaryContainer', + color: colorScheme.tertiaryContainer, + onColor: colorScheme.onTertiaryContainer), + ColorChip( + label: 'onTertiaryContainer', + color: colorScheme.onTertiaryContainer, + onColor: colorScheme.tertiaryContainer), + ], + ), + divider, + ColorGroup( + children: [ + ColorChip( + label: 'error', + color: colorScheme.error, + onColor: colorScheme.onError), + ColorChip( + label: 'onError', + color: colorScheme.onError, + onColor: colorScheme.error), + ColorChip( + label: 'errorContainer', + color: colorScheme.errorContainer, + onColor: colorScheme.onErrorContainer), + ColorChip( + label: 'onErrorContainer', + color: colorScheme.onErrorContainer, + onColor: colorScheme.errorContainer), + ], + ), + divider, + ColorGroup( + children: [ + ColorChip( + label: 'background', + color: colorScheme.background, + onColor: colorScheme.onBackground), + ColorChip( + label: 'onBackground', + color: colorScheme.onBackground, + onColor: colorScheme.background), + ], + ), + divider, + ColorGroup( + children: [ + ColorChip( + label: 'surface', + color: colorScheme.surface, + onColor: colorScheme.onSurface), + ColorChip( + label: 'onSurface', + color: colorScheme.onSurface, + onColor: colorScheme.surface), + ColorChip( + label: 'surfaceVariant', + color: colorScheme.surfaceVariant, + onColor: colorScheme.onSurfaceVariant), + ColorChip( + label: 'onSurfaceVariant', + color: colorScheme.onSurfaceVariant, + onColor: colorScheme.surfaceVariant), + ], + ), + divider, + ColorGroup( + children: [ + ColorChip(label: 'outline', color: colorScheme.outline), + ColorChip(label: 'shadow', color: colorScheme.shadow), + ColorChip( + label: 'inverseSurface', + color: colorScheme.inverseSurface, + onColor: colorScheme.onInverseSurface), + ColorChip( + label: 'onInverseSurface', + color: colorScheme.onInverseSurface, + onColor: colorScheme.inverseSurface), + ColorChip( + label: 'inversePrimary', + color: colorScheme.inversePrimary, + onColor: colorScheme.primary), + ], + ), + ], + ); + } +} + +class ColorGroup extends StatelessWidget { + const ColorGroup({super.key, required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: children, + ), + ), + ); + } +} + +class ColorChip extends StatelessWidget { + const ColorChip({ + super.key, + required this.color, + required this.label, + this.onColor, + }); + + final Color color; + final Color? onColor; + final String label; + + static Color contrastColor(Color color) { + final brightness = ThemeData.estimateBrightnessForColor(color); + switch (brightness) { + case Brightness.dark: + return Colors.white; + case Brightness.light: + return Colors.black; + } + } + + @override + Widget build(BuildContext context) { + final Color labelColor = onColor ?? contrastColor(color); + + return Container( + color: color, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded(child: Text(label, style: TextStyle(color: labelColor))), + ], + ), + ), + ); + } +} diff --git a/lib/component_screen.dart b/lib/component_screen.dart new file mode 100644 index 0000000..b6d07da --- /dev/null +++ b/lib/component_screen.dart @@ -0,0 +1,2554 @@ +// 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 scaffoldKey; + final bool showSecondList; + + @override + Widget build(BuildContext context) { + List children = [ + const Actions(), + colDivider, + const Communication(), + colDivider, + const Containment(), + if (!showSecondList) ...[ + colDivider, + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs() + ], + ]; + List 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 scaffoldKey; + + @override + Widget build(BuildContext context) { + List children = [ + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs(), + ]; + List 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 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 heights, + required int index, + }) : _heights = heights, + _index = index, + super(); + + List _heights; + List get heights => _heights; + set heights(List 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 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: [ + 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 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 createState() => _ButtonsState(); +} + +class _ButtonsState extends State { + @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: [ + 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: [ + 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: [ + 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 createState() => _TextFieldsState(); +} + +class _TextFieldsState extends State { + 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 createState() => _DialogsState(); +} + +class _DialogsState extends State { + void openDialog(BuildContext context) { + showDialog( + 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: [ + TextButton( + child: const Text('Dismiss'), + onPressed: () => Navigator.of(context).pop(), + ), + FilledButton( + child: const Text('Okay'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } + + void openFullscreenDialog(BuildContext context) { + showDialog( + 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: [ + 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: [ + SwitchRow(isEnabled: true), + SwitchRow(isEnabled: false), + ], + ), + ); + } +} + +class SwitchRow extends StatefulWidget { + const SwitchRow({super.key, required this.isEnabled}); + + final bool isEnabled; + + @override + State createState() => _SwitchRowState(); +} + +class _SwitchRowState extends State { + bool value0 = false; + bool value1 = true; + + final MaterialStateProperty thumbIcon = + MaterialStateProperty.resolveWith((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: [ + // 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 createState() => _CheckboxesState(); +} + +class _CheckboxesState extends State { + 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: [ + 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 createState() => _RadiosState(); +} + +enum Options { option1, option2, option3 } + +class _RadiosState extends State { + Options? _selectedOption = Options.option1; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Radio buttons', + tooltipMessage: 'Use RadioListTile or Radio', + child: Column( + children: [ + RadioListTile( + title: const Text('Option 1'), + value: Options.option1, + groupValue: _selectedOption, + onChanged: (value) { + setState(() { + _selectedOption = value; + }); + }, + ), + RadioListTile( + title: const Text('Option 2'), + value: Options.option2, + groupValue: _selectedOption, + onChanged: (value) { + setState(() { + _selectedOption = value; + }); + }, + ), + RadioListTile( + title: const Text('Option 3'), + value: Options.option3, + groupValue: _selectedOption, + onChanged: null, + ), + ], + ), + ); + } +} + +class ProgressIndicators extends StatefulWidget { + const ProgressIndicators({super.key}); + + @override + State createState() => _ProgressIndicatorsState(); +} + +class _ProgressIndicatorsState extends State { + 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: [ + Row( + children: [ + IconButton( + isSelected: playProgressIndicator, + selectedIcon: const Icon(Icons.pause), + icon: const Icon(Icons.play_arrow), + onPressed: () { + setState(() { + playProgressIndicator = !playProgressIndicator; + }); + }, + ), + Expanded( + child: Row( + children: [ + rowDivider, + CircularProgressIndicator( + value: progressValue, + ), + rowDivider, + Expanded( + child: LinearProgressIndicator( + value: progressValue, + ), + ), + rowDivider, + ], + ), + ), + ], + ), + ], + ), + ); + } +} + +const List 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), + ), + NavigationDestination( + tooltip: '', + icon: Icon(Icons.invert_colors_on_outlined), + label: 'Home', + selectedIcon: Icon(Icons.opacity), + ) +]; + +const List 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 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 createState() => _NavigationBarsState(); +} + +class _NavigationBarsState extends State { + 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 createState() => _IconToggleButtonsState(); +} + +class _IconToggleButtonsState extends State { + 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: [ + Column( + // Standard IconButton + children: [ + 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: [ + // 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: [ + // 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: [ + // 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 createState() => _ChipsState(); +} + +class _ChipsState extends State { + 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: [ + Wrap( + spacing: smallSpacing, + runSpacing: smallSpacing, + children: [ + 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: [ + 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 createState() => _DatePickersState(); +} + +class _DatePickersState extends State { + 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 createState() => _TimePickersState(); +} + +class _TimePickersState extends State { + 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', + child: Column( + children: [ + 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 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 createState() => _BottomSheetSectionState(); +} + +class _BottomSheetSectionState extends State { + bool isNonModalBottomSheetOpen = false; + PersistentBottomSheetController? _nonModalBottomSheetController; + + @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('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 or showBottomSheet', + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text( + 'Show modal bottom sheet', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () { + showModalBottomSheet( + 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( + 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: [ + 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: [ + 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 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 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 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 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 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 createState() => _NavigationRailSectionState(); +} + +class _NavigationRailSectionState extends State { + 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: [ + ...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 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', + 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), + ), + ], + ), + // 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 createState() => _MenusState(); +} + +class _MenusState 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: 'Menus', + tooltipMessage: 'Use MenuAnchor or DropdownMenu', + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonAnchorExample(), + rowDivider, + IconButtonAnchorExample(), + ], + ), + colDivider, + Wrap( + alignment: WrapAlignment.spaceAround, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: smallSpacing, + runSpacing: smallSpacing, + 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 or RangeSlider', + child: Column( + children: [ + 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 createState() => _SearchAnchorsState(); +} + +class _SearchAnchorsState extends State { + String? selectedColor; + List searchHistory = []; + + Iterable 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 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: [ + SearchAnchor.bar( + barHintText: 'Search colors', + suggestionsBuilder: (context, controller) { + if (controller.text.isEmpty) { + if (searchHistory.isNotEmpty) { + return getHistoryList(controller); + } + return [ + 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 createState() => _ComponentDecorationState(); +} + +class _ComponentDecorationState extends State { + 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 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; +} diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..eef34bb --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,67 @@ +// 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'; + +// NavigationRail shows if the screen width is greater or equal to +// narrowScreenWidthThreshold; otherwise, NavigationBar is used for navigation. +const double narrowScreenWidthThreshold = 450; + +const double mediumWidthBreakpoint = 1000; +const double largeWidthBreakpoint = 1500; + +const double transitionLength = 500; + +// Whether the user has chosen a theme color via a direct [ColorSeed] selection, +// or an image [ColorImageProvider]. +enum ColorSelectionMethod { + colorSeed, + image, +} + +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 ColorImageProvider { + leaves('Leaves', + 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_1.png'), + peonies('Peonies', + 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_2.png'), + bubbles('Bubbles', + 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_3.png'), + seaweed('Seaweed', + 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_4.png'), + seagrapes('Sea Grapes', + 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_5.png'), + petals('Petals', + 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_6.png'); + + const ColorImageProvider(this.label, this.url); + final String label; + final String url; +} + +enum ScreenSelected { + component(0), + color(1), + typography(2), + elevation(3), + home(4); + + const ScreenSelected(this.value); + final int value; +} diff --git a/lib/elevation_screen.dart b/lib/elevation_screen.dart new file mode 100644 index 0000000..6522e9b --- /dev/null +++ b/lib/elevation_screen.dart @@ -0,0 +1,187 @@ +// 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'; + +class ElevationScreen extends StatelessWidget { + const ElevationScreen({super.key}); + + @override + Widget build(BuildContext context) { + Color shadowColor = Theme.of(context).colorScheme.shadow; + Color surfaceTint = Theme.of(context).colorScheme.primary; + return Expanded( + 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, + shadowColor: Colors.transparent, + ), + 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, + ), + 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), + ], + ), + ); + } +} + +const double narrowScreenWidthThreshold = 450; + +class ElevationGrid extends StatelessWidget { + const ElevationGrid({super.key, this.shadowColor, this.surfaceTintColor}); + + final Color? shadowColor; + final Color? surfaceTintColor; + + List elevationCards( + Color? shadowColor, Color? surfaceTintColor) { + return elevations + .map( + (elevationInfo) => ElevationCard( + info: elevationInfo, + shadowColor: shadowColor, + surfaceTint: surfaceTintColor, + ), + ) + .toList(); + } + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.all(8), + sliver: SliverLayoutBuilder(builder: (context, constraints) { + if (constraints.crossAxisExtent < narrowScreenWidthThreshold) { + return SliverGrid.count( + crossAxisCount: 3, + children: elevationCards(shadowColor, surfaceTintColor), + ); + } else { + return SliverGrid.count( + crossAxisCount: 6, + children: elevationCards(shadowColor, surfaceTintColor), + ); + } + }), + ); + } +} + +class ElevationCard extends StatefulWidget { + const ElevationCard( + {super.key, required this.info, this.shadowColor, this.surfaceTint}); + + final ElevationInfo info; + final Color? shadowColor; + final Color? surfaceTint; + + @override + State createState() => _ElevationCardState(); +} + +class _ElevationCardState extends State { + late double _elevation; + + @override + void initState() { + super.initState(); + _elevation = widget.info.elevation; + } + + @override + Widget build(BuildContext context) { + const BorderRadius borderRadius = BorderRadius.all(Radius.circular(4.0)); + final Color color = Theme.of(context).colorScheme.surface; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + borderRadius: borderRadius, + elevation: _elevation, + color: color, + shadowColor: widget.shadowColor, + surfaceTintColor: widget.surfaceTint, + type: MaterialType.card, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Level ${widget.info.level}', + style: Theme.of(context).textTheme.labelMedium, + ), + Text( + '${widget.info.elevation.toInt()} dp', + style: Theme.of(context).textTheme.labelMedium, + ), + if (widget.surfaceTint != null) + Expanded( + child: Align( + alignment: Alignment.bottomRight, + child: Text( + '${widget.info.overlayPercent}%', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class ElevationInfo { + const ElevationInfo(this.level, this.elevation, this.overlayPercent); + final int level; + final double elevation; + final int overlayPercent; +} + +const List elevations = [ + ElevationInfo(0, 0.0, 0), + ElevationInfo(1, 1.0, 5), + ElevationInfo(2, 3.0, 8), + ElevationInfo(3, 6.0, 11), + ElevationInfo(4, 8.0, 12), + ElevationInfo(5, 12.0, 14), +]; diff --git a/lib/home.dart b/lib/home.dart new file mode 100644 index 0000000..518cded --- /dev/null +++ b/lib/home.dart @@ -0,0 +1,877 @@ +// 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 'color_palettes_screen.dart'; +import 'component_screen.dart'; +import 'constants.dart'; +import 'elevation_screen.dart'; +import 'typography_screen.dart'; +import 'pages/home_page.dart'; + +class Home extends StatefulWidget { + const Home({ + super.key, + required this.useLightMode, + required this.useMaterial3, + required this.colorSelected, + required this.handleBrightnessChange, + required this.handleMaterialVersionChange, + required this.handleColorSelect, + required this.handleImageSelect, + required this.colorSelectionMethod, + required this.imageSelected, + }); + + final bool useLightMode; + final bool useMaterial3; + final ColorSeed colorSelected; + final ColorImageProvider imageSelected; + final ColorSelectionMethod colorSelectionMethod; + + final void Function(bool useLightMode) handleBrightnessChange; + final void Function() handleMaterialVersionChange; + final void Function(int value) handleColorSelect; + final void Function(int value) handleImageSelect; + + @override + State createState() => _HomeState(); +} + +class _HomeState 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; + + int screenIndex = ScreenSelected.component.value; + + @override + initState() { + super.initState(); + 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(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final double width = MediaQuery.of(context).size.width; + final AnimationStatus status = controller.status; + if (width > mediumWidthBreakpoint) { + if (width > largeWidthBreakpoint) { + 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 > mediumWidthBreakpoint ? 1 : 0; + } + } + + void handleScreenChanged(int screenSelected) { + setState(() { + screenIndex = screenSelected; + }); + } + + Widget createScreenFor( + ScreenSelected screenSelected, bool showNavBarExample) { + switch (screenSelected) { + case ScreenSelected.component: + return Expanded( + child: OneTwoTransition( + animation: railAnimation, + one: FirstComponentList( + showNavBottomBar: showNavBarExample, + scaffoldKey: scaffoldKey, + showSecondList: showMediumSizeLayout || showLargeSizeLayout), + two: SecondComponentList( + scaffoldKey: scaffoldKey, + ), + ), + ); + case ScreenSelected.color: + return const ColorPalettesScreen(); + case ScreenSelected.typography: + return const TypographyScreen(); + case ScreenSelected.elevation: + return const ElevationScreen(); + case ScreenSelected.home: + return const HomePage(); + } + } + + PreferredSizeWidget createAppBar() { + return AppBar( + title: widget.useMaterial3 + ? const Text('Material 3') + : const Text('Material 2'), + actions: !showMediumSizeLayout && !showLargeSizeLayout + ? [ + _BrightnessButton( + handleBrightnessChange: widget.handleBrightnessChange, + ), + _Material3Button( + handleMaterialVersionChange: widget.handleMaterialVersionChange, + ), + _ColorSeedButton( + handleColorSelect: widget.handleColorSelect, + colorSelected: widget.colorSelected, + colorSelectionMethod: widget.colorSelectionMethod, + ), + _ColorImageButton( + handleImageSelect: widget.handleImageSelect, + imageSelected: widget.imageSelected, + colorSelectionMethod: widget.colorSelectionMethod, + ) + ] + : [Container()], + ); + } + + Widget _trailingActions() => Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: _BrightnessButton( + handleBrightnessChange: widget.handleBrightnessChange, + showTooltipBelow: false, + ), + ), + Flexible( + child: _Material3Button( + handleMaterialVersionChange: widget.handleMaterialVersionChange, + showTooltipBelow: false, + ), + ), + Flexible( + child: _ColorSeedButton( + handleColorSelect: widget.handleColorSelect, + colorSelected: widget.colorSelected, + colorSelectionMethod: widget.colorSelectionMethod, + ), + ), + Flexible( + child: _ColorImageButton( + handleImageSelect: widget.handleImageSelect, + imageSelected: widget.imageSelected, + colorSelectionMethod: widget.colorSelectionMethod, + ), + ), + ], + ); + + @override + Widget build(BuildContext context) { + return 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( + useLightMode: widget.useLightMode, + handleBrightnessChange: widget.handleBrightnessChange, + useMaterial3: widget.useMaterial3, + handleMaterialVersionChange: + widget.handleMaterialVersionChange, + handleImageSelect: widget.handleImageSelect, + handleColorSelect: widget.handleColorSelect, + colorSelectionMethod: widget.colorSelectionMethod, + imageSelected: widget.imageSelected, + colorSelected: widget.colorSelected, + ) + : _trailingActions(), + ), + ), + ), + navigationBar: NavigationBars( + onSelectItem: (index) { + setState(() { + screenIndex = index; + handleScreenChanged(screenIndex); + }); + }, + selectedIndex: screenIndex, + isExampleBar: false, + ), + ); + }, + ); + } +} + +class _BrightnessButton extends StatelessWidget { + const _BrightnessButton({ + required this.handleBrightnessChange, + this.showTooltipBelow = true, + }); + + final Function handleBrightnessChange; + final bool showTooltipBelow; + + @override + Widget build(BuildContext context) { + final isBright = Theme.of(context).brightness == Brightness.light; + return Tooltip( + preferBelow: showTooltipBelow, + message: 'Toggle brightness', + child: IconButton( + icon: isBright + ? const Icon(Icons.dark_mode_outlined) + : const Icon(Icons.light_mode_outlined), + onPressed: () => handleBrightnessChange(!isBright), + ), + ); + } +} + +class _Material3Button extends StatelessWidget { + const _Material3Button({ + required this.handleMaterialVersionChange, + this.showTooltipBelow = true, + }); + + final void Function() handleMaterialVersionChange; + final bool showTooltipBelow; + + @override + Widget build(BuildContext context) { + final useMaterial3 = Theme.of(context).useMaterial3; + return Tooltip( + preferBelow: showTooltipBelow, + message: 'Switch to Material ${useMaterial3 ? 2 : 3}', + child: IconButton( + icon: useMaterial3 + ? const Icon(Icons.filter_2) + : const Icon(Icons.filter_3), + onPressed: handleMaterialVersionChange, + ), + ); + } +} + +class _ColorSeedButton extends StatelessWidget { + const _ColorSeedButton({ + required this.handleColorSelect, + required this.colorSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleColorSelect; + final ColorSeed colorSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon( + Icons.palette_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + tooltip: 'Select a seed color', + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + itemBuilder: (context) { + return List.generate(ColorSeed.values.length, (index) { + ColorSeed currentColor = ColorSeed.values[index]; + + return PopupMenuItem( + value: index, + enabled: currentColor != colorSelected || + colorSelectionMethod != ColorSelectionMethod.colorSeed, + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Icon( + currentColor == colorSelected && + colorSelectionMethod != ColorSelectionMethod.image + ? Icons.color_lens + : Icons.color_lens_outlined, + color: currentColor.color, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text(currentColor.label), + ), + ], + ), + ); + }); + }, + onSelected: handleColorSelect, + ); + } +} + +class _ColorImageButton extends StatelessWidget { + const _ColorImageButton({ + required this.handleImageSelect, + required this.imageSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleImageSelect; + final ColorImageProvider imageSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon( + Icons.image_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + tooltip: 'Select a color extraction image', + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + itemBuilder: (context) { + return List.generate(ColorImageProvider.values.length, (index) { + ColorImageProvider currentImageProvider = + ColorImageProvider.values[index]; + + return PopupMenuItem( + value: index, + enabled: currentImageProvider != imageSelected || + colorSelectionMethod != ColorSelectionMethod.image, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 48), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image( + image: NetworkImage( + ColorImageProvider.values[index].url), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text(currentImageProvider.label), + ), + ], + ), + ); + }); + }, + onSelected: handleImageSelect, + ); + } +} + +class _ExpandedTrailingActions extends StatelessWidget { + const _ExpandedTrailingActions({ + required this.useLightMode, + required this.handleBrightnessChange, + required this.useMaterial3, + required this.handleMaterialVersionChange, + required this.handleColorSelect, + required this.handleImageSelect, + required this.imageSelected, + required this.colorSelected, + required this.colorSelectionMethod, + }); + + final void Function(bool) handleBrightnessChange; + final void Function() handleMaterialVersionChange; + final void Function(int) handleImageSelect; + final void Function(int) handleColorSelect; + + final bool useLightMode; + final bool useMaterial3; + + final ColorImageProvider imageSelected; + final ColorSeed colorSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final trailingActionsBody = 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: (value) { + handleBrightnessChange(value); + }) + ], + ), + Row( + children: [ + useMaterial3 + ? const Text('Material 3') + : const Text('Material 2'), + Expanded(child: Container()), + Switch( + value: useMaterial3, + onChanged: (_) { + handleMaterialVersionChange(); + }) + ], + ), + const Divider(), + _ExpandedColorSeedAction( + handleColorSelect: handleColorSelect, + colorSelected: colorSelected, + colorSelectionMethod: colorSelectionMethod, + ), + const Divider(), + _ExpandedImageColorAction( + handleImageSelect: handleImageSelect, + imageSelected: imageSelected, + colorSelectionMethod: colorSelectionMethod, + ), + ], + ), + ); + return screenHeight > 740 + ? trailingActionsBody + : SingleChildScrollView(child: trailingActionsBody); + } +} + +class _ExpandedColorSeedAction extends StatelessWidget { + const _ExpandedColorSeedAction({ + required this.handleColorSelect, + required this.colorSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleColorSelect; + final ColorSeed colorSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200.0), + child: GridView.count( + crossAxisCount: 3, + children: List.generate( + ColorSeed.values.length, + (i) => IconButton( + icon: const Icon(Icons.radio_button_unchecked), + color: ColorSeed.values[i].color, + isSelected: colorSelected.color == ColorSeed.values[i].color && + colorSelectionMethod == ColorSelectionMethod.colorSeed, + selectedIcon: const Icon(Icons.circle), + onPressed: () { + handleColorSelect(i); + }, + tooltip: ColorSeed.values[i].label, + ), + ), + ), + ); + } +} + +class _ExpandedImageColorAction extends StatelessWidget { + const _ExpandedImageColorAction({ + required this.handleImageSelect, + required this.imageSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleImageSelect; + final ColorImageProvider imageSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 150.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: GridView.count( + crossAxisCount: 3, + children: List.generate( + ColorImageProvider.values.length, + (i) => Tooltip( + message: ColorImageProvider.values[i].name, + child: InkWell( + borderRadius: BorderRadius.circular(4.0), + onTap: () => handleImageSelect(i), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + borderRadius: BorderRadius.circular(4.0), + elevation: imageSelected == ColorImageProvider.values[i] && + colorSelectionMethod == ColorSelectionMethod.image + ? 3 + : 0, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(4.0), + child: Image( + image: NetworkImage(ColorImageProvider.values[i].url), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +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, + ), + 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: mediumWidthBreakpoint, + ).animate(SizeAnimation(widget.animation)); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + flex: mediumWidthBreakpoint.toInt(), + child: widget.one, + ), + if (widthAnimation.value.toInt() > 0) ...[ + Flexible( + flex: widthAnimation.value.toInt(), + child: FractionalTranslation( + translation: offsetAnimation.value, + child: widget.two, + ), + ) + ], + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 752801b..5a0ec56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,44 +1,110 @@ +// 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/material.dart'; -import 'pages/welcome_page.dart'; -import 'pages/home_page.dart'; + +import 'constants.dart'; +import 'home.dart'; void main() { - runApp(const MyApp()); + runApp( + const App(), + ); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + bool useMaterial3 = true; + ThemeMode themeMode = ThemeMode.system; + ColorSeed colorSelected = ColorSeed.baseColor; + ColorImageProvider imageSelected = ColorImageProvider.leaves; + ColorScheme? imageColorScheme = const ColorScheme.light(); + ColorSelectionMethod colorSelectionMethod = ColorSelectionMethod.colorSeed; + + bool get useLightMode { + switch (themeMode) { + case ThemeMode.system: + return View.of(context).platformDispatcher.platformBrightness == + Brightness.light; + case ThemeMode.light: + return true; + case ThemeMode.dark: + return false; + } + } + + void handleBrightnessChange(bool useLightMode) { + setState(() { + themeMode = useLightMode ? ThemeMode.light : ThemeMode.dark; + }); + } + + void handleMaterialVersionChange() { + setState(() { + useMaterial3 = !useMaterial3; + }); + } + + void handleColorSelect(int value) { + setState(() { + colorSelectionMethod = ColorSelectionMethod.colorSeed; + colorSelected = ColorSeed.values[value]; + }); + } + + void handleImageSelect(int value) { + final String url = ColorImageProvider.values[value].url; + ColorScheme.fromImageProvider(provider: NetworkImage(url)) + .then((newScheme) { + setState(() { + colorSelectionMethod = ColorSelectionMethod.image; + imageSelected = ColorImageProvider.values[value]; + imageColorScheme = newScheme; + }); + }); + } - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + title: 'Material 3', + themeMode: themeMode, theme: ThemeData( - primaryColor: Colors.red, - accentCoulor: Colors.blue, - fontFamily: 'Roboto', - textTheme: TextTheme( - headline1: TextStyle(fontSize: 24), - headline2: TextStyle(fontSize: 20), - headline3: TextStyle(fontSize: 16), - )), - // Cambia la proprietà `home` con `WelcomePage()` - home: WelcomePage(), - // Aggiungi un nuovo `onGenerateRoute` - onGenerateRoute: (settings) { - // Se la route è `/home`, esci dal `WelcomePage()` e vai alla `HomePage()` - if (settings.name == '/home') { - Navigator.pop(context); - return MaterialPageRoute( - builder: (context) => const HomePage( - title: 'Titolo', - )); - } - // Altrimenti, restituisci la route predefinita - return MaterialPageRoute(builder: (context) => WelcomePage()); - }, + colorSchemeSeed: colorSelectionMethod == ColorSelectionMethod.colorSeed + ? colorSelected.color + : null, + colorScheme: colorSelectionMethod == ColorSelectionMethod.image + ? imageColorScheme + : null, + useMaterial3: useMaterial3, + brightness: Brightness.light, + ), + darkTheme: ThemeData( + colorSchemeSeed: colorSelectionMethod == ColorSelectionMethod.colorSeed + ? colorSelected.color + : imageColorScheme!.primary, + useMaterial3: useMaterial3, + brightness: Brightness.dark, + ), + home: Home( + useLightMode: useLightMode, + useMaterial3: useMaterial3, + colorSelected: colorSelected, + imageSelected: imageSelected, + handleBrightnessChange: handleBrightnessChange, + handleMaterialVersionChange: handleMaterialVersionChange, + handleColorSelect: handleColorSelect, + handleImageSelect: handleImageSelect, + colorSelectionMethod: colorSelectionMethod, + ), ); } } diff --git a/lib/firebase_options.dart b/lib/old/firebase_options.dart similarity index 100% rename from lib/firebase_options.dart rename to lib/old/firebase_options.dart diff --git a/lib/old/main.dart b/lib/old/main.dart new file mode 100644 index 0000000..03d2d95 --- /dev/null +++ b/lib/old/main.dart @@ -0,0 +1,45 @@ +/* +import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; +import '../pages/welcome_page.dart'; +import '../pages/home_page.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primaryColor: Colors.red, + accentCoulor: Colors.blue, + fontFamily: 'Roboto', + textTheme: TextTheme( + headline1: TextStyle(fontSize: 24), + headline2: TextStyle(fontSize: 20), + headline3: TextStyle(fontSize: 16), + )), + // Cambia la proprietà `home` con `WelcomePage()` + home: WelcomePage(), + // Aggiungi un nuovo `onGenerateRoute` + onGenerateRoute: (settings) { + // Se la route è `/home`, esci dal `WelcomePage()` e vai alla `HomePage()` + if (settings.name == '/home') { + Navigator.pop(context); + return MaterialPageRoute( + builder: (context) => const HomePage( + title: 'Titolo', + )); + } + // Altrimenti, restituisci la route predefinita + return MaterialPageRoute(builder: (context) => WelcomePage()); + }, + ); + } + } */ \ No newline at end of file diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ef12025..29d7e2d 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,89 +1,24 @@ import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { - const HomePage({super.key, required this.title}); + const HomePage({super.key}); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; + final String title = "home page"; @override State createState() => _HomePageState(); } class _HomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the HomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), + title: Text('Hello world'), ), body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), + child: Text('Hello world!'), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. ); } } diff --git a/lib/typography_screen.dart b/lib/typography_screen.dart new file mode 100644 index 0000000..6950e95 --- /dev/null +++ b/lib/typography_screen.dart @@ -0,0 +1,63 @@ +// 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'; + +class TypographyScreen extends StatelessWidget { + const TypographyScreen({super.key}); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context) + .textTheme + .apply(displayColor: Theme.of(context).colorScheme.onSurface); + return Expanded( + child: ListView( + children: [ + const SizedBox(height: 7), + TextStyleExample( + name: 'Display Large', style: textTheme.displayLarge!), + TextStyleExample( + name: 'Display Medium', style: textTheme.displayMedium!), + TextStyleExample( + name: 'Display Small', style: textTheme.displaySmall!), + TextStyleExample( + name: 'Headline Large', style: textTheme.headlineLarge!), + TextStyleExample( + name: 'Headline Medium', style: textTheme.headlineMedium!), + TextStyleExample( + name: 'Headline Small', style: textTheme.headlineSmall!), + TextStyleExample(name: 'Title Large', style: textTheme.titleLarge!), + TextStyleExample(name: 'Title Medium', style: textTheme.titleMedium!), + TextStyleExample(name: 'Title Small', style: textTheme.titleSmall!), + TextStyleExample(name: 'Label Large', style: textTheme.labelLarge!), + TextStyleExample(name: 'Label Medium', style: textTheme.labelMedium!), + TextStyleExample(name: 'Label Small', style: textTheme.labelSmall!), + TextStyleExample(name: 'Body Large', style: textTheme.bodyLarge!), + TextStyleExample(name: 'Body Medium', style: textTheme.bodyMedium!), + TextStyleExample(name: 'Body Small', style: textTheme.bodySmall!), + ], + ), + ); + } +} + +class TextStyleExample extends StatelessWidget { + const TextStyleExample({ + super.key, + required this.name, + required this.style, + }); + + final String name; + final TextStyle style; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text(name, style: style), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 84d5e35..911c217 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 firebase_core: ^2.16.0 + url_launcher: ^6.1.14 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 5e5d15e..8e2fba4 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:progetto_m335_flutter/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const App()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);