Flutter GDE Way To Code A Side Sheet
Easy way to implement side sheets and canonical layout panes that is not in the flutter SDK or the flutter docs.
In Material Design 3 Side Sheets that contain navigation drawers and pane widgets that contain canonical layout content are the same in that they are surface containers with surface color roles applied. But, once again, GDE's are not covering it, and it's not the Flutter SDK docs.
And that is why you should follow me or subscribe to my substack as I did deep and fill in the material design holes of the flutter SDK.
Material Design 3 Fine-Tuning
Usually, 8 months or so before the useMaterial3 flag is turned on by default in the Flutter SDK, usually the Material IO team fine-tunes the Material Design specification. In the Material Design 3 case, we got three different refinements:
-Surface Color Roles
-Side Sheets As Navigation Drawer Containers
-Canonical Layouts implying Pane Container Widgets
But what you have missed in reading the Material Design 3 specification is that the Canonical Pane Container Widget and the navigation drawer side sheet containers are the same exact widget that needs surface color roles applied. And with all material design fine-tuning that goes on in the Flutter world, the surface color roles are not yet fully in the Flutter SDK.
The trick I am showing is re-purposing the ElevationOverlay to compute the surface color role tint color and then use that construct to form a Pane widget that can take a child parameter that sets the correct surface color role.
The ElevationOverlay Surface Color Role Trick
First, we need a SurfaceColorEhum as our model:
// Copyright 2023 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
/// This enum represents the new surface colors introduced in Material Design 3.0. | |
/// | |
/// The different colors represent different tones and can be used to convey | |
/// hierarchy, depth, and focus. They can be used as background colors for | |
/// surfaces such as cards, dialog boxes, or containers. | |
/// | |
/// To select a surface color for a widget, use the `SurfaceColorEnum` values | |
/// with the `PaneContainerWidget`. The corresponding color is selected using | |
/// the `NewSurfaceTheme.getSurfaceColor` method and the current theme. | |
/// | |
/// For more information about the new surface colors, see the Material Design 3.0 | |
/// blog post: https://material.io/blog/tone-based-surface-color-m3 | |
enum SurfaceColorEnum { | |
/// The lowest tone color that can be used as a background for a surface. | |
surface, | |
/// A slightly higher tone color that can be used for low-emphasis surfaces. | |
surfaceContainerLowest, | |
/// A higher tone color that can be used for medium-emphasis surfaces. | |
surfaceContainerLow, | |
/// A higher tone color that can be used for high-emphasis surfaces. | |
surfaceContainer, | |
/// A higher tone color that can be used for elevated surfaces, such as dialogs. | |
surfaceContainerHigh, | |
/// The highest tone color that can be used as a background for a surface. | |
surfaceContainerHighest, | |
} |
And now we need a construct to use the ElevationOverlay:
// Copyright 2023 Fredrick Allan Grott. 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:custom_scaffold/surface_color_enum.dart'; | |
import 'package:flutter/material.dart'; | |
/// Provides methods for getting the appropriate surface color based on the selected | |
/// [SurfaceColorEnum] and the current [Theme] in [BuildContext]. | |
class NewSurfaceTheme { | |
/// Returns the surface color based on the [selectedColor] and the current [Theme] | |
/// in [context]. | |
/// | |
/// [SurfaceColorEnum.surface] returns the surface color from the current theme. | |
/// | |
/// [SurfaceColorEnum.surfaceContainerLowest] to [SurfaceColorEnum.surfaceContainerHigh] | |
/// return the surface color tinted based on the elevation, with increasing opacity. | |
/// | |
/// [SurfaceColorEnum.surfaceContainerHighest] returns the surface variant color from | |
/// the current theme. | |
static Color getSurfaceColor( | |
SurfaceColorEnum selectedColor, BuildContext context) { | |
switch (selectedColor) { | |
case SurfaceColorEnum.surface: | |
return Theme.of(context).colorScheme.surface; | |
case SurfaceColorEnum.surfaceContainerLowest: | |
return Theme.of(context).brightness == Brightness.dark | |
? Colors.black | |
: Colors.white; | |
case SurfaceColorEnum.surfaceContainerLow: | |
return ElevationOverlay.applySurfaceTint( | |
Theme.of(context).colorScheme.surface, | |
Theme.of(context).colorScheme.surfaceTint, | |
1, | |
); | |
case SurfaceColorEnum.surfaceContainer: | |
return ElevationOverlay.applySurfaceTint( | |
Theme.of(context).colorScheme.surface, | |
Theme.of(context).colorScheme.surfaceTint, | |
2, | |
); | |
case SurfaceColorEnum.surfaceContainerHigh: | |
return ElevationOverlay.applySurfaceTint( | |
Theme.of(context).colorScheme.surface, | |
Theme.of(context).colorScheme.surfaceTint, | |
3, | |
); | |
case SurfaceColorEnum.surfaceContainerHighest: | |
return Theme.of(context).colorScheme.surfaceVariant; | |
} | |
} | |
} |
Just a class with a getSurfaceColor method to use switch and case statements to use the ElevationOverlay applySurfaceTint method to compute the surface color role color. Remember, we have to do it this way as the surface color roles are not in the ColorScheme at this time. And, note the due to using BuildContext to retrieve Theme.of(context) to retrieve ColorScheme parameters it can only be applied in a widget.
Thus, let me now show the Pane widget that uses this trick.
Side Sheet And Canonical Pane Widget
Now, we not only need the surface color role color computed as part of this widget. Because, it is a container of either the navigation drawers or canonical layout content it needs width and height parameters that should default to double infinity, a border radius set, and require a child widget:
// Copyright 2023 Fredrick Allan Grott. 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:custom_scaffold/new_surface_theme.dart'; | |
import 'package:custom_scaffold/surface_color_enum.dart'; | |
import 'package:flutter/material.dart'; | |
/// The [PaneContainerWidget] is a wrapper widget for widgets that are inserted inside the | |
/// [PageLayout]. The container allows you to select the surface background color, container | |
/// dimensions, and border radius. | |
class PaneContainerWidget extends StatelessWidget { | |
/// The child widget to be wrapped with the container. | |
final Widget child; | |
/// The color of the surface of the container. Defaults to [SurfaceColorEnum.surface]. | |
final SurfaceColorEnum surfaceColor; | |
/// The padding for the container's child widget. Defaults to [EdgeInsets.zero]. | |
final EdgeInsetsGeometry padding; | |
/// The width of the container. Defaults to [double.infinity]. | |
final double width; | |
/// The height of the container. Defaults to [double.infinity]. | |
final double height; | |
/// The top border radius for the container. Defaults to 12. | |
final double topBorderRadius; | |
/// The bottom border radius for the container. Defaults to 12. | |
final double bottomBorderRadius; | |
/// The [PaneContainerWidget] is a wrapper widget for widgets that are inserted inside the | |
/// [PageLayout]. The container allows you to select the surface background color, container | |
/// dimensions, and border radius. | |
/// | |
/// Example of usage: | |
/// | |
/// ```dart | |
/// PaneContainerWidget( | |
/// child: YourWidget(), | |
/// surfaceColor: SurfaceColorEnum.primaryVariant, | |
/// padding: EdgeInsets.all(16), | |
/// ) | |
/// ``` | |
const PaneContainerWidget({ | |
Key? key, | |
required this.child, | |
this.surfaceColor = SurfaceColorEnum.surface, | |
this.padding = EdgeInsets.zero, | |
this.height = double.infinity, | |
this.width = double.infinity, | |
this.topBorderRadius = 12, | |
this.bottomBorderRadius = 12, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: padding, | |
child: Material( | |
borderRadius: BorderRadius.vertical( | |
top: Radius.circular(topBorderRadius), | |
bottom: Radius.circular(bottomBorderRadius), | |
), | |
color: NewSurfaceTheme.getSurfaceColor(surfaceColor, context), | |
child: SizedBox( | |
width: width, | |
height: height, | |
child: child, | |
), | |
), | |
); | |
} | |
} |
And to properly wrap the child widget along with a height and width, I should use a SizedBox widget. And, of course, rounding that out by correctly setting padding to EdgeInsets of zero. From the code samples of most Flutter GDE's it is what they would come up with if solving this problem.
Why Follow Me Or Subscribe To My Substack
One of the benefits of following me or subscribing to my Substack is that I go through and fill all the material design implementation holes in the Flutter SDK with new awesome widgets. And obviously, another needed widget for me to code over this weekend is an expanded card that does not add the extra scroll bars on mobile but correctly adjusts to each window size class of compact, medium, expanded, and evel.