theme_data.dart 38.9 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 'chip_theme.dart';
12
import 'colors.dart';
13 14
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
15
import 'input_decorator.dart';
16
import 'slider_theme.dart';
Adam Barth's avatar
Adam Barth committed
17
import 'typography.dart';
18

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
/// 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,
}

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
// 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);

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

220
  /// Create a [ThemeData] given a set of exact values. All the values
221 222 223 224 225
  /// 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.
226
  const ThemeData.raw({
227 228 229
    @required this.brightness,
    @required this.primaryColor,
    @required this.primaryColorBrightness,
230 231
    @required this.primaryColorLight,
    @required this.primaryColorDark,
232 233 234 235
    @required this.accentColor,
    @required this.accentColorBrightness,
    @required this.canvasColor,
    @required this.scaffoldBackgroundColor,
236
    @required this.bottomAppBarColor,
237 238 239 240
    @required this.cardColor,
    @required this.dividerColor,
    @required this.highlightColor,
    @required this.splashColor,
241
    @required this.splashFactory,
242 243 244 245
    @required this.selectedRowColor,
    @required this.unselectedWidgetColor,
    @required this.disabledColor,
    @required this.buttonColor,
246
    @required this.buttonTheme,
247 248 249 250 251 252 253 254 255 256 257
    @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,
258
    @required this.inputDecorationTheme,
259 260 261
    @required this.iconTheme,
    @required this.primaryIconTheme,
    @required this.accentIconTheme,
262
    @required this.sliderTheme,
263
    @required this.chipTheme,
264
    @required this.platform,
265 266 267
  }) : assert(brightness != null),
       assert(primaryColor != null),
       assert(primaryColorBrightness != null),
268 269
       assert(primaryColorLight != null),
       assert(primaryColorDark != null),
270 271 272 273
       assert(accentColor != null),
       assert(accentColorBrightness != null),
       assert(canvasColor != null),
       assert(scaffoldBackgroundColor != null),
274
       assert(bottomAppBarColor != null),
275 276 277 278
       assert(cardColor != null),
       assert(dividerColor != null),
       assert(highlightColor != null),
       assert(splashColor != null),
279
       assert(splashFactory != null),
280 281 282
       assert(selectedRowColor != null),
       assert(unselectedWidgetColor != null),
       assert(disabledColor != null),
283
       assert(buttonTheme != null),
284 285 286 287 288 289 290 291 292 293 294
       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),
295
       assert(inputDecorationTheme != null),
296 297 298
       assert(iconTheme != null),
       assert(primaryIconTheme != null),
       assert(accentIconTheme != null),
299
       assert(sliderTheme != null),
300
       assert(chipTheme != null),
301
       assert(platform != null);
Ian Hickson's avatar
Ian Hickson committed
302

303
  /// A default light blue theme.
304 305 306
  ///
  /// This theme does not contain text geometry. Instead, it is expected that
  /// this theme is localized using text geometry using [ThemeData.localize].
307
  factory ThemeData.light() => new ThemeData(brightness: Brightness.light);
308 309

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

315
  /// The default color theme. Same as [new ThemeData.light].
316 317
  ///
  /// This is used by [Theme.of] when no theme has been specified.
318 319 320 321 322 323
  ///
  /// 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.
324 325
  factory ThemeData.fallback() => new ThemeData.light();

326 327 328
  /// 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.
329
  ///
330 331
  /// When the [Brightness] is dark, the canvas, card, and primary colors are
  /// all dark. When the [Brightness] is light, the canvas and card colors
332 333
  /// are bright, and the primary color's darkness varies as described by
  /// primaryColorBrightness. The primaryColor does not contrast well with the
334
  /// card and canvas colors when the brightness is dark; when the brightness is
335
  /// dark, use Colors.white or the accentColor for a contrasting color.
336
  final Brightness brightness;
337

338
  /// The background color for major parts of the app (toolbars, tab bars, etc)
339
  final Color primaryColor;
340

341
  /// The brightness of the [primaryColor]. Used to determine the color of text and
342
  /// icons placed on top of the primary color (e.g. toolbar text).
343
  final Brightness primaryColorBrightness;
344

345 346 347 348 349 350
  /// A lighter version of the [primaryColor].
  final Color primaryColorLight;

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

351
  /// The foreground color for widgets (knobs, text, overscroll edge effect, etc).
352
  final Color accentColor;
353

354
  /// The brightness of the [accentColor]. Used to determine the color of text
355 356
  /// and icons placed on top of the accent color (e.g. the icons on a floating
  /// action button).
357
  final Brightness accentColorBrightness;
358

359
  /// The default color of [MaterialType.canvas] [Material].
360
  final Color canvasColor;
361

362 363 364 365
  /// 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;

366 367
  /// The default color of the [BottomAppBar].
  ///
Josh Soref's avatar
Josh Soref committed
368
  /// This can be overridden by specifying [BottomAppBar.color].
369 370
  final Color bottomAppBarColor;

371
  /// The color of [Material] when it is used as a [Card].
372
  final Color cardColor;
373 374

  /// The color of [Divider]s and [PopupMenuDivider]s, also used
375
  /// between [ListTile]s, between rows in [DataTable]s, and so forth.
376 377 378
  ///
  /// To create an appropriate [BorderSide] that uses this color, consider
  /// [Divider.createBorderSide].
379
  final Color dividerColor;
380 381 382

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

  /// The color of ink splashes. See [InkWell].
386
  final Color splashColor;
387

388 389 390 391 392 393 394
  /// 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
395
  ///    more aggressively than the default.
396 397
  final InteractiveInkFeatureFactory splashFactory;

398 399 400 401 402 403 404 405 406 407 408
  /// 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).
409
  final Color disabledColor;
410

411
  /// The default fill color of the [Material] used in [RaisedButton]s.
412
  final Color buttonColor;
413

414 415 416 417
  /// Defines the default configuration of button widgets, like [RaisedButton]
  /// and [FlatButton].
  final ButtonThemeData buttonTheme;

418 419 420 421 422 423
  /// 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;

424
  /// The color of text selections in text fields, such as [TextField].
425 426
  final Color textSelectionColor;

427
  /// The color of the handles used to adjust what part of the text is currently selected.
428 429
  final Color textSelectionHandleColor;

430 431
  /// A color that contrasts with the [primaryColor], e.g. used as the
  /// remaining part of a progress bar.
432 433
  final Color backgroundColor;

434 435
  /// The background color of [Dialog] elements.
  final Color dialogBackgroundColor;
436

437
  /// The color of the selected tab indicator in a tab bar.
438 439
  final Color indicatorColor;

440
  /// The color to use for hint text or placeholder text, e.g. in
441
  /// [TextField] fields.
442 443
  final Color hintColor;

444
  /// The color to use for input validation errors, e.g. in [TextField] fields.
445 446
  final Color errorColor;

447
  /// Text with a color that contrasts with the card and canvas colors.
448
  final TextTheme textTheme;
449 450 451 452

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

453 454 455
  /// A text theme that contrasts with the accent color.
  final TextTheme accentTextTheme;

456 457 458 459 460 461
  /// 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
462 463 464
  /// An icon theme that contrasts with the card and canvas colors.
  final IconThemeData iconTheme;

465
  /// An icon theme that contrasts with the primary color.
466 467
  final IconThemeData primaryIconTheme;

468 469 470
  /// An icon theme that contrasts with the accent color.
  final IconThemeData accentIconTheme;

471 472 473 474 475
  /// The colors and shapes used to render [Slider].
  ///
  /// This is the value returned from [SliderTheme.of].
  final SliderThemeData sliderTheme;

476 477 478 479 480
  /// The colors and styles used to render [Chip], [
  ///
  /// This is the value returned from [ChipTheme.of].
  final ChipThemeData chipTheme;

481 482 483 484 485
  /// The platform the material widgets should adapt to target.
  ///
  /// Defaults to the current platform.
  final TargetPlatform platform;

486 487 488 489 490
  /// Creates a copy of this theme but with the given fields replaced with the new values.
  ThemeData copyWith({
    Brightness brightness,
    Color primaryColor,
    Brightness primaryColorBrightness,
491 492
    Color primaryColorLight,
    Color primaryColorDark,
493 494 495
    Color accentColor,
    Brightness accentColorBrightness,
    Color canvasColor,
496
    Color scaffoldBackgroundColor,
497
    Color bottomAppBarColor,
498 499 500 501
    Color cardColor,
    Color dividerColor,
    Color highlightColor,
    Color splashColor,
502
    InteractiveInkFeatureFactory splashFactory,
503 504 505 506
    Color selectedRowColor,
    Color unselectedWidgetColor,
    Color disabledColor,
    Color buttonColor,
507
    ButtonThemeData buttonTheme,
508 509 510 511
    Color secondaryHeaderColor,
    Color textSelectionColor,
    Color textSelectionHandleColor,
    Color backgroundColor,
512
    Color dialogBackgroundColor,
513 514 515 516 517
    Color indicatorColor,
    Color hintColor,
    Color errorColor,
    TextTheme textTheme,
    TextTheme primaryTextTheme,
518
    TextTheme accentTextTheme,
519
    InputDecorationTheme inputDecorationTheme,
520 521
    IconThemeData iconTheme,
    IconThemeData primaryIconTheme,
522
    IconThemeData accentIconTheme,
523
    SliderThemeData sliderTheme,
524
    ChipThemeData chipTheme,
525 526
    TargetPlatform platform,
  }) {
527 528 529 530
    return new ThemeData.raw(
      brightness: brightness ?? this.brightness,
      primaryColor: primaryColor ?? this.primaryColor,
      primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness,
531 532
      primaryColorLight: primaryColorLight ?? this.primaryColorLight,
      primaryColorDark: primaryColorDark ?? this.primaryColorDark,
533 534 535 536
      accentColor: accentColor ?? this.accentColor,
      accentColorBrightness: accentColorBrightness ?? this.accentColorBrightness,
      canvasColor: canvasColor ?? this.canvasColor,
      scaffoldBackgroundColor: scaffoldBackgroundColor ?? this.scaffoldBackgroundColor,
537
      bottomAppBarColor: bottomAppBarColor ?? this.bottomAppBarColor,
538 539 540 541
      cardColor: cardColor ?? this.cardColor,
      dividerColor: dividerColor ?? this.dividerColor,
      highlightColor: highlightColor ?? this.highlightColor,
      splashColor: splashColor ?? this.splashColor,
542
      splashFactory: splashFactory ?? this.splashFactory,
543 544 545 546
      selectedRowColor: selectedRowColor ?? this.selectedRowColor,
      unselectedWidgetColor: unselectedWidgetColor ?? this.unselectedWidgetColor,
      disabledColor: disabledColor ?? this.disabledColor,
      buttonColor: buttonColor ?? this.buttonColor,
547
      buttonTheme: buttonTheme ?? this.buttonTheme,
548 549 550 551 552 553 554 555 556 557 558
      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,
559
      inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
560 561 562
      iconTheme: iconTheme ?? this.iconTheme,
      primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
      accentIconTheme: accentIconTheme ?? this.accentIconTheme,
563
      sliderTheme: sliderTheme ?? this.sliderTheme,
564
      chipTheme: chipTheme ?? this.chipTheme,
565
      platform: platform ?? this.platform,
566 567 568
    );
  }

569 570 571 572 573 574 575
  // 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.
576 577
  static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache =
      new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
578

579 580
  /// Returns a new theme built by merging the text geometry provided by the
  /// [localTextGeometry] theme with the [baseTheme].
581
  ///
582 583 584 585
  /// 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].
586
  static ThemeData localize(ThemeData baseTheme, TextTheme localTextGeometry) {
587 588 589 590 591 592 593 594 595 596 597 598
    // 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
599 600
    assert(baseTheme != null);
    assert(localTextGeometry != null);
601 602 603 604 605 606 607 608 609 610 611

    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),
        );
      },
    );
612 613
  }

614 615 616 617 618 619
  /// 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) {
620
    final double relativeLuminance = color.computeLuminance();
621 622 623 624 625 626

    // 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
627
    // <https://material.io/go/design-theming#color-color-palette>.
628
    const double kThreshold = 0.15;
629
    if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold)
630 631 632 633
      return Brightness.light;
    return Brightness.dark;
  }

634
  /// Linearly interpolate between two themes.
635 636
  ///
  /// The arguments must not be null.
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
  ///
  /// 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);
653
    return new ThemeData.raw(
654 655 656
      brightness: t < 0.5 ? a.brightness : b.brightness,
      primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t),
      primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness,
657 658
      primaryColorLight: Color.lerp(a.primaryColorLight, b.primaryColorLight, t),
      primaryColorDark: Color.lerp(a.primaryColorDark, b.primaryColorDark, t),
659 660
      canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t),
      scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t),
661
      bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
662 663 664 665
      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),
666
      splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
667 668 669 670
      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),
671
      buttonTheme: t < 0.5 ? a.buttonTheme : b.buttonTheme,
672 673 674 675 676 677 678 679 680 681 682 683 684
      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),
685
      inputDecorationTheme: t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
686 687 688
      iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
      primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t),
      accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
689
      sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
690
      chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t),
691
      platform: t < 0.5 ? a.platform : b.platform,
692 693 694
    );
  }

695
  @override
696
  bool operator ==(Object other) {
697 698
    if (other.runtimeType != runtimeType)
      return false;
699
    final ThemeData otherData = other;
700
    return (otherData.brightness == brightness) &&
701 702
           (otherData.primaryColor == primaryColor) &&
           (otherData.primaryColorBrightness == primaryColorBrightness) &&
703
           (otherData.canvasColor == canvasColor) &&
704
           (otherData.scaffoldBackgroundColor == scaffoldBackgroundColor) &&
705
           (otherData.bottomAppBarColor == bottomAppBarColor) &&
706 707 708
           (otherData.cardColor == cardColor) &&
           (otherData.dividerColor == dividerColor) &&
           (otherData.highlightColor == highlightColor) &&
709
           (otherData.splashColor == splashColor) &&
710
           (otherData.splashFactory == splashFactory) &&
711 712
           (otherData.selectedRowColor == selectedRowColor) &&
           (otherData.unselectedWidgetColor == unselectedWidgetColor) &&
713
           (otherData.disabledColor == disabledColor) &&
714
           (otherData.buttonColor == buttonColor) &&
715
           (otherData.buttonTheme == buttonTheme) &&
716
           (otherData.secondaryHeaderColor == secondaryHeaderColor) &&
717
           (otherData.textSelectionColor == textSelectionColor) &&
718
           (otherData.textSelectionHandleColor == textSelectionHandleColor) &&
719
           (otherData.backgroundColor == backgroundColor) &&
720
           (otherData.dialogBackgroundColor == dialogBackgroundColor) &&
721 722 723 724
           (otherData.accentColor == accentColor) &&
           (otherData.accentColorBrightness == accentColorBrightness) &&
           (otherData.indicatorColor == indicatorColor) &&
           (otherData.hintColor == hintColor) &&
725
           (otherData.errorColor == errorColor) &&
726
           (otherData.textTheme == textTheme) &&
727
           (otherData.primaryTextTheme == primaryTextTheme) &&
728
           (otherData.accentTextTheme == accentTextTheme) &&
729
           (otherData.inputDecorationTheme == inputDecorationTheme) &&
Ian Hickson's avatar
Ian Hickson committed
730
           (otherData.iconTheme == iconTheme) &&
731
           (otherData.primaryIconTheme == primaryIconTheme) &&
732
           (otherData.accentIconTheme == accentIconTheme) &&
733
           (otherData.sliderTheme == sliderTheme) &&
734
           (otherData.chipTheme == chipTheme) &&
735
           (otherData.platform == platform);
736
  }
737 738

  @override
739
  int get hashCode {
740
    return hashValues(
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775
      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,
776
        chipTheme,
777 778
        platform,
      ),
779
    );
780
  }
Hixie's avatar
Hixie committed
781

782
  @override
783 784
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
785
    final ThemeData defaultData = new ThemeData.fallback();
786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
    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));
820
    properties.add(new DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
821
  }
822
}
823

824 825
class _IdentityThemeDataCacheKey {
  _IdentityThemeDataCacheKey(this.baseTheme, this.localTextGeometry);
826

827
  final ThemeData baseTheme;
828 829
  final TextTheme localTextGeometry;

830 831
  // Using XOR to make the hash function as fast as possible (e.g. Jenkins is
  // noticeably slower).
832
  @override
833
  int get hashCode => identityHashCode(baseTheme) ^ identityHashCode(localTextGeometry);
834 835

  @override
836 837 838 839
  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;
840
    return identical(baseTheme, otherKey.baseTheme) && identical(localTextGeometry, otherKey.localTextGeometry);
841 842 843
  }
}

844 845 846 847 848 849
/// 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> {
850
  _FifoCache(this._maximumSize) : assert(_maximumSize != null && _maximumSize > 0);
851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876

  /// 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();
  }
877
}