Support for elevation based dark theme overlay color in the Material widget (#35560)

Added support for a semi-transparent white overlay color for `Material` widgets to indicate their elevation in a dart theme. A new `ThemeData.applyElevationOverlayColor` flag was added to control this behavior, which is off by default for backwards compatibility reasons.
diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart
index 2b86746..293d1fc 100644
--- a/packages/flutter/lib/src/material/material.dart
+++ b/packages/flutter/lib/src/material/material.dart
@@ -1,11 +1,13 @@
 // Copyright 2015 The Chromium 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 'dart:math' as math;
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
 
+import 'colors.dart';
 import 'constants.dart';
 import 'theme.dart';
 
@@ -200,15 +202,23 @@
   /// {@template flutter.material.material.elevation}
   /// The z-coordinate at which to place this material relative to its parent.
   ///
-  /// This controls the size of the shadow below the material.
+  /// This controls the size of the shadow below the material and the opacity
+  /// of the elevation overlay color if it is applied.
   ///
   /// If this is non-zero, the contents of the material are clipped, because the
   /// widget conceptually defines an independent printed piece of material.
   ///
-  /// Defaults to 0. Changing this value will cause the shadow to animate over
-  /// [animationDuration].
+  /// Defaults to 0. Changing this value will cause the shadow and the elevation
+  /// overlay to animate over [animationDuration].
   ///
   /// The value is non-negative.
+  ///
+  /// See also:
+  ///
+  ///   * [ThemeData.applyElevationOverlayColor] which controls the whether
+  ///     an overlay color will be applied to indicate elevation.
+  ///   * [color] which may have an elevation overlay applied.
+  ///
   /// {@endtemplate}
   final double elevation;
 
@@ -217,6 +227,11 @@
   /// 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
+  /// this color is [ThemeData.colorScheme.surface] then a semi-transparent
+  /// white will be composited on top this color to indicate the elevation.
+  ///
   /// By default, the color is derived from the [type] of material.
   final Color color;
 
@@ -252,7 +267,7 @@
   final Clip clipBehavior;
 
   /// Defines the duration of animated changes for [shape], [elevation],
-  /// and [shadowColor].
+  /// [shadowColor] and the elevation overlay if it is applied.
   ///
   /// The default value is [kThemeChangeDuration].
   final Duration animationDuration;
@@ -301,20 +316,43 @@
   static const double defaultSplashRadius = 35.0;
 }
 
+// Apply a semi-transparent white on surface colors to
+// indicate the level of elevation.
+Color _elevationOverlayColor(BuildContext context, Color background, double elevation) {
+  final ThemeData theme = Theme.of(context);
+  if (elevation > 0.0 &&
+      theme.applyElevationOverlayColor &&
+      background == theme.colorScheme.surface) {
+
+    // Compute the opacity for the given elevation
+    // This formula matches the values in the spec:
+    // https://material.io/design/color/dark-theme.html#properties
+    final double opacity = (4.5 * math.log(elevation + 1) + 2) / 100.0;
+    final Color overlay = Colors.white.withOpacity(opacity);
+    return Color.alphaBlend(overlay, background);
+  }
+  return background;
+}
+
 class _MaterialState extends State<Material> with TickerProviderStateMixin {
   final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer');
 
   Color _getBackgroundColor(BuildContext context) {
-    if (widget.color != null)
-      return widget.color;
-    switch (widget.type) {
-      case MaterialType.canvas:
-        return Theme.of(context).canvasColor;
-      case MaterialType.card:
-        return Theme.of(context).cardColor;
-      default:
-        return null;
+    final ThemeData theme = Theme.of(context);
+    Color color = widget.color;
+    if (color == null) {
+      switch (widget.type) {
+        case MaterialType.canvas:
+          color = theme.canvasColor;
+          break;
+        case MaterialType.card:
+          color = theme.cardColor;
+          break;
+        default:
+          break;
+      }
     }
+    return color;
   }
 
   @override
@@ -366,7 +404,7 @@
         clipBehavior: widget.clipBehavior,
         borderRadius: BorderRadius.zero,
         elevation: widget.elevation,
-        color: backgroundColor,
+        color: _elevationOverlayColor(context, backgroundColor, widget.elevation),
         shadowColor: widget.shadowColor,
         animateColor: false,
         child: contents,
@@ -711,6 +749,7 @@
   @override
   Widget build(BuildContext context) {
     final ShapeBorder shape = _border.evaluate(animation);
+    final double elevation = _elevation.evaluate(animation);
     return PhysicalShape(
       child: _ShapeBorderPaint(
         child: widget.child,
@@ -722,8 +761,8 @@
         textDirection: Directionality.of(context),
       ),
       clipBehavior: widget.clipBehavior,
-      elevation: _elevation.evaluate(animation),
-      color: widget.color,
+      elevation: elevation,
+      color: _elevationOverlayColor(context, widget.color, elevation),
       shadowColor: _shadowColor.evaluate(animation),
     );
   }
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 53dc558..e0c1dd6 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -159,6 +159,7 @@
     ChipThemeData chipTheme,
     TargetPlatform platform,
     MaterialTapTargetSize materialTapTargetSize,
+    bool applyElevationOverlayColor,
     PageTransitionsTheme pageTransitionsTheme,
     AppBarTheme appBarTheme,
     BottomAppBarTheme bottomAppBarTheme,
@@ -228,6 +229,7 @@
     final TextTheme defaultAccentTextTheme = accentIsDark ? typography.white : typography.black;
     accentTextTheme = defaultAccentTextTheme.merge(accentTextTheme);
     materialTapTargetSize ??= MaterialTapTargetSize.padded;
+    applyElevationOverlayColor ??= false;
     if (fontFamily != null) {
       textTheme = textTheme.apply(fontFamily: fontFamily);
       primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily);
@@ -315,6 +317,7 @@
       chipTheme: chipTheme,
       platform: platform,
       materialTapTargetSize: materialTapTargetSize,
+      applyElevationOverlayColor: applyElevationOverlayColor,
       pageTransitionsTheme: pageTransitionsTheme,
       appBarTheme: appBarTheme,
       bottomAppBarTheme: bottomAppBarTheme,
@@ -384,6 +387,7 @@
     @required this.chipTheme,
     @required this.platform,
     @required this.materialTapTargetSize,
+    @required this.applyElevationOverlayColor,
     @required this.pageTransitionsTheme,
     @required this.appBarTheme,
     @required this.bottomAppBarTheme,
@@ -679,6 +683,38 @@
   /// Configures the hit test size of certain Material widgets.
   final MaterialTapTargetSize materialTapTargetSize;
 
+  /// Apply a semi-transparent white overlay on Material surfaces to indicate
+  /// elevation 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 white
+  /// overlay increases in opacity. [applyElevationOverlayColor] turns the
+  /// application of this overlay on or off.
+  ///
+  /// If [true] a semi-transparent white overlay will be applied to the color
+  /// of [Material] widgets when their [Material.color] is [colorScheme.surface].
+  /// The level of transparency is based on [Material.elevation] as per the
+  /// Material Dark theme specification.
+  ///
+  /// If [false] the surface color will be used unmodified.
+  ///
+  /// Defaults to [false].
+  ///
+  /// Note: this setting is here to maintain backwards compatibility with
+  /// apps that were built before the Material Dark theme specification
+  /// was published. New apps should set this to [true] for any themes
+  /// where [brightness] is [Brightness.dark].
+  ///
+  /// See also:
+  ///
+  ///  * [Material.elevation], which effects how transparent the white overlay is.
+  ///  * [Material.color], the white color overlay will only be applied of the
+  ///    material's color is [colorScheme.surface].
+  ///  * <https://material.io/design/color/dark-theme.html>, which specifies how
+  ///    the overlay should be applied.
+  final bool applyElevationOverlayColor;
+
   /// Default [MaterialPageRoute] transitions per [TargetPlatform].
   ///
   /// [MaterialPageRoute.buildTransitions] delegates to a [PageTransitionsBuilder]
@@ -779,6 +815,7 @@
     ChipThemeData chipTheme,
     TargetPlatform platform,
     MaterialTapTargetSize materialTapTargetSize,
+    bool applyElevationOverlayColor,
     PageTransitionsTheme pageTransitionsTheme,
     AppBarTheme appBarTheme,
     BottomAppBarTheme bottomAppBarTheme,
@@ -837,6 +874,7 @@
       chipTheme: chipTheme ?? this.chipTheme,
       platform: platform ?? this.platform,
       materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
+      applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
       pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
       appBarTheme: appBarTheme ?? this.appBarTheme,
       bottomAppBarTheme: bottomAppBarTheme ?? this.bottomAppBarTheme,
@@ -973,6 +1011,7 @@
       chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t),
       platform: t < 0.5 ? a.platform : b.platform,
       materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
+      applyElevationOverlayColor: t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
       pageTransitionsTheme: t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
       appBarTheme: AppBarTheme.lerp(a.appBarTheme, b.appBarTheme, t),
       bottomAppBarTheme: BottomAppBarTheme.lerp(a.bottomAppBarTheme, b.bottomAppBarTheme, t),
@@ -1037,6 +1076,7 @@
            (otherData.chipTheme == chipTheme) &&
            (otherData.platform == platform) &&
            (otherData.materialTapTargetSize == materialTapTargetSize) &&
+           (otherData.applyElevationOverlayColor == applyElevationOverlayColor) &&
            (otherData.pageTransitionsTheme == pageTransitionsTheme) &&
            (otherData.appBarTheme == appBarTheme) &&
            (otherData.bottomAppBarTheme == bottomAppBarTheme) &&
@@ -1100,6 +1140,7 @@
       chipTheme,
       platform,
       materialTapTargetSize,
+      applyElevationOverlayColor,
       pageTransitionsTheme,
       appBarTheme,
       bottomAppBarTheme,
@@ -1160,6 +1201,7 @@
     properties.add(DiagnosticsProperty<CardTheme>('cardTheme', cardTheme));
     properties.add(DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
     properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize));
+    properties.add(DiagnosticsProperty<bool>('applyElevationOverlayColor', applyElevationOverlayColor));
     properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme));
     properties.add(DiagnosticsProperty<AppBarTheme>('appBarTheme', appBarTheme, defaultValue: defaultData.appBarTheme));
     properties.add(DiagnosticsProperty<BottomAppBarTheme>('bottomAppBarTheme', bottomAppBarTheme, defaultValue: defaultData.bottomAppBarTheme));
diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart
index cb5b103..4a7cb82 100644
--- a/packages/flutter/test/material/material_test.dart
+++ b/packages/flutter/test/material/material_test.dart
@@ -21,12 +21,14 @@
 Widget buildMaterial({
   double elevation = 0.0,
   Color shadowColor = const Color(0xFF00FF00),
+  Color color = const Color(0xFF0000FF),
 }) {
   return Center(
     child: SizedBox(
       height: 100.0,
       width: 100.0,
       child: Material(
+        color: color,
         shadowColor: shadowColor,
         elevation: elevation,
         shape: const CircleBorder(),
@@ -35,7 +37,7 @@
   );
 }
 
-RenderPhysicalShape getShadow(WidgetTester tester) {
+RenderPhysicalShape getModel(WidgetTester tester) {
   return tester.renderObject(find.byType(PhysicalShape));
 }
 
@@ -55,6 +57,12 @@
   bool shouldRepaint(PaintRecorder oldDelegate) => false;
 }
 
+class ElevationColor {
+  const ElevationColor(this.elevation, this.color);
+  final double elevation;
+  final Color color;
+}
+
 void main() {
   testWidgets('default Material debugFillProperties', (WidgetTester tester) async {
     final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
@@ -163,23 +171,23 @@
     // a kThemeChangeDuration time interval.
 
     await tester.pumpWidget(buildMaterial(elevation: 0.0));
-    final RenderPhysicalShape modelA = getShadow(tester);
+    final RenderPhysicalShape modelA = getModel(tester);
     expect(modelA.elevation, equals(0.0));
 
     await tester.pumpWidget(buildMaterial(elevation: 9.0));
-    final RenderPhysicalShape modelB = getShadow(tester);
+    final RenderPhysicalShape modelB = getModel(tester);
     expect(modelB.elevation, equals(0.0));
 
     await tester.pump(const Duration(milliseconds: 1));
-    final RenderPhysicalShape modelC = getShadow(tester);
+    final RenderPhysicalShape modelC = getModel(tester);
     expect(modelC.elevation, closeTo(0.0, 0.001));
 
     await tester.pump(kThemeChangeDuration ~/ 2);
-    final RenderPhysicalShape modelD = getShadow(tester);
+    final RenderPhysicalShape modelD = getModel(tester);
     expect(modelD.elevation, isNot(closeTo(0.0, 0.001)));
 
     await tester.pump(kThemeChangeDuration);
-    final RenderPhysicalShape modelE = getShadow(tester);
+    final RenderPhysicalShape modelE = getModel(tester);
     expect(modelE.elevation, equals(9.0));
   });
 
@@ -188,26 +196,96 @@
     // a kThemeChangeDuration time interval.
 
     await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00)));
-    final RenderPhysicalShape modelA = getShadow(tester);
+    final RenderPhysicalShape modelA = getModel(tester);
     expect(modelA.shadowColor, equals(const Color(0xFF00FF00)));
 
     await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000)));
-    final RenderPhysicalShape modelB = getShadow(tester);
+    final RenderPhysicalShape modelB = getModel(tester);
     expect(modelB.shadowColor, equals(const Color(0xFF00FF00)));
 
     await tester.pump(const Duration(milliseconds: 1));
-    final RenderPhysicalShape modelC = getShadow(tester);
+    final RenderPhysicalShape modelC = getModel(tester);
     expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00)));
 
     await tester.pump(kThemeChangeDuration ~/ 2);
-    final RenderPhysicalShape modelD = getShadow(tester);
+    final RenderPhysicalShape modelD = getModel(tester);
     expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00))));
 
     await tester.pump(kThemeChangeDuration);
-    final RenderPhysicalShape modelE = getShadow(tester);
+    final RenderPhysicalShape modelE = getModel(tester);
     expect(modelE.shadowColor, equals(const Color(0xFFFF0000)));
   });
 
+  group('Elevation Overlay', () {
+
+    testWidgets('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async {
+      const Color surfaceColor = Color(0xFF121212);
+      await tester.pumpWidget(Theme(
+          data: ThemeData(
+            applyElevationOverlayColor: false,
+            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('applyElevationOverlayColor set to true overlays a transparent white on surface color', (WidgetTester tester) async {
+      // The colors we should get with a base surface color of 0xFF121212 for
+      // a given elevation
+      const List<ElevationColor> elevationColors = <ElevationColor>[
+        ElevationColor(0.0, Color(0xFF121212)),
+        ElevationColor(1.0, Color(0xFF1E1E1E)),
+        ElevationColor(2.0, Color(0xFF222222)),
+        ElevationColor(3.0, Color(0xFF252525)),
+        ElevationColor(4.0, Color(0xFF282828)),
+        ElevationColor(6.0, Color(0xFF2B2B2B)),
+        ElevationColor(8.0, Color(0xFF2D2D2D)),
+        ElevationColor(12.0, Color(0xFF323232)),
+        ElevationColor(16.0, Color(0xFF353535)),
+        ElevationColor(24.0, Color(0xFF393939)),
+      ];
+      const Color surfaceColor = Color(0xFF121212);
+
+      for (ElevationColor test in elevationColors) {
+        await tester.pumpWidget(
+            Theme(
+              data: ThemeData(
+                applyElevationOverlayColor: true,
+                colorScheme: const ColorScheme.dark().copyWith(surface: surfaceColor),
+              ),
+              child: buildMaterial(
+                color: surfaceColor,
+                elevation: test.elevation,
+              ),
+            )
+        );
+        await tester.pumpAndSettle(); // wait for the elevation animation to finish
+        final RenderPhysicalShape model = getModel(tester);
+        expect(model.color, equals(test.color));
+      }
+    });
+
+    testWidgets('overlay will only apply to materials using colorScheme.surface', (WidgetTester tester) async {
+      await tester.pumpWidget(
+          Theme(
+            data: ThemeData(
+              applyElevationOverlayColor: true,
+              colorScheme: const ColorScheme.dark().copyWith(surface: const Color(0xFF121212)),
+            ),
+            child: buildMaterial(
+                color: Colors.cyan,
+                elevation: 8.0
+            ),
+          )
+      );
+      final RenderPhysicalShape model = getModel(tester);
+      expect(model.color, equals(Colors.cyan));
+    });
+
+  });
+
   group('Transparency clipping', () {
     testWidgets('No clip by default', (WidgetTester tester) async {
       final GlobalKey materialKey = GlobalKey();