theme_data.dart 38.2 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:ui' show Color, hashValues;
6

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/widgets.dart';
9

10
import 'button_theme.dart';
11
import 'colors.dart';
12 13
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
14
import 'input_decorator.dart';
15
import 'slider_theme.dart';
Adam Barth's avatar
Adam Barth committed
16
import 'typography.dart';
17

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
/// Describes the contrast needs of a color.
enum Brightness {
  /// The color is dark and will require a light text color to achieve readable
  /// contrast.
  ///
  /// For example, the color might be dark grey, requiring white text.
  dark,

  /// The color is light and will require a dark text color to achieve readable
  /// contrast.
  ///
  /// For example, the color might be bright white, requiring black text.
  light,
}

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// Deriving these values is black magic. The spec claims that pressed buttons
// have a highlight of 0x66999999, but that's clearly wrong. The videos in the
// spec show that buttons have a composited highlight of #E1E1E1 on a background
// of #FAFAFA. Assuming that the highlight really has an opacity of 0x66, we can
// solve for the actual color of the highlight:
const Color _kLightThemeHighlightColor = const Color(0x66BCBCBC);

// The same video shows the splash compositing to #D7D7D7 on a background of
// #E1E1E1. Again, assuming the splash has an opacity of 0x66, we can solve for
// the actual color of the splash:
const Color _kLightThemeSplashColor = const Color(0x66C8C8C8);

// Unfortunately, a similar video isn't available for the dark theme, which
// means we assume the values in the spec are actually correct.
const Color _kDarkThemeHighlightColor = const Color(0x40CCCCCC);
const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC);

50 51 52
/// Holds the color and typography values for a material design theme.
///
/// Use this class to configure a [Theme] widget.
53 54
///
/// To obtain the current theme, use [Theme.of].
55
@immutable
56 57
class ThemeData extends Diagnosticable {
  /// Create a [ThemeData] given a set of preferred values.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  ///
  /// Default values will be derived for arguments that are omitted.
  ///
  /// The most useful values to give are, in order of importance:
  ///
  ///  * The desired theme [brightness].
  ///
  ///  * The primary color palette (the [primarySwatch]), chosen from
  ///    one of the swatches defined by the material design spec. This
  ///    should be one of the maps from the [Colors] class that do not
  ///    have "accent" in their name.
  ///
  ///  * The [accentColor], sometimes called the secondary color, and,
  ///    if the accent color is specified, its brightness
  ///    ([accentColorBrightness]), so that the right contrasting text
  ///    color will be used over the accent color.
  ///
75
  /// See <https://material.google.com/style/color.html> for
76
  /// more discussion on how to pick the right colors.
77
  factory ThemeData({
78
    Brightness brightness,
79
    MaterialColor primarySwatch,
80
    Color primaryColor,
81
    Brightness primaryColorBrightness,
82 83
    Color primaryColorLight,
    Color primaryColorDark,
84
    Color accentColor,
85
    Brightness accentColorBrightness,
86
    Color canvasColor,
87
    Color scaffoldBackgroundColor,
88
    Color bottomAppBarColor,
89 90 91 92
    Color cardColor,
    Color dividerColor,
    Color highlightColor,
    Color splashColor,
93
    InteractiveInkFeatureFactory splashFactory,
94 95
    Color selectedRowColor,
    Color unselectedWidgetColor,
96
    Color disabledColor,
97
    Color buttonColor,
98
    ButtonThemeData buttonTheme,
99
    Color secondaryHeaderColor,
100
    Color textSelectionColor,
101
    Color textSelectionHandleColor,
102
    Color backgroundColor,
103
    Color dialogBackgroundColor,
104 105
    Color indicatorColor,
    Color hintColor,
106
    Color errorColor,
107
    String fontFamily,
108
    TextTheme textTheme,
109
    TextTheme primaryTextTheme,
110
    TextTheme accentTextTheme,
111
    InputDecorationTheme inputDecorationTheme,
Ian Hickson's avatar
Ian Hickson committed
112
    IconThemeData iconTheme,
113
    IconThemeData primaryIconTheme,
114
    IconThemeData accentIconTheme,
115 116
    SliderThemeData sliderTheme,
    TargetPlatform platform,
117
  }) {
118 119
    brightness ??= Brightness.light;
    final bool isDark = brightness == Brightness.dark;
120
    primarySwatch ??= Colors.blue;
121
    primaryColor ??= isDark ? Colors.grey[900] : primarySwatch;
122
    primaryColorBrightness ??= estimateBrightnessForColor(primaryColor);
123 124
    primaryColorLight ??= isDark ? Colors.grey[500] : primarySwatch[100];
    primaryColorDark ??= isDark ? Colors.black : primarySwatch[700];
Ian Hickson's avatar
Ian Hickson committed
125
    final bool primaryIsDark = primaryColorBrightness == Brightness.dark;
Hans Muller's avatar
Hans Muller committed
126
    accentColor ??= isDark ? Colors.tealAccent[200] : primarySwatch[500];
127
    accentColorBrightness ??= estimateBrightnessForColor(accentColor);
128
    final bool accentIsDark = accentColorBrightness == Brightness.dark;
129
    canvasColor ??= isDark ? Colors.grey[850] : Colors.grey[50];
130
    scaffoldBackgroundColor ??= canvasColor;
131
    bottomAppBarColor ??= isDark ? Colors.grey[800] : Colors.white;
132 133 134 135
    cardColor ??= isDark ? Colors.grey[800] : Colors.white;
    dividerColor ??= isDark ? const Color(0x1FFFFFFF) : const Color(0x1F000000);
    highlightColor ??= isDark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor;
    splashColor ??= isDark ? _kDarkThemeSplashColor : _kLightThemeSplashColor;
136
    splashFactory ??= InkSplash.splashFactory;
137 138
    selectedRowColor ??= Colors.grey[100];
    unselectedWidgetColor ??= isDark ? Colors.white70 : Colors.black54;
139
    disabledColor ??= isDark ? Colors.white30 : Colors.black26;
140
    buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
141
    buttonTheme ??= const ButtonThemeData();
142 143
    // Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess.
    secondaryHeaderColor ??= isDark ? Colors.grey[700] : primarySwatch[50];
144
    textSelectionColor ??= isDark ? accentColor : primarySwatch[200];
145
    textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300];
146
    backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200];
147
    dialogBackgroundColor ??= isDark ? Colors.grey[800] : Colors.white;
148
    indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
149
    hintColor ??= isDark ?  const Color(0x80FFFFFF) : const Color(0x8A000000);
150
    errorColor ??= Colors.red[700];
151
    inputDecorationTheme ??= const InputDecorationTheme();
152 153 154
    iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
    primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
    accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
155
    platform ??= defaultTargetPlatform;
156 157
    final Typography typography = new Typography(platform: platform);
    textTheme ??= isDark ? typography.white : typography.black;
158 159
    primaryTextTheme ??= primaryIsDark ? typography.white : typography.black;
    accentTextTheme ??= accentIsDark ? typography.white : typography.black;
160 161 162 163 164
    if (fontFamily != null) {
      textTheme = textTheme.apply(fontFamily: fontFamily);
      primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily);
      accentTextTheme = accentTextTheme.apply(fontFamily: fontFamily);
    }
165
    sliderTheme ??= new SliderThemeData.fromPrimaryColors(
166 167 168
      primaryColor: primaryColor,
      primaryColorLight: primaryColorLight,
      primaryColorDark: primaryColorDark,
169
      valueIndicatorTextStyle: accentTextTheme.body2,
170
    );
171 172 173 174
    return new ThemeData.raw(
      brightness: brightness,
      primaryColor: primaryColor,
      primaryColorBrightness: primaryColorBrightness,
175 176
      primaryColorLight: primaryColorLight,
      primaryColorDark: primaryColorDark,
177 178
      accentColor: accentColor,
      accentColorBrightness: accentColorBrightness,
179
      canvasColor: canvasColor,
180
      scaffoldBackgroundColor: scaffoldBackgroundColor,
181
      bottomAppBarColor: bottomAppBarColor,
182 183 184 185
      cardColor: cardColor,
      dividerColor: dividerColor,
      highlightColor: highlightColor,
      splashColor: splashColor,
186
      splashFactory: splashFactory,
187 188
      selectedRowColor: selectedRowColor,
      unselectedWidgetColor: unselectedWidgetColor,
189
      disabledColor: disabledColor,
190
      buttonColor: buttonColor,
191
      buttonTheme: buttonTheme,
192
      secondaryHeaderColor: secondaryHeaderColor,
193
      textSelectionColor: textSelectionColor,
194
      textSelectionHandleColor: textSelectionHandleColor,
195
      backgroundColor: backgroundColor,
196
      dialogBackgroundColor: dialogBackgroundColor,
197 198
      indicatorColor: indicatorColor,
      hintColor: hintColor,
199
      errorColor: errorColor,
200
      textTheme: textTheme,
201
      primaryTextTheme: primaryTextTheme,
202
      accentTextTheme: accentTextTheme,
203
      inputDecorationTheme: inputDecorationTheme,
Ian Hickson's avatar
Ian Hickson committed
204
      iconTheme: iconTheme,
205
      primaryIconTheme: primaryIconTheme,
206
      accentIconTheme: accentIconTheme,
207 208
      sliderTheme: sliderTheme,
      platform: platform,
209
    );
210 211
  }

212
  /// Create a [ThemeData] given a set of exact values. All the values
213 214 215 216 217
  /// must be specified.
  ///
  /// This will rarely be used directly. It is used by [lerp] to
  /// create intermediate themes based on two themes created with the
  /// [new ThemeData] constructor.
218
  const ThemeData.raw({
219 220 221
    @required this.brightness,
    @required this.primaryColor,
    @required this.primaryColorBrightness,
222 223
    @required this.primaryColorLight,
    @required this.primaryColorDark,
224 225 226 227
    @required this.accentColor,
    @required this.accentColorBrightness,
    @required this.canvasColor,
    @required this.scaffoldBackgroundColor,
228
    @required this.bottomAppBarColor,
229 230 231 232
    @required this.cardColor,
    @required this.dividerColor,
    @required this.highlightColor,
    @required this.splashColor,
233
    @required this.splashFactory,
234 235 236 237
    @required this.selectedRowColor,
    @required this.unselectedWidgetColor,
    @required this.disabledColor,
    @required this.buttonColor,
238
    @required this.buttonTheme,
239 240 241 242 243 244 245 246 247 248 249
    @required this.secondaryHeaderColor,
    @required this.textSelectionColor,
    @required this.textSelectionHandleColor,
    @required this.backgroundColor,
    @required this.dialogBackgroundColor,
    @required this.indicatorColor,
    @required this.hintColor,
    @required this.errorColor,
    @required this.textTheme,
    @required this.primaryTextTheme,
    @required this.accentTextTheme,
250
    @required this.inputDecorationTheme,
251 252 253
    @required this.iconTheme,
    @required this.primaryIconTheme,
    @required this.accentIconTheme,
254 255
    @required this.sliderTheme,
    @required this.platform,
256 257 258
  }) : assert(brightness != null),
       assert(primaryColor != null),
       assert(primaryColorBrightness != null),
259 260
       assert(primaryColorLight != null),
       assert(primaryColorDark != null),
261 262 263 264
       assert(accentColor != null),
       assert(accentColorBrightness != null),
       assert(canvasColor != null),
       assert(scaffoldBackgroundColor != null),
265
       assert(bottomAppBarColor != null),
266 267 268 269
       assert(cardColor != null),
       assert(dividerColor != null),
       assert(highlightColor != null),
       assert(splashColor != null),
270
       assert(splashFactory != null),
271 272 273
       assert(selectedRowColor != null),
       assert(unselectedWidgetColor != null),
       assert(disabledColor != null),
274
       assert(buttonTheme != null),
275 276 277 278 279 280 281 282 283 284 285
       assert(secondaryHeaderColor != null),
       assert(textSelectionColor != null),
       assert(textSelectionHandleColor != null),
       assert(backgroundColor != null),
       assert(dialogBackgroundColor != null),
       assert(indicatorColor != null),
       assert(hintColor != null),
       assert(errorColor != null),
       assert(textTheme != null),
       assert(primaryTextTheme != null),
       assert(accentTextTheme != null),
286
       assert(inputDecorationTheme != null),
287 288 289
       assert(iconTheme != null),
       assert(primaryIconTheme != null),
       assert(accentIconTheme != null),
290
       assert(sliderTheme != null),
291
       assert(platform != null);
Ian Hickson's avatar
Ian Hickson committed
292

293
  /// A default light blue theme.
294 295 296
  ///
  /// This theme does not contain text geometry. Instead, it is expected that
  /// this theme is localized using text geometry using [ThemeData.localize].
297
  factory ThemeData.light() => new ThemeData(brightness: Brightness.light);
298 299

  /// A default dark theme with a teal accent color.
300 301 302
  ///
  /// This theme does not contain text geometry. Instead, it is expected that
  /// this theme is localized using text geometry using [ThemeData.localize].
303
  factory ThemeData.dark() => new ThemeData(brightness: Brightness.dark);
304

305
  /// The default color theme. Same as [new ThemeData.light].
306 307
  ///
  /// This is used by [Theme.of] when no theme has been specified.
308 309 310 311 312 313
  ///
  /// This theme does not contain text geometry. Instead, it is expected that
  /// this theme is localized using text geometry using [ThemeData.localize].
  ///
  /// Most applications would use [Theme.of], which provides correct localized
  /// text geometry.
314 315
  factory ThemeData.fallback() => new ThemeData.light();

316 317 318
  /// The brightness of the overall theme of the application. Used by widgets
  /// like buttons to determine what color to pick when not using the primary or
  /// accent color.
319
  ///
320 321
  /// When the [Brightness] is dark, the canvas, card, and primary colors are
  /// all dark. When the [Brightness] is light, the canvas and card colors
322 323
  /// are bright, and the primary color's darkness varies as described by
  /// primaryColorBrightness. The primaryColor does not contrast well with the
324
  /// card and canvas colors when the brightness is dark; when the brightness is
325
  /// dark, use Colors.white or the accentColor for a contrasting color.
326
  final Brightness brightness;
327

328
  /// The background color for major parts of the app (toolbars, tab bars, etc)
329
  final Color primaryColor;
330

331
  /// The brightness of the [primaryColor]. Used to determine the color of text and
332
  /// icons placed on top of the primary color (e.g. toolbar text).
333
  final Brightness primaryColorBrightness;
334

335 336 337 338 339 340
  /// A lighter version of the [primaryColor].
  final Color primaryColorLight;

  /// A darker version of the [primaryColor].
  final Color primaryColorDark;

341
  /// The foreground color for widgets (knobs, text, overscroll edge effect, etc).
342
  final Color accentColor;
343

344
  /// The brightness of the [accentColor]. Used to determine the color of text
345 346
  /// and icons placed on top of the accent color (e.g. the icons on a floating
  /// action button).
347
  final Brightness accentColorBrightness;
348

349
  /// The default color of [MaterialType.canvas] [Material].
350
  final Color canvasColor;
351

352 353 354 355
  /// The default color of the [Material] that underlies the [Scaffold]. The
  /// background color for a typical material app or a page within the app.
  final Color scaffoldBackgroundColor;

356 357
  /// The default color of the [BottomAppBar].
  ///
Josh Soref's avatar
Josh Soref committed
358
  /// This can be overridden by specifying [BottomAppBar.color].
359 360
  final Color bottomAppBarColor;

361
  /// The color of [Material] when it is used as a [Card].
362
  final Color cardColor;
363 364

  /// The color of [Divider]s and [PopupMenuDivider]s, also used
365
  /// between [ListTile]s, between rows in [DataTable]s, and so forth.
366 367 368
  ///
  /// To create an appropriate [BorderSide] that uses this color, consider
  /// [Divider.createBorderSide].
369
  final Color dividerColor;
370 371 372

  /// The highlight color used during ink splash animations or to
  /// indicate an item in a menu is selected.
373
  final Color highlightColor;
374 375

  /// The color of ink splashes. See [InkWell].
376
  final Color splashColor;
377

378 379 380 381 382 383 384
  /// Defines the appearance of ink splashes produces by [InkWell]
  /// and [InkResponse].
  ///
  /// See also:
  ///
  ///  * [InkSplash.splashFactory], which defines the default splash.
  ///  * [InkRipple.splashFactory], which defines a splash that spreads out
385
  ///    more aggressively than the default.
386 387
  final InteractiveInkFeatureFactory splashFactory;

388 389 390 391 392 393 394 395 396 397 398
  /// The color used to highlight selected rows.
  final Color selectedRowColor;

  /// The color used for widgets in their inactive (but enabled)
  /// state. For example, an unchecked checkbox. Usually contrasted
  /// with the [accentColor]. See also [disabledColor].
  final Color unselectedWidgetColor;

  /// The color used for widgets that are inoperative, regardless of
  /// their state. For example, a disabled checkbox (which may be
  /// checked or unchecked).
399
  final Color disabledColor;
400

401
  /// The default fill color of the [Material] used in [RaisedButton]s.
402
  final Color buttonColor;
403

404 405 406 407
  /// Defines the default configuration of button widgets, like [RaisedButton]
  /// and [FlatButton].
  final ButtonThemeData buttonTheme;

408 409 410 411 412 413
  /// The color of the header of a [PaginatedDataTable] when there are selected rows.
  // According to the spec for data tables:
  // https://material.google.com/components/data-tables.html#data-tables-tables-within-cards
  // ...this should be the "50-value of secondary app color".
  final Color secondaryHeaderColor;

414
  /// The color of text selections in text fields, such as [TextField].
415 416
  final Color textSelectionColor;

417
  /// The color of the handles used to adjust what part of the text is currently selected.
418 419
  final Color textSelectionHandleColor;

420 421
  /// A color that contrasts with the [primaryColor], e.g. used as the
  /// remaining part of a progress bar.
422 423
  final Color backgroundColor;

424 425
  /// The background color of [Dialog] elements.
  final Color dialogBackgroundColor;
426

427
  /// The color of the selected tab indicator in a tab bar.
428 429
  final Color indicatorColor;

430
  /// The color to use for hint text or placeholder text, e.g. in
431
  /// [TextField] fields.
432 433
  final Color hintColor;

434
  /// The color to use for input validation errors, e.g. in [TextField] fields.
435 436
  final Color errorColor;

437
  /// Text with a color that contrasts with the card and canvas colors.
438
  final TextTheme textTheme;
439 440 441 442

  /// A text theme that contrasts with the primary color.
  final TextTheme primaryTextTheme;

443 444 445
  /// A text theme that contrasts with the accent color.
  final TextTheme accentTextTheme;

446 447 448 449 450 451
  /// The default [InputDecoration] values for [InputDecorator], [TextField],
  /// and [TextFormField] are based on this theme.
  ///
  /// See [InputDecoration.applyDefaults].
  final InputDecorationTheme inputDecorationTheme;

Ian Hickson's avatar
Ian Hickson committed
452 453 454
  /// An icon theme that contrasts with the card and canvas colors.
  final IconThemeData iconTheme;

455
  /// An icon theme that contrasts with the primary color.
456 457
  final IconThemeData primaryIconTheme;

458 459 460
  /// An icon theme that contrasts with the accent color.
  final IconThemeData accentIconTheme;

461 462 463 464 465
  /// The colors and shapes used to render [Slider].
  ///
  /// This is the value returned from [SliderTheme.of].
  final SliderThemeData sliderTheme;

466 467 468 469 470
  /// The platform the material widgets should adapt to target.
  ///
  /// Defaults to the current platform.
  final TargetPlatform platform;

471 472 473 474 475
  /// Creates a copy of this theme but with the given fields replaced with the new values.
  ThemeData copyWith({
    Brightness brightness,
    Color primaryColor,
    Brightness primaryColorBrightness,
476 477
    Color primaryColorLight,
    Color primaryColorDark,
478 479 480
    Color accentColor,
    Brightness accentColorBrightness,
    Color canvasColor,
481
    Color scaffoldBackgroundColor,
482
    Color bottomAppBarColor,
483 484 485 486
    Color cardColor,
    Color dividerColor,
    Color highlightColor,
    Color splashColor,
487
    InteractiveInkFeatureFactory splashFactory,
488 489 490 491
    Color selectedRowColor,
    Color unselectedWidgetColor,
    Color disabledColor,
    Color buttonColor,
492
    ButtonThemeData buttonTheme,
493 494 495 496
    Color secondaryHeaderColor,
    Color textSelectionColor,
    Color textSelectionHandleColor,
    Color backgroundColor,
497
    Color dialogBackgroundColor,
498 499 500 501 502
    Color indicatorColor,
    Color hintColor,
    Color errorColor,
    TextTheme textTheme,
    TextTheme primaryTextTheme,
503
    TextTheme accentTextTheme,
504
    InputDecorationTheme inputDecorationTheme,
505 506
    IconThemeData iconTheme,
    IconThemeData primaryIconTheme,
507
    IconThemeData accentIconTheme,
508
    SliderThemeData sliderTheme,
509 510
    TargetPlatform platform,
  }) {
511 512 513 514
    return new ThemeData.raw(
      brightness: brightness ?? this.brightness,
      primaryColor: primaryColor ?? this.primaryColor,
      primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness,
515 516
      primaryColorLight: primaryColorLight ?? this.primaryColorLight,
      primaryColorDark: primaryColorDark ?? this.primaryColorDark,
517 518 519 520
      accentColor: accentColor ?? this.accentColor,
      accentColorBrightness: accentColorBrightness ?? this.accentColorBrightness,
      canvasColor: canvasColor ?? this.canvasColor,
      scaffoldBackgroundColor: scaffoldBackgroundColor ?? this.scaffoldBackgroundColor,
521
      bottomAppBarColor: bottomAppBarColor ?? this.bottomAppBarColor,
522 523 524 525
      cardColor: cardColor ?? this.cardColor,
      dividerColor: dividerColor ?? this.dividerColor,
      highlightColor: highlightColor ?? this.highlightColor,
      splashColor: splashColor ?? this.splashColor,
526
      splashFactory: splashFactory ?? this.splashFactory,
527 528 529 530
      selectedRowColor: selectedRowColor ?? this.selectedRowColor,
      unselectedWidgetColor: unselectedWidgetColor ?? this.unselectedWidgetColor,
      disabledColor: disabledColor ?? this.disabledColor,
      buttonColor: buttonColor ?? this.buttonColor,
531
      buttonTheme: buttonTheme ?? this.buttonTheme,
532 533 534 535 536 537 538 539 540 541 542
      secondaryHeaderColor: secondaryHeaderColor ?? this.secondaryHeaderColor,
      textSelectionColor: textSelectionColor ?? this.textSelectionColor,
      textSelectionHandleColor: textSelectionHandleColor ?? this.textSelectionHandleColor,
      backgroundColor: backgroundColor ?? this.backgroundColor,
      dialogBackgroundColor: dialogBackgroundColor ?? this.dialogBackgroundColor,
      indicatorColor: indicatorColor ?? this.indicatorColor,
      hintColor: hintColor ?? this.hintColor,
      errorColor: errorColor ?? this.errorColor,
      textTheme: textTheme ?? this.textTheme,
      primaryTextTheme: primaryTextTheme ?? this.primaryTextTheme,
      accentTextTheme: accentTextTheme ?? this.accentTextTheme,
543
      inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
544 545 546
      iconTheme: iconTheme ?? this.iconTheme,
      primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
      accentIconTheme: accentIconTheme ?? this.accentIconTheme,
547
      sliderTheme: sliderTheme ?? this.sliderTheme,
548
      platform: platform ?? this.platform,
549 550 551
    );
  }

552 553 554 555 556 557 558
  // The number 5 was chosen without any real science or research behind it. It
  // just seemed like a number that's not too big (we should be able to fit 5
  // copies of ThemeData in memory comfortably) and not too small (most apps
  // shouldn't have more than 5 theme/localization pairs).
  static const int _localizedThemeDataCacheSize = 5;

  /// Caches localized themes to speed up the [localize] method.
559 560
  static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache =
      new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
561

562 563
  /// Returns a new theme built by merging the text geometry provided by the
  /// [localTextGeometry] theme with the [baseTheme].
564
  ///
565 566 567 568
  /// For those text styles in the [baseTheme] whose [TextStyle.inherit] is set
  /// to true, the returned theme's text styles inherit the geometric properties
  /// of [localTextGeometry]. The resulting text styles' [TextStyle.inherit] is
  /// set to those provided by [localTextGeometry].
569
  static ThemeData localize(ThemeData baseTheme, TextTheme localTextGeometry) {
570 571 572 573 574 575 576 577 578 579 580 581
    // WARNING: this method memoizes the result in a cache based on the
    // previously seen baseTheme and localTextGeometry. Memoization is safe
    // because all inputs and outputs of this function are deeply immutable, and
    // the computations are referentially transparent. It only short-circuits
    // the computation if the new inputs are identical() to the previous ones.
    // It does not use the == operator, which performs a costly deep comparison.
    //
    // When changing this method, make sure the memoization logic is correct.
    // Remember:
    //
    // There are only two hard things in Computer Science: cache invalidation
    // and naming things. -- Phil Karlton
582 583
    assert(baseTheme != null);
    assert(localTextGeometry != null);
584 585 586 587 588 589 590 591 592 593 594

    return _localizedThemeDataCache.putIfAbsent(
      new _IdentityThemeDataCacheKey(baseTheme, localTextGeometry),
      () {
        return baseTheme.copyWith(
          primaryTextTheme: localTextGeometry.merge(baseTheme.primaryTextTheme),
          accentTextTheme: localTextGeometry.merge(baseTheme.accentTextTheme),
          textTheme: localTextGeometry.merge(baseTheme.textTheme),
        );
      },
    );
595 596
  }

597 598 599 600 601 602
  /// Determines whether the given [Color] is [Brightness.light] or
  /// [Brightness.dark].
  ///
  /// This compares the luminosity of the given color to a threshold value that
  /// matches the material design specification.
  static Brightness estimateBrightnessForColor(Color color) {
603
    final double relativeLuminance = color.computeLuminance();
604 605 606 607 608 609 610 611

    // See <https://www.w3.org/TR/WCAG20/#contrast-ratiodef>
    // The spec says to use kThreshold=0.0525, but Material Design appears to bias
    // more towards using light text than WCAG20 recommends. Material Design spec
    // doesn't say what value to use, but 0.15 seemed close to what the Material
    // Design spec shows for its color palette on
    // <https://material.io/guidelines/style/color.html#color-color-palette>.
    const double kThreshold = 0.15;
612
    if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold)
613 614 615 616
      return Brightness.light;
    return Brightness.dark;
  }

617
  /// Linearly interpolate between two themes.
618 619
  ///
  /// The arguments must not be null.
620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
  ///
  /// The `t` argument represents position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  static ThemeData lerp(ThemeData a, ThemeData b, double t) {
    assert(a != null);
    assert(b != null);
    assert(t != null);
636
    return new ThemeData.raw(
637 638 639
      brightness: t < 0.5 ? a.brightness : b.brightness,
      primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t),
      primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness,
640 641
      primaryColorLight: Color.lerp(a.primaryColorLight, b.primaryColorLight, t),
      primaryColorDark: Color.lerp(a.primaryColorDark, b.primaryColorDark, t),
642 643
      canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t),
      scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t),
644
      bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
645 646 647 648
      cardColor: Color.lerp(a.cardColor, b.cardColor, t),
      dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
      highlightColor: Color.lerp(a.highlightColor, b.highlightColor, t),
      splashColor: Color.lerp(a.splashColor, b.splashColor, t),
649
      splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
650 651 652 653
      selectedRowColor: Color.lerp(a.selectedRowColor, b.selectedRowColor, t),
      unselectedWidgetColor: Color.lerp(a.unselectedWidgetColor, b.unselectedWidgetColor, t),
      disabledColor: Color.lerp(a.disabledColor, b.disabledColor, t),
      buttonColor: Color.lerp(a.buttonColor, b.buttonColor, t),
654
      buttonTheme: t < 0.5 ? a.buttonTheme : b.buttonTheme,
655 656 657 658 659 660 661 662 663 664 665 666 667
      secondaryHeaderColor: Color.lerp(a.secondaryHeaderColor, b.secondaryHeaderColor, t),
      textSelectionColor: Color.lerp(a.textSelectionColor, b.textSelectionColor, t),
      textSelectionHandleColor: Color.lerp(a.textSelectionHandleColor, b.textSelectionHandleColor, t),
      backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
      dialogBackgroundColor: Color.lerp(a.dialogBackgroundColor, b.dialogBackgroundColor, t),
      accentColor: Color.lerp(a.accentColor, b.accentColor, t),
      accentColorBrightness: t < 0.5 ? a.accentColorBrightness : b.accentColorBrightness,
      indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
      hintColor: Color.lerp(a.hintColor, b.hintColor, t),
      errorColor: Color.lerp(a.errorColor, b.errorColor, t),
      textTheme: TextTheme.lerp(a.textTheme, b.textTheme, t),
      primaryTextTheme: TextTheme.lerp(a.primaryTextTheme, b.primaryTextTheme, t),
      accentTextTheme: TextTheme.lerp(a.accentTextTheme, b.accentTextTheme, t),
668
      inputDecorationTheme: t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
669 670 671
      iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
      primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t),
      accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
672
      sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
673
      platform: t < 0.5 ? a.platform : b.platform,
674 675 676
    );
  }

677
  @override
678
  bool operator ==(Object other) {
679 680
    if (other.runtimeType != runtimeType)
      return false;
681
    final ThemeData otherData = other;
682
    return (otherData.brightness == brightness) &&
683 684
           (otherData.primaryColor == primaryColor) &&
           (otherData.primaryColorBrightness == primaryColorBrightness) &&
685
           (otherData.canvasColor == canvasColor) &&
686
           (otherData.scaffoldBackgroundColor == scaffoldBackgroundColor) &&
687
           (otherData.bottomAppBarColor == bottomAppBarColor) &&
688 689 690
           (otherData.cardColor == cardColor) &&
           (otherData.dividerColor == dividerColor) &&
           (otherData.highlightColor == highlightColor) &&
691
           (otherData.splashColor == splashColor) &&
692
           (otherData.splashFactory == splashFactory) &&
693 694
           (otherData.selectedRowColor == selectedRowColor) &&
           (otherData.unselectedWidgetColor == unselectedWidgetColor) &&
695
           (otherData.disabledColor == disabledColor) &&
696
           (otherData.buttonColor == buttonColor) &&
697
           (otherData.buttonTheme == buttonTheme) &&
698
           (otherData.secondaryHeaderColor == secondaryHeaderColor) &&
699
           (otherData.textSelectionColor == textSelectionColor) &&
700
           (otherData.textSelectionHandleColor == textSelectionHandleColor) &&
701
           (otherData.backgroundColor == backgroundColor) &&
702
           (otherData.dialogBackgroundColor == dialogBackgroundColor) &&
703 704 705 706
           (otherData.accentColor == accentColor) &&
           (otherData.accentColorBrightness == accentColorBrightness) &&
           (otherData.indicatorColor == indicatorColor) &&
           (otherData.hintColor == hintColor) &&
707
           (otherData.errorColor == errorColor) &&
708
           (otherData.textTheme == textTheme) &&
709
           (otherData.primaryTextTheme == primaryTextTheme) &&
710
           (otherData.accentTextTheme == accentTextTheme) &&
711
           (otherData.inputDecorationTheme == inputDecorationTheme) &&
Ian Hickson's avatar
Ian Hickson committed
712
           (otherData.iconTheme == iconTheme) &&
713
           (otherData.primaryIconTheme == primaryIconTheme) &&
714
           (otherData.accentIconTheme == accentIconTheme) &&
715
           (otherData.sliderTheme == sliderTheme) &&
716
           (otherData.platform == platform);
717
  }
718 719

  @override
720
  int get hashCode {
721
    return hashValues(
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758
      brightness,
      primaryColor,
      primaryColorBrightness,
      canvasColor,
      scaffoldBackgroundColor,
      bottomAppBarColor,
      cardColor,
      dividerColor,
      highlightColor,
      splashColor,
      splashFactory,
      selectedRowColor,
      unselectedWidgetColor,
      disabledColor,
      buttonColor,
      buttonTheme,
      secondaryHeaderColor,
      textSelectionColor,
      textSelectionHandleColor,
      hashValues(  // Too many values.
        backgroundColor,
        accentColor,
        accentColorBrightness,
        indicatorColor,
        dialogBackgroundColor,
        hintColor,
        errorColor,
        textTheme,
        primaryTextTheme,
        accentTextTheme,
        iconTheme,
        inputDecorationTheme,
        primaryIconTheme,
        accentIconTheme,
        sliderTheme,
        platform,
      ),
759
    );
760
  }
Hixie's avatar
Hixie committed
761

762
  @override
763 764
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
765
    final ThemeData defaultData = new ThemeData.fallback();
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
    properties.add(new EnumProperty<TargetPlatform>('platform', platform, defaultValue: defaultTargetPlatform));
    properties.add(new EnumProperty<Brightness>('brightness', brightness, defaultValue: defaultData.brightness));
    properties.add(new DiagnosticsProperty<Color>('primaryColor', primaryColor, defaultValue: defaultData.primaryColor));
    properties.add(new EnumProperty<Brightness>('primaryColorBrightness', primaryColorBrightness, defaultValue: defaultData.primaryColorBrightness));
    properties.add(new DiagnosticsProperty<Color>('accentColor', accentColor, defaultValue: defaultData.accentColor));
    properties.add(new EnumProperty<Brightness>('accentColorBrightness', accentColorBrightness, defaultValue: defaultData.accentColorBrightness));
    properties.add(new DiagnosticsProperty<Color>('canvasColor', canvasColor, defaultValue: defaultData.canvasColor));
    properties.add(new DiagnosticsProperty<Color>('scaffoldBackgroundColor', scaffoldBackgroundColor, defaultValue: defaultData.scaffoldBackgroundColor));
    properties.add(new DiagnosticsProperty<Color>('bottomAppBarColor', bottomAppBarColor, defaultValue: defaultData.bottomAppBarColor));
    properties.add(new DiagnosticsProperty<Color>('cardColor', cardColor, defaultValue: defaultData.cardColor));
    properties.add(new DiagnosticsProperty<Color>('dividerColor', dividerColor, defaultValue: defaultData.dividerColor));
    properties.add(new DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: defaultData.highlightColor));
    properties.add(new DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: defaultData.splashColor));
    properties.add(new DiagnosticsProperty<Color>('selectedRowColor', selectedRowColor, defaultValue: defaultData.selectedRowColor));
    properties.add(new DiagnosticsProperty<Color>('unselectedWidgetColor', unselectedWidgetColor, defaultValue: defaultData.unselectedWidgetColor));
    properties.add(new DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: defaultData.disabledColor));
    properties.add(new DiagnosticsProperty<Color>('buttonColor', buttonColor, defaultValue: defaultData.buttonColor));
    properties.add(new DiagnosticsProperty<Color>('secondaryHeaderColor', secondaryHeaderColor, defaultValue: defaultData.secondaryHeaderColor));
    properties.add(new DiagnosticsProperty<Color>('textSelectionColor', textSelectionColor, defaultValue: defaultData.textSelectionColor));
    properties.add(new DiagnosticsProperty<Color>('textSelectionHandleColor', textSelectionHandleColor, defaultValue: defaultData.textSelectionHandleColor));
    properties.add(new DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
    properties.add(new DiagnosticsProperty<Color>('dialogBackgroundColor', dialogBackgroundColor, defaultValue: defaultData.dialogBackgroundColor));
    properties.add(new DiagnosticsProperty<Color>('indicatorColor', indicatorColor, defaultValue: defaultData.indicatorColor));
    properties.add(new DiagnosticsProperty<Color>('hintColor', hintColor, defaultValue: defaultData.hintColor));
    properties.add(new DiagnosticsProperty<Color>('errorColor', errorColor, defaultValue: defaultData.errorColor));
    properties.add(new DiagnosticsProperty<ButtonThemeData>('buttonTheme', buttonTheme));
    properties.add(new DiagnosticsProperty<TextTheme>('textTheme', textTheme));
    properties.add(new DiagnosticsProperty<TextTheme>('primaryTextTheme', primaryTextTheme));
    properties.add(new DiagnosticsProperty<TextTheme>('accentTextTheme', accentTextTheme));
    properties.add(new DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme));
    properties.add(new DiagnosticsProperty<IconThemeData>('iconTheme', iconTheme));
    properties.add(new DiagnosticsProperty<IconThemeData>('primaryIconTheme', primaryIconTheme));
    properties.add(new DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme));
    properties.add(new DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
800
  }
801
}
802

803 804
class _IdentityThemeDataCacheKey {
  _IdentityThemeDataCacheKey(this.baseTheme, this.localTextGeometry);
805

806
  final ThemeData baseTheme;
807 808
  final TextTheme localTextGeometry;

809 810
  // Using XOR to make the hash function as fast as possible (e.g. Jenkins is
  // noticeably slower).
811
  @override
812
  int get hashCode => identityHashCode(baseTheme) ^ identityHashCode(localTextGeometry);
813 814

  @override
815 816 817 818
  bool operator ==(Object other) {
    // We are explicitly ignoring the possibility that the types might not
    // match in the interests of speed.
    final _IdentityThemeDataCacheKey otherKey = other;
819
    return identical(baseTheme, otherKey.baseTheme) && identical(localTextGeometry, otherKey.localTextGeometry);
820 821 822
  }
}

823 824 825 826 827 828
/// Cache of objects of limited size that uses the first in first out eviction
/// strategy (a.k.a least recently inserted).
///
/// The key that was inserted before all other keys is evicted first, i.e. the
/// one inserted least recently.
class _FifoCache<K, V> {
829
  _FifoCache(this._maximumSize) : assert(_maximumSize != null && _maximumSize > 0);
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855

  /// In Dart the map literal uses a linked hash-map implementation, whose keys
  /// are stored such that [Map.keys] returns them in the order they were
  /// inserted.
  final Map<K, V> _cache = <K, V>{};

  /// Maximum number of entries to store in the cache.
  ///
  /// Once this many entries have been cached, the entry inserted least recently
  /// is evicted when adding a new entry.
  final int _maximumSize;

  /// Returns the previously cached value for the given key, if available;
  /// if not, calls the given callback to obtain it first.
  ///
  /// The arguments must not be null.
  V putIfAbsent(K key, V loader()) {
    assert(key != null);
    assert(loader != null);
    final V result = _cache[key];
    if (result != null)
      return result;
    if (_cache.length == _maximumSize)
      _cache.remove(_cache.keys.first);
    return _cache[key] = loader();
  }
856
}