Add support for surface tint color overlays to Material
widget. (#100036)
This commit is contained in:
parent
e99a66a47e
commit
6ec0b83580
@ -21,6 +21,7 @@ import 'package:gen_defaults/dialog_template.dart';
|
||||
import 'package:gen_defaults/fab_template.dart';
|
||||
import 'package:gen_defaults/navigation_bar_template.dart';
|
||||
import 'package:gen_defaults/navigation_rail_template.dart';
|
||||
import 'package:gen_defaults/surface_tint.dart';
|
||||
import 'package:gen_defaults/typography_template.dart';
|
||||
|
||||
Map<String, dynamic> _readTokenFile(String fileName) {
|
||||
@ -70,9 +71,10 @@ Future<void> main(List<String> args) async {
|
||||
tokens['colorsLight'] = _readTokenFile('color_light.json');
|
||||
tokens['colorsDark'] = _readTokenFile('color_dark.json');
|
||||
|
||||
DialogTemplate('$materialLib/dialog.dart', tokens).updateFile();
|
||||
FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile();
|
||||
NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile();
|
||||
NavigationRailTemplate('$materialLib/navigation_rail.dart', tokens).updateFile();
|
||||
SurfaceTintTemplate('$materialLib/elevation_overlay.dart', tokens).updateFile();
|
||||
TypographyTemplate('$materialLib/typography.dart', tokens).updateFile();
|
||||
DialogTemplate('$materialLib/dialog.dart', tokens).updateFile();
|
||||
}
|
||||
|
27
dev/tools/gen_defaults/lib/surface_tint.dart
Normal file
27
dev/tools/gen_defaults/lib/surface_tint.dart
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'template.dart';
|
||||
|
||||
class SurfaceTintTemplate extends TokenTemplate {
|
||||
const SurfaceTintTemplate(String fileName, Map<String, dynamic> tokens) : super(fileName, tokens);
|
||||
|
||||
@override
|
||||
String generate() => '''
|
||||
// Generated version ${tokens["version"]}
|
||||
|
||||
// Surface tint opacities based on elevations according to the
|
||||
// Material Design 3 specification:
|
||||
// https://m3.material.io/styles/color/the-color-system/color-roles
|
||||
// Ordered by increasing elevation.
|
||||
const List<_ElevationOpacity> _surfaceTintElevationOpacities = <_ElevationOpacity>[
|
||||
_ElevationOpacity(${tokens['md.sys.elevation.level0']}, 0.0), // Elevation level 0
|
||||
_ElevationOpacity(${tokens['md.sys.elevation.level1']}, 0.05), // Elevation level 1
|
||||
_ElevationOpacity(${tokens['md.sys.elevation.level2']}, 0.08), // Elevation level 2
|
||||
_ElevationOpacity(${tokens['md.sys.elevation.level3']}, 0.11), // Elevation level 3
|
||||
_ElevationOpacity(${tokens['md.sys.elevation.level4']}, 0.12), // Elevation level 4
|
||||
_ElevationOpacity(${tokens['md.sys.elevation.level5']}, 0.14), // Elevation level 5
|
||||
];
|
||||
''';
|
||||
}
|
@ -381,6 +381,8 @@ class _RawMaterialButtonState extends State<RawMaterialButton> with MaterialStat
|
||||
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
|
||||
shape: effectiveShape,
|
||||
color: widget.fillColor,
|
||||
// For compatibility during the M3 migration the default shadow needs to be passed.
|
||||
shadowColor: Theme.of(context).useMaterial3 ? Theme.of(context).shadowColor : null,
|
||||
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
|
||||
animationDuration: widget.animationDuration,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
|
@ -9,15 +9,65 @@ import 'package:flutter/widgets.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// A utility class for dealing with the overlay color needed
|
||||
/// to indicate elevation of surfaces in a dark theme.
|
||||
/// to indicate elevation of surfaces.
|
||||
class ElevationOverlay {
|
||||
// This class is not meant to be instantiated or extended; this constructor
|
||||
// prevents instantiation and extension.
|
||||
ElevationOverlay._();
|
||||
|
||||
/// Applies a surface tint color to a given container color to indicate
|
||||
/// the level of its elevation.
|
||||
///
|
||||
/// With Material Design 3, some components will use a "surface tint" color
|
||||
/// overlay with an opacity applied to their base color to indicate they are
|
||||
/// elevated. The amount of opacity will vary with the elevation as described
|
||||
/// in: https://m3.material.io/styles/color/the-color-system/color-roles.
|
||||
///
|
||||
/// If [surfaceTint] is not null then the returned color will be the given
|
||||
/// [color] with the [surfaceTint] of the appropriate opacity applies to it.
|
||||
/// Otherwise it will just return [color] unmodified.
|
||||
static Color applySurfaceTint(Color color, Color? surfaceTint, double elevation) {
|
||||
if (surfaceTint != null) {
|
||||
return Color.alphaBlend(surfaceTint.withOpacity(_surfaceTintOpacityForElevation(elevation)), color);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// Calculates the opacity of the surface tint color from the elevation by
|
||||
// looking it up in the token generated table of opacities, interpolating
|
||||
// between values as needed. If the elevation is outside the range of values
|
||||
// in the table it will clamp to the smallest or largest opacity.
|
||||
static double _surfaceTintOpacityForElevation(double elevation) {
|
||||
if (elevation < _surfaceTintElevationOpacities[0].elevation) {
|
||||
// Elevation less than the first entry, so just clamp it to the first one.
|
||||
return _surfaceTintElevationOpacities[0].opacity;
|
||||
}
|
||||
|
||||
// Walk the opacity list and find the closest match(es) for the elevation.
|
||||
int index = 0;
|
||||
while (elevation >= _surfaceTintElevationOpacities[index].elevation) {
|
||||
// If we found it exactly or walked off the end of the list just return it.
|
||||
if (elevation == _surfaceTintElevationOpacities[index].elevation ||
|
||||
index + 1 == _surfaceTintElevationOpacities.length) {
|
||||
return _surfaceTintElevationOpacities[index].opacity;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// Interpolate between the two opacity values
|
||||
final _ElevationOpacity lower = _surfaceTintElevationOpacities[index - 1];
|
||||
final _ElevationOpacity upper = _surfaceTintElevationOpacities[index];
|
||||
final double t = (elevation - lower.elevation) / (upper.elevation - lower.elevation);
|
||||
return lower.opacity + t * (upper.opacity - lower.opacity);
|
||||
}
|
||||
|
||||
/// Applies an overlay color to a surface color to indicate
|
||||
/// the level of its elevation in a dark theme.
|
||||
///
|
||||
/// If using Material Design 3, this type of color overlay is no longer used.
|
||||
/// Instead a "surface tint" overlay is used instead. See [applySurfaceTint],
|
||||
/// [ThemeData.useMaterial3] for more information.
|
||||
///
|
||||
/// Material drop shadows can be difficult to see in a dark theme, so the
|
||||
/// elevation of a surface should be portrayed with an "overlay" in addition
|
||||
/// to the shadow. As the elevation of the component increases, the
|
||||
@ -55,6 +105,10 @@ class ElevationOverlay {
|
||||
/// Computes the appropriate overlay color used to indicate elevation in
|
||||
/// dark themes.
|
||||
///
|
||||
/// If using Material Design 3, this type of color overlay is no longer used.
|
||||
/// Instead a "surface tint" overlay is used instead. See [applySurfaceTint],
|
||||
/// [ThemeData.useMaterial3] for more information.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * https://material.io/design/color/dark-theme.html#properties which
|
||||
@ -67,6 +121,10 @@ class ElevationOverlay {
|
||||
/// Returns a color blended by laying a semi-transparent overlay (using the
|
||||
/// [overlay] color) on top of a surface (using the [surface] color).
|
||||
///
|
||||
/// If using Material Design 3, this type of color overlay is no longer used.
|
||||
/// Instead a "surface tint" overlay is used instead. See [applySurfaceTint],
|
||||
/// [ThemeData.useMaterial3] for more information.
|
||||
///
|
||||
/// The opacity of the overlay depends on [elevation]. As [elevation]
|
||||
/// increases, the opacity will also increase.
|
||||
///
|
||||
@ -84,3 +142,34 @@ class ElevationOverlay {
|
||||
return color.withOpacity(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// A data class to hold the opacity at a given elevation.
|
||||
class _ElevationOpacity {
|
||||
const _ElevationOpacity(this.elevation, this.opacity);
|
||||
|
||||
final double elevation;
|
||||
final double opacity;
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES
|
||||
|
||||
// Generated code to the end of this file. Do not edit by hand.
|
||||
// These defaults are generated from the Material Design Token
|
||||
// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// Generated version v0_90
|
||||
|
||||
// Surface tint opacities based on elevations according to the
|
||||
// Material Design 3 specification:
|
||||
// https://m3.material.io/styles/color/the-color-system/color-roles
|
||||
// Ordered by increasing elevation.
|
||||
const List<_ElevationOpacity> _surfaceTintElevationOpacities = <_ElevationOpacity>[
|
||||
_ElevationOpacity(0.0, 0.0), // Elevation level 0
|
||||
_ElevationOpacity(1.0, 0.05), // Elevation level 1
|
||||
_ElevationOpacity(3.0, 0.08), // Elevation level 2
|
||||
_ElevationOpacity(6.0, 0.11), // Elevation level 3
|
||||
_ElevationOpacity(8.0, 0.12), // Elevation level 4
|
||||
_ElevationOpacity(12.0, 0.14), // Elevation level 5
|
||||
];
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES
|
||||
|
@ -111,10 +111,11 @@ abstract class MaterialInkController {
|
||||
///
|
||||
/// In general, the features of a [Material] should not change over time (e.g. a
|
||||
/// [Material] should not change its [color], [shadowColor] or [type]).
|
||||
/// Changes to [elevation] and [shadowColor] are animated for [animationDuration].
|
||||
/// Changes to [shape] are animated if [type] is not [MaterialType.transparency]
|
||||
/// and [ShapeBorder.lerp] between the previous and next [shape] values is
|
||||
/// supported. Shape changes are also animated for [animationDuration].
|
||||
/// Changes to [elevation], [shadowColor] and [surfaceTintColor] are animated
|
||||
/// for [animationDuration]. Changes to [shape] are animated if [type] is
|
||||
/// not [MaterialType.transparency] and [ShapeBorder.lerp] between the previous
|
||||
/// and next [shape] values is supported. Shape changes are also animated
|
||||
/// for [animationDuration].
|
||||
///
|
||||
/// ## Shape
|
||||
///
|
||||
@ -166,7 +167,7 @@ abstract class MaterialInkController {
|
||||
class Material extends StatefulWidget {
|
||||
/// Creates a piece of material.
|
||||
///
|
||||
/// The [type], [elevation], [shadowColor], [borderOnForeground],
|
||||
/// The [type], [elevation], [borderOnForeground],
|
||||
/// [clipBehavior], and [animationDuration] arguments must not be null.
|
||||
/// Additionally, [elevation] must be non-negative.
|
||||
///
|
||||
@ -181,6 +182,7 @@ class Material extends StatefulWidget {
|
||||
this.elevation = 0.0,
|
||||
this.color,
|
||||
this.shadowColor,
|
||||
this.surfaceTintColor,
|
||||
this.textStyle,
|
||||
this.borderRadius,
|
||||
this.shape,
|
||||
@ -217,16 +219,20 @@ class Material extends StatefulWidget {
|
||||
/// widget conceptually defines an independent printed piece of material.
|
||||
///
|
||||
/// Defaults to 0. Changing this value will cause the shadow and the elevation
|
||||
/// overlay to animate over [Material.animationDuration].
|
||||
/// overlay or surface tint to animate over [Material.animationDuration].
|
||||
///
|
||||
/// The value is non-negative.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ThemeData.useMaterial3] which defines whether a surface tint or
|
||||
/// elevation overlay is used to indicate elevation.
|
||||
/// * [ThemeData.applyElevationOverlayColor] which controls the whether
|
||||
/// an overlay color will be applied to indicate elevation.
|
||||
/// * [Material.color] which may have an elevation overlay applied.
|
||||
///
|
||||
/// * [Material.shadowColor] which will be used for the color of a drop shadow.
|
||||
/// * [Material.surfaceTintColor] which will be used as the overlay tint to
|
||||
/// show elevation.
|
||||
/// {@endtemplate}
|
||||
final double elevation;
|
||||
|
||||
@ -235,29 +241,59 @@ class Material extends StatefulWidget {
|
||||
/// Must be opaque. To create a transparent piece of material, use
|
||||
/// [MaterialType.transparency].
|
||||
///
|
||||
/// To support dark themes, if the surrounding
|
||||
/// [ThemeData.applyElevationOverlayColor] is true and [ThemeData.brightness]
|
||||
/// is [Brightness.dark] then a semi-transparent overlay color will be
|
||||
/// composited on top of this color to indicate the elevation.
|
||||
/// If [ThemeData.useMaterial3] is true then an optional [surfaceTintColor]
|
||||
/// overlay may be applied on top of this color to indicate elevation.
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is false and [ThemeData.applyElevationOverlayColor]
|
||||
/// is true and [ThemeData.brightness] is [Brightness.dark] then a
|
||||
/// semi-transparent overlay color will be composited on top of this
|
||||
/// color to indicate the elevation. This is no longer needed for Material
|
||||
/// Design 3, which uses [surfaceTintColor].
|
||||
///
|
||||
/// By default, the color is derived from the [type] of material.
|
||||
final Color? color;
|
||||
|
||||
/// The color to paint the shadow below the material.
|
||||
///
|
||||
/// If null, [ThemeData.shadowColor] is used, which defaults to fully opaque black.
|
||||
/// When [ThemeData.useMaterial3] is true, and this is null, then no drop
|
||||
/// shadow will be rendered for this material. If it is non-null, then this
|
||||
/// color will be used to render a drop shadow below the material.
|
||||
///
|
||||
/// Shadows can be difficult to see in a dark theme, so the elevation of a
|
||||
/// surface should be portrayed with an "overlay" in addition to the shadow.
|
||||
/// As the elevation of the component increases, the overlay increases in
|
||||
/// opacity.
|
||||
/// When [ThemeData.useMaterial3] is false, and this is null, then
|
||||
/// [ThemeData.shadowColor] is used, which defaults to fully opaque black.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ThemeData.useMaterial3], which determines the default value for this
|
||||
/// property if it is null.
|
||||
/// * [ThemeData.applyElevationOverlayColor], which turns elevation overlay
|
||||
/// on or off for dark themes.
|
||||
final Color? shadowColor;
|
||||
|
||||
/// The color of the surface tint overlay applied to the material color
|
||||
/// to indicate elevation.
|
||||
///
|
||||
/// Material Design 3 introduced a new way for some components to indicate
|
||||
/// their elevation by using a surface tint color overlay on top of the
|
||||
/// base material [color]. This overlay is painted with an opacity that is
|
||||
/// related to the [elevation] of the material.
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is false, then this property is not used.
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is true and [surfaceTintColor] is not null,
|
||||
/// then it will be used to overlay the base [color] with an opacity based
|
||||
/// on the [elevation].
|
||||
///
|
||||
/// Otherwise, no surface tint will be applied.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ThemeData.useMaterial3], which turns this feature on.
|
||||
/// * [ElevationOverlay.applySurfaceTint], which is used to implement the
|
||||
/// tint.
|
||||
/// * https://m3.material.io/styles/color/the-color-system/color-roles
|
||||
/// which specifies how the overlay is applied.
|
||||
final Color? surfaceTintColor;
|
||||
|
||||
/// The typographical style to use for text within this material.
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@ -287,7 +323,7 @@ class Material extends StatefulWidget {
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// Defines the duration of animated changes for [shape], [elevation],
|
||||
/// [shadowColor] and the elevation overlay if it is applied.
|
||||
/// [shadowColor], [surfaceTintColor] and the elevation overlay if it is applied.
|
||||
///
|
||||
/// The default value is [kThemeChangeDuration].
|
||||
final Duration animationDuration;
|
||||
@ -327,6 +363,7 @@ class Material extends StatefulWidget {
|
||||
properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
|
||||
properties.add(ColorProperty('color', color, defaultValue: null));
|
||||
properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
|
||||
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
|
||||
textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
|
||||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<bool>('borderOnForeground', borderOnForeground, defaultValue: true));
|
||||
@ -362,6 +399,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final Color? backgroundColor = _getBackgroundColor(context);
|
||||
assert(
|
||||
backgroundColor != null || widget.type == MaterialType.transparency,
|
||||
@ -403,14 +441,18 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
|
||||
// we choose not to as we want the change from the fast-path to the
|
||||
// slow-path to be noticeable in the construction site of Material.
|
||||
if (widget.type == MaterialType.canvas && widget.shape == null && widget.borderRadius == null) {
|
||||
final Color color = Theme.of(context).useMaterial3
|
||||
? ElevationOverlay.applySurfaceTint(backgroundColor!, widget.surfaceTintColor, widget.elevation)
|
||||
: ElevationOverlay.applyOverlay(context, backgroundColor!, widget.elevation);
|
||||
|
||||
return AnimatedPhysicalModel(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: widget.animationDuration,
|
||||
shape: BoxShape.rectangle,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
elevation: widget.elevation,
|
||||
color: ElevationOverlay.applyOverlay(context, backgroundColor!, widget.elevation),
|
||||
shadowColor: widget.shadowColor ?? Theme.of(context).shadowColor,
|
||||
color: color,
|
||||
shadowColor: widget.shadowColor ?? (theme.useMaterial3 ? const Color(0x00000000) : theme.shadowColor),
|
||||
animateColor: false,
|
||||
child: contents,
|
||||
);
|
||||
@ -435,7 +477,8 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
|
||||
clipBehavior: widget.clipBehavior,
|
||||
elevation: widget.elevation,
|
||||
color: backgroundColor!,
|
||||
shadowColor: widget.shadowColor ?? Theme.of(context).shadowColor,
|
||||
shadowColor: widget.shadowColor ?? (theme.useMaterial3 ? const Color(0x00000000) : theme.shadowColor),
|
||||
surfaceTintColor: widget.surfaceTintColor,
|
||||
child: contents,
|
||||
);
|
||||
}
|
||||
@ -694,6 +737,7 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
required this.elevation,
|
||||
required this.color,
|
||||
required this.shadowColor,
|
||||
required this.surfaceTintColor,
|
||||
Curve curve = Curves.linear,
|
||||
required Duration duration,
|
||||
}) : assert(child != null),
|
||||
@ -738,6 +782,9 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
/// The target shadow color.
|
||||
final Color shadowColor;
|
||||
|
||||
/// The target surface tint color.
|
||||
final Color? surfaceTintColor;
|
||||
|
||||
@override
|
||||
_MaterialInteriorState createState() => _MaterialInteriorState();
|
||||
|
||||
@ -753,6 +800,7 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
|
||||
class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior> {
|
||||
Tween<double>? _elevation;
|
||||
ColorTween? _surfaceTintColor;
|
||||
ColorTween? _shadowColor;
|
||||
ShapeBorderTween? _border;
|
||||
|
||||
@ -768,6 +816,13 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
|
||||
widget.shadowColor,
|
||||
(dynamic value) => ColorTween(begin: value as Color),
|
||||
) as ColorTween?;
|
||||
_surfaceTintColor = widget.surfaceTintColor != null
|
||||
? visitor(
|
||||
_surfaceTintColor,
|
||||
widget.surfaceTintColor,
|
||||
(dynamic value) => ColorTween(begin: value as Color),
|
||||
) as ColorTween?
|
||||
: null;
|
||||
_border = visitor(
|
||||
_border,
|
||||
widget.shape,
|
||||
@ -779,6 +834,9 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
|
||||
Widget build(BuildContext context) {
|
||||
final ShapeBorder shape = _border!.evaluate(animation)!;
|
||||
final double elevation = _elevation!.evaluate(animation);
|
||||
final Color color = Theme.of(context).useMaterial3
|
||||
? ElevationOverlay.applySurfaceTint(widget.color, _surfaceTintColor?.evaluate(animation), elevation)
|
||||
: ElevationOverlay.applyOverlay(context, widget.color, elevation);
|
||||
return PhysicalShape(
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: shape,
|
||||
@ -786,7 +844,7 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
|
||||
),
|
||||
clipBehavior: widget.clipBehavior,
|
||||
elevation: elevation,
|
||||
color: ElevationOverlay.applyOverlay(context, widget.color, elevation),
|
||||
color: color,
|
||||
shadowColor: _shadowColor!.evaluate(animation)!,
|
||||
child: _ShapeBorderPaint(
|
||||
shape: shape,
|
||||
|
@ -437,7 +437,7 @@ class ThemeData with Diagnosticable {
|
||||
dialogBackgroundColor ??= colorScheme.background;
|
||||
indicatorColor ??= onPrimarySurfaceColor;
|
||||
errorColor ??= colorScheme.error;
|
||||
applyElevationOverlayColor ??= isDark;
|
||||
applyElevationOverlayColor ??= brightness == Brightness.dark;
|
||||
}
|
||||
applyElevationOverlayColor ??= false;
|
||||
primarySwatch ??= Colors.blue;
|
||||
@ -922,6 +922,7 @@ class ThemeData with Diagnosticable {
|
||||
factory ThemeData.from({
|
||||
required ColorScheme colorScheme,
|
||||
TextTheme? textTheme,
|
||||
bool? useMaterial3,
|
||||
}) {
|
||||
final bool isDark = colorScheme.brightness == Brightness.dark;
|
||||
|
||||
@ -947,6 +948,7 @@ class ThemeData with Diagnosticable {
|
||||
errorColor: colorScheme.error,
|
||||
textTheme: textTheme,
|
||||
applyElevationOverlayColor: isDark,
|
||||
useMaterial3: useMaterial3,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1003,6 +1005,11 @@ class ThemeData with Diagnosticable {
|
||||
/// Apply a semi-transparent overlay color on Material surfaces to indicate
|
||||
/// elevation for dark themes.
|
||||
///
|
||||
/// If [useMaterial3] is true, then this flag is ignored as there is a new
|
||||
/// [Material.surfaceTintColor] used to create an overlay for Material 3.
|
||||
/// This flag is meant only for the Material 2 elevation overlay for dark
|
||||
/// themes.
|
||||
///
|
||||
/// Material drop shadows can be difficult to see in a dark theme, so the
|
||||
/// elevation of a surface should be portrayed with an "overlay" in addition
|
||||
/// to the shadow. As the elevation of the component increases, the
|
||||
@ -1161,6 +1168,7 @@ class ThemeData with Diagnosticable {
|
||||
/// * [AlertDialog]
|
||||
/// * [Dialog]
|
||||
/// * [FloatingActionButton]
|
||||
/// * [Material]
|
||||
/// * [NavigationBar]
|
||||
/// * [NavigationRail]
|
||||
///
|
||||
|
88
packages/flutter/test/material/elevation_overlay_test.dart
Normal file
88
packages/flutter/test/material/elevation_overlay_test.dart
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('applySurfaceTint with null surface tint returns given color', () {
|
||||
final Color result = ElevationOverlay.applySurfaceTint(const Color(0xff888888), null, 42.0);
|
||||
|
||||
expect(result, equals(const Color(0xFF888888)));
|
||||
});
|
||||
|
||||
test('applySurfaceTint with exact elevation levels uses the right opacity overlay', () {
|
||||
const Color baseColor = Color(0xff888888);
|
||||
const Color surfaceTintColor = Color(0xff44CCFF);
|
||||
|
||||
Color overlayWithOpacity(double opacity) {
|
||||
return Color.alphaBlend(surfaceTintColor.withOpacity(opacity), baseColor);
|
||||
}
|
||||
|
||||
// Based on values from the spec:
|
||||
// https://m3.material.io/styles/color/the-color-system/color-roles
|
||||
|
||||
// Elevation level 0 (0.0) - should have opacity 0.0.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 0.0), equals(overlayWithOpacity(0.0)));
|
||||
|
||||
// Elevation level 1 (1.0) - should have opacity 0.05.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 1.0), equals(overlayWithOpacity(0.05)));
|
||||
|
||||
// Elevation level 2 (3.0) - should have opacity 0.08.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 3.0), equals(overlayWithOpacity(0.08)));
|
||||
|
||||
// Elevation level 3 (6.0) - should have opacity 0.11`.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 6.0), equals(overlayWithOpacity(0.11)));
|
||||
|
||||
// Elevation level 4 (8.0) - should have opacity 0.12.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 8.0), equals(overlayWithOpacity(0.12)));
|
||||
|
||||
// Elevation level 5 (12.0) - should have opacity 0.14.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 12.0), equals(overlayWithOpacity(0.14)));
|
||||
});
|
||||
|
||||
test('applySurfaceTint with elevation lower than level 0 should have no overlay', () {
|
||||
const Color baseColor = Color(0xff888888);
|
||||
const Color surfaceTintColor = Color(0xff44CCFF);
|
||||
|
||||
Color overlayWithOpacity(double opacity) {
|
||||
return Color.alphaBlend(surfaceTintColor.withOpacity(opacity), baseColor);
|
||||
}
|
||||
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, -42.0), equals(overlayWithOpacity(0.0)));
|
||||
});
|
||||
|
||||
test('applySurfaceTint with elevation higher than level 5 should have no level 5 overlay', () {
|
||||
const Color baseColor = Color(0xff888888);
|
||||
const Color surfaceTintColor = Color(0xff44CCFF);
|
||||
|
||||
Color overlayWithOpacity(double opacity) {
|
||||
return Color.alphaBlend(surfaceTintColor.withOpacity(opacity), baseColor);
|
||||
}
|
||||
|
||||
// Elevation level 5 (12.0) - should have opacity 0.14.
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 42.0), equals(overlayWithOpacity(0.14)));
|
||||
});
|
||||
|
||||
test('applySurfaceTint with elevation between two levels should interpolate the opacity', () {
|
||||
const Color baseColor = Color(0xff888888);
|
||||
const Color surfaceTintColor = Color(0xff44CCFF);
|
||||
|
||||
Color overlayWithOpacity(double opacity) {
|
||||
return Color.alphaBlend(surfaceTintColor.withOpacity(opacity), baseColor);
|
||||
}
|
||||
|
||||
// Elevation between level 4 (8.0) and level 5 (12.0) should be interpolated
|
||||
// between the opacities 0.12 and 0.14.
|
||||
|
||||
// One third (0.3): (elevation 9.2) -> (opacity 0.126)
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 9.2), equals(overlayWithOpacity(0.126)));
|
||||
|
||||
// Half way (0.5): (elevation 10.0) -> (opacity 0.13)
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 10.0), equals(overlayWithOpacity(0.13)));
|
||||
|
||||
// Two thirds (0.6): (elevation 10.4) -> (opacity 0.132)
|
||||
expect(ElevationOverlay.applySurfaceTint(baseColor, surfaceTintColor, 10.4), equals(overlayWithOpacity(0.132)));
|
||||
});
|
||||
}
|
@ -25,6 +25,7 @@ class NotifyMaterial extends StatelessWidget {
|
||||
Widget buildMaterial({
|
||||
double elevation = 0.0,
|
||||
Color shadowColor = const Color(0xFF00FF00),
|
||||
Color? surfaceTintColor,
|
||||
Color color = const Color(0xFF0000FF),
|
||||
}) {
|
||||
return Center(
|
||||
@ -34,6 +35,7 @@ Widget buildMaterial({
|
||||
child: Material(
|
||||
color: color,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
elevation: elevation,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
@ -99,6 +101,7 @@ void main() {
|
||||
const Material(
|
||||
color: Color(0xFFFFFFFF),
|
||||
shadowColor: Color(0xffff0000),
|
||||
surfaceTintColor: Color(0xff0000ff),
|
||||
textStyle: TextStyle(color: Color(0xff00ff00)),
|
||||
borderRadius: BorderRadiusDirectional.all(Radius.circular(10)),
|
||||
).debugFillProperties(builder);
|
||||
@ -112,6 +115,7 @@ void main() {
|
||||
'type: canvas',
|
||||
'color: Color(0xffffffff)',
|
||||
'shadowColor: Color(0xffff0000)',
|
||||
'surfaceTintColor: Color(0xff0000ff)',
|
||||
'textStyle.inherit: true',
|
||||
'textStyle.color: Color(0xff00ff00)',
|
||||
'borderRadius: BorderRadiusDirectional.circular(10.0)',
|
||||
@ -265,12 +269,72 @@ void main() {
|
||||
expect(pressed, isTrue);
|
||||
});
|
||||
|
||||
group('Elevation Overlay', () {
|
||||
group('Surface Tint Overlay', () {
|
||||
testWidgets('applyElevationOverlayColor does not effect anything with useMaterial3 set to true', (WidgetTester tester) async {
|
||||
const Color surfaceColor = Color(0xFF121212);
|
||||
await tester.pumpWidget(Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: true,
|
||||
applyElevationOverlayColor: true,
|
||||
colorScheme: const ColorScheme.dark().copyWith(surface: surfaceColor),
|
||||
),
|
||||
child: buildMaterial(color: surfaceColor, elevation: 8.0),
|
||||
));
|
||||
final RenderPhysicalShape model = getModel(tester);
|
||||
expect(model.color, equals(surfaceColor));
|
||||
});
|
||||
|
||||
testWidgets('surfaceTintColor is used to as an overlay to indicate elevation', (WidgetTester tester) async {
|
||||
const Color baseColor = Color(0xFF121212);
|
||||
const Color surfaceTintColor = Color(0xff44CCFF);
|
||||
|
||||
// With no surfaceTintColor specified, it should not apply an overlay
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: true,
|
||||
),
|
||||
child: buildMaterial(
|
||||
color: baseColor,
|
||||
elevation: 12.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final RenderPhysicalShape noTintModel = getModel(tester);
|
||||
expect(noTintModel.color, equals(baseColor));
|
||||
|
||||
// With surfaceTintColor specified, it should not apply an overlay based
|
||||
// on the elevation.
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: true,
|
||||
),
|
||||
child: buildMaterial(
|
||||
color: baseColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
elevation: 12.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final RenderPhysicalShape tintModel = getModel(tester);
|
||||
|
||||
// Final color should be the base with a tint of 0.14 opacity or 0xff192c33
|
||||
expect(tintModel.color, equals(const Color(0xff192c33)));
|
||||
});
|
||||
|
||||
}); // Surface Tint Overlay group
|
||||
|
||||
group('Elevation Overlay M2', () {
|
||||
// These tests only apply to the Material 2 overlay mechanism. This group
|
||||
// can be removed after migration to Material 3 is complete.
|
||||
testWidgets('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async {
|
||||
const Color surfaceColor = Color(0xFF121212);
|
||||
await tester.pumpWidget(Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: false,
|
||||
applyElevationOverlayColor: false,
|
||||
colorScheme: const ColorScheme.dark().copyWith(surface: surfaceColor),
|
||||
),
|
||||
@ -303,6 +367,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: false,
|
||||
applyElevationOverlayColor: true,
|
||||
colorScheme: const ColorScheme.dark().copyWith(
|
||||
surface: surfaceColor,
|
||||
@ -325,6 +390,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: false,
|
||||
applyElevationOverlayColor: true,
|
||||
colorScheme: const ColorScheme.dark(),
|
||||
),
|
||||
@ -343,6 +409,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: false,
|
||||
applyElevationOverlayColor: true,
|
||||
colorScheme: const ColorScheme.light(),
|
||||
),
|
||||
@ -364,6 +431,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: false,
|
||||
applyElevationOverlayColor: true,
|
||||
colorScheme: const ColorScheme.dark(),
|
||||
),
|
||||
@ -390,6 +458,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: false,
|
||||
applyElevationOverlayColor: true,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
surface: surfaceColor,
|
||||
@ -407,7 +476,8 @@ void main() {
|
||||
expect(model.color, equals(surfaceColorWithOverlay));
|
||||
expect(model.color, isNot(equals(surfaceColor)));
|
||||
});
|
||||
});
|
||||
|
||||
}); // Elevation Overlay M2 group
|
||||
|
||||
group('Transparency clipping', () {
|
||||
testWidgets('No clip by default', (WidgetTester tester) async {
|
||||
|
Loading…
x
Reference in New Issue
Block a user