// 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 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'theme.dart'; /// A utility class for dealing with the overlay color needed /// to indicate elevation of surfaces. abstract final class 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 and not completely transparent ([Color.alpha] /// is 0), then the returned color will be the given [color] with the /// [surfaceTint] of the appropriate opacity applied to it. Otherwise it will /// just return [color] unmodified. static Color applySurfaceTint(Color color, Color? surfaceTint, double elevation) { if (surfaceTint != null && surfaceTint != Colors.transparent) { 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 /// overlay increases in opacity. This function computes and applies this /// overlay to a given color as needed. /// /// If the ambient theme is dark ([ThemeData.brightness] is [Brightness.dark]), /// and [ThemeData.applyElevationOverlayColor] is true, and the given /// [color] is [ColorScheme.surface] then this will return a version of /// the [color] with a semi-transparent [ColorScheme.onSurface] overlaid /// on top of it. The opacity of the overlay is computed based on the /// [elevation]. /// /// Otherwise it will just return the [color] unmodified. /// /// See also: /// /// * [ThemeData.applyElevationOverlayColor] which controls the whether /// an overlay color will be applied to indicate elevation. /// * [overlayColor] which computes the needed overlay color. /// * [Material] which uses this to apply an elevation overlay to its surface. /// * <https://material.io/design/color/dark-theme.html>, which specifies how /// the overlay should be applied. static Color applyOverlay(BuildContext context, Color color, double elevation) { final ThemeData theme = Theme.of(context); if (elevation > 0.0 && theme.applyElevationOverlayColor && theme.brightness == Brightness.dark && color.withOpacity(1.0) == theme.colorScheme.surface.withOpacity(1.0)) { return colorWithOverlay(color, theme.colorScheme.onSurface, elevation); } return color; } /// 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 /// specifies the exact overlay values for a given elevation. static Color overlayColor(BuildContext context, double elevation) { final ThemeData theme = Theme.of(context); return _overlayColor(theme.colorScheme.onSurface, elevation); } /// 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. /// /// See https://material.io/design/color/dark-theme.html#properties. static Color colorWithOverlay(Color surface, Color overlay, double elevation) { return Color.alphaBlend(_overlayColor(overlay, elevation), surface); } /// Applies an opacity to [color] based on [elevation]. static Color _overlayColor(Color color, double elevation) { // 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; 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 - SurfaceTint // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // 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 - SurfaceTint