Unverified Commit 6ec0b835 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Add support for surface tint color overlays to `Material` widget. (#100036)

parent e99a66a4
......@@ -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();
}
// 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
......@@ -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]
///
......
// 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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment