Introduce Theme extensions (#98033)
* first pass * x * x * address feedback * support multiple extensions * add convenience function, Object ⇒ dynamic, lerping * remove not-useful comment * fix examples/api lower sdk constraint * remove trailing spaces * remove another pesky trailing space * improve lerp * address feedback * hide map implementation from constructor and copyWith * use iterableproperty * Revert "hide map implementation from constructor and copyWith" This reverts commit a6994af0046e3c90dbc9405cac628feb5b2d3031. * slow down sample * make theme extension params required * add null check * improve documentation * fix hashCode and operator == overrides * modify existing tests * remove trailing spaces * add all tests except lerping * fix lerping bug * add toString to themeExtension example * add lerping test * assume non-nullability in example * address feedback * update docs * remove trailing space * use Map.unmodifiable
This commit is contained in:
parent
6af40a7004
commit
8c1c2f6af5
126
examples/api/lib/material/theme/theme_extension.1.dart
Normal file
126
examples/api/lib/material/theme/theme_extension.1.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Flutter code sample for ThemeExtension
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MyColors extends ThemeExtension<MyColors> {
|
||||||
|
const MyColors({
|
||||||
|
required this.blue,
|
||||||
|
required this.red,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color? blue;
|
||||||
|
final Color? red;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyColors copyWith({Color? red, Color? blue}) {
|
||||||
|
return MyColors(
|
||||||
|
blue: blue ?? this.blue,
|
||||||
|
red: red ?? this.red,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyColors lerp(ThemeExtension<MyColors>? other, double t) {
|
||||||
|
if (other is! MyColors) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return MyColors(
|
||||||
|
blue: Color.lerp(blue, other.blue, t),
|
||||||
|
red: Color.lerp(red, other.red, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
@override
|
||||||
|
String toString() => 'MyColors(blue: $blue, red: $red)';
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Slow down time to see lerping.
|
||||||
|
timeDilation = 5.0;
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatefulWidget {
|
||||||
|
const MyApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const String _title = 'Flutter Code Sample';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
bool isLightTheme = true;
|
||||||
|
|
||||||
|
void toggleTheme() {
|
||||||
|
setState(() => isLightTheme = !isLightTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: MyApp._title,
|
||||||
|
theme: ThemeData.light().copyWith(
|
||||||
|
extensions: <ThemeExtension<dynamic>>{
|
||||||
|
const MyColors(
|
||||||
|
blue: Color(0xFF1E88E5),
|
||||||
|
red: Color(0xFFE53935),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
darkTheme: ThemeData.dark().copyWith(
|
||||||
|
extensions: <ThemeExtension<dynamic>>{
|
||||||
|
const MyColors(
|
||||||
|
blue: Color(0xFF90CAF9),
|
||||||
|
red: Color(0xFFEF9A9A),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
themeMode: isLightTheme ? ThemeMode.light : ThemeMode.dark,
|
||||||
|
home: Home(
|
||||||
|
isLightTheme: isLightTheme,
|
||||||
|
toggleTheme: toggleTheme,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Home extends StatelessWidget {
|
||||||
|
const Home({
|
||||||
|
Key? key,
|
||||||
|
required this.isLightTheme,
|
||||||
|
required this.toggleTheme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final bool isLightTheme;
|
||||||
|
final void Function() toggleTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final MyColors myColors = Theme.of(context).extension<MyColors>()!;
|
||||||
|
return Material(
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(width: 100, height: 100, color: myColors.blue),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Container(width: 100, height: 100, color: myColors.red),
|
||||||
|
const SizedBox(width: 50),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(isLightTheme ? Icons.nightlight : Icons.wb_sunny),
|
||||||
|
onPressed: toggleTheme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ publish_to: 'none'
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.14.0-383.0.dev <3.0.0"
|
sdk: ">=2.17.0-0 <3.0.0"
|
||||||
flutter: ">=2.5.0-6.0.pre.30 <3.0.0"
|
flutter: ">=2.5.0-6.0.pre.30 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -53,6 +53,36 @@ import 'typography.dart';
|
|||||||
|
|
||||||
export 'package:flutter/services.dart' show Brightness;
|
export 'package:flutter/services.dart' show Brightness;
|
||||||
|
|
||||||
|
/// An interface that defines custom additions to a [ThemeData] object.
|
||||||
|
///
|
||||||
|
/// Typically used for custom colors. To use, subclass [ThemeExtension],
|
||||||
|
/// define a number of fields (e.g. [Color]s), and implement the [copyWith] and
|
||||||
|
/// [lerp] methods. The latter will ensure smooth transitions of properties when
|
||||||
|
/// switching themes.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample shows how to create and use a subclass of [ThemeExtension] that
|
||||||
|
/// defines two colors.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/theme/theme_extension.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
abstract class ThemeExtension<T extends ThemeExtension<T>> {
|
||||||
|
/// Enable const constructor for subclasses.
|
||||||
|
const ThemeExtension();
|
||||||
|
|
||||||
|
/// The extension's type.
|
||||||
|
Object get type => T;
|
||||||
|
|
||||||
|
/// Creates a copy of this theme extension with the given fields
|
||||||
|
/// replaced by the non-null parameter values.
|
||||||
|
ThemeExtension<T> copyWith();
|
||||||
|
|
||||||
|
/// Linearly interpolate with another [ThemeExtension] object.
|
||||||
|
///
|
||||||
|
/// {@macro dart.ui.shadow.lerp}
|
||||||
|
ThemeExtension<T> lerp(ThemeExtension<T>? other, double t);
|
||||||
|
}
|
||||||
|
|
||||||
// Deriving these values is black magic. The spec claims that pressed buttons
|
// 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
|
// 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
|
// spec show that buttons have a composited highlight of #E1E1E1 on a background
|
||||||
@ -243,6 +273,7 @@ class ThemeData with Diagnosticable {
|
|||||||
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
||||||
bool? applyElevationOverlayColor,
|
bool? applyElevationOverlayColor,
|
||||||
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
|
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
|
||||||
|
Iterable<ThemeExtension<dynamic>>? extensions,
|
||||||
InputDecorationTheme? inputDecorationTheme,
|
InputDecorationTheme? inputDecorationTheme,
|
||||||
MaterialTapTargetSize? materialTapTargetSize,
|
MaterialTapTargetSize? materialTapTargetSize,
|
||||||
PageTransitionsTheme? pageTransitionsTheme,
|
PageTransitionsTheme? pageTransitionsTheme,
|
||||||
@ -390,6 +421,7 @@ class ThemeData with Diagnosticable {
|
|||||||
}) {
|
}) {
|
||||||
// GENERAL CONFIGURATION
|
// GENERAL CONFIGURATION
|
||||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||||
|
extensions ??= <ThemeExtension<dynamic>>[];
|
||||||
inputDecorationTheme ??= const InputDecorationTheme();
|
inputDecorationTheme ??= const InputDecorationTheme();
|
||||||
platform ??= defaultTargetPlatform;
|
platform ??= defaultTargetPlatform;
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
@ -562,6 +594,7 @@ class ThemeData with Diagnosticable {
|
|||||||
androidOverscrollIndicator: androidOverscrollIndicator,
|
androidOverscrollIndicator: androidOverscrollIndicator,
|
||||||
applyElevationOverlayColor: applyElevationOverlayColor,
|
applyElevationOverlayColor: applyElevationOverlayColor,
|
||||||
cupertinoOverrideTheme: cupertinoOverrideTheme,
|
cupertinoOverrideTheme: cupertinoOverrideTheme,
|
||||||
|
extensions: _themeExtensionIterableToMap(extensions),
|
||||||
inputDecorationTheme: inputDecorationTheme,
|
inputDecorationTheme: inputDecorationTheme,
|
||||||
materialTapTargetSize: materialTapTargetSize,
|
materialTapTargetSize: materialTapTargetSize,
|
||||||
pageTransitionsTheme: pageTransitionsTheme,
|
pageTransitionsTheme: pageTransitionsTheme,
|
||||||
@ -665,6 +698,7 @@ class ThemeData with Diagnosticable {
|
|||||||
required this.androidOverscrollIndicator,
|
required this.androidOverscrollIndicator,
|
||||||
required this.applyElevationOverlayColor,
|
required this.applyElevationOverlayColor,
|
||||||
required this.cupertinoOverrideTheme,
|
required this.cupertinoOverrideTheme,
|
||||||
|
required this.extensions,
|
||||||
required this.inputDecorationTheme,
|
required this.inputDecorationTheme,
|
||||||
required this.materialTapTargetSize,
|
required this.materialTapTargetSize,
|
||||||
required this.pageTransitionsTheme,
|
required this.pageTransitionsTheme,
|
||||||
@ -807,6 +841,7 @@ class ThemeData with Diagnosticable {
|
|||||||
required this.primaryColorBrightness,
|
required this.primaryColorBrightness,
|
||||||
}) : // GENERAL CONFIGURATION
|
}) : // GENERAL CONFIGURATION
|
||||||
assert(applyElevationOverlayColor != null),
|
assert(applyElevationOverlayColor != null),
|
||||||
|
assert(extensions != null),
|
||||||
assert(inputDecorationTheme != null),
|
assert(inputDecorationTheme != null),
|
||||||
assert(materialTapTargetSize != null),
|
assert(materialTapTargetSize != null),
|
||||||
assert(pageTransitionsTheme != null),
|
assert(pageTransitionsTheme != null),
|
||||||
@ -1053,6 +1088,32 @@ class ThemeData with Diagnosticable {
|
|||||||
/// can be overridden using attributes of this [cupertinoOverrideTheme].
|
/// can be overridden using attributes of this [cupertinoOverrideTheme].
|
||||||
final NoDefaultCupertinoThemeData? cupertinoOverrideTheme;
|
final NoDefaultCupertinoThemeData? cupertinoOverrideTheme;
|
||||||
|
|
||||||
|
/// Arbitrary additions to this theme.
|
||||||
|
///
|
||||||
|
/// To define extensions, pass an [Iterable] containing one or more [ThemeExtension]
|
||||||
|
/// subclasses to [ThemeData.new] or [copyWith].
|
||||||
|
///
|
||||||
|
/// To obtain an extension, use [extension].
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample shows how to create and use a subclass of [ThemeExtension] that
|
||||||
|
/// defines two colors.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/theme/theme_extension.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [extension], a convenience function for obtaining a specific extension.
|
||||||
|
final Map<Object, ThemeExtension<dynamic>> extensions;
|
||||||
|
|
||||||
|
/// Used to obtain a particular [ThemeExtension] from [extensions].
|
||||||
|
///
|
||||||
|
/// Obtain with `Theme.of(context).extension<MyThemeExtension>()`.
|
||||||
|
///
|
||||||
|
/// See [extensions] for an interactive example.
|
||||||
|
T? extension<T>() => extensions[T] as T;
|
||||||
|
|
||||||
/// The default [InputDecoration] values for [InputDecorator], [TextField],
|
/// The default [InputDecoration] values for [InputDecorator], [TextField],
|
||||||
/// and [TextFormField] are based on this theme.
|
/// and [TextFormField] are based on this theme.
|
||||||
///
|
///
|
||||||
@ -1588,6 +1649,7 @@ class ThemeData with Diagnosticable {
|
|||||||
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
||||||
bool? applyElevationOverlayColor,
|
bool? applyElevationOverlayColor,
|
||||||
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
|
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
|
||||||
|
Iterable<ThemeExtension<dynamic>>? extensions,
|
||||||
InputDecorationTheme? inputDecorationTheme,
|
InputDecorationTheme? inputDecorationTheme,
|
||||||
MaterialTapTargetSize? materialTapTargetSize,
|
MaterialTapTargetSize? materialTapTargetSize,
|
||||||
PageTransitionsTheme? pageTransitionsTheme,
|
PageTransitionsTheme? pageTransitionsTheme,
|
||||||
@ -1736,6 +1798,7 @@ class ThemeData with Diagnosticable {
|
|||||||
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
|
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
|
||||||
applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
|
applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
|
||||||
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
|
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
|
||||||
|
extensions: (extensions != null) ? _themeExtensionIterableToMap(extensions) : this.extensions,
|
||||||
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
|
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
|
||||||
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
|
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
|
||||||
pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
|
pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
|
||||||
@ -1889,6 +1952,34 @@ class ThemeData with Diagnosticable {
|
|||||||
return Brightness.dark;
|
return Brightness.dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolate between two [extensions].
|
||||||
|
///
|
||||||
|
/// Includes all theme extensions in [a] and [b].
|
||||||
|
///
|
||||||
|
/// {@macro dart.ui.shadow.lerp}
|
||||||
|
static Map<Object, ThemeExtension<dynamic>> _lerpThemeExtensions(ThemeData a, ThemeData b, double t) {
|
||||||
|
// Lerp [a].
|
||||||
|
final Map<Object, ThemeExtension<dynamic>> newExtensions = a.extensions.map((Object id, ThemeExtension<dynamic> extensionA) {
|
||||||
|
final ThemeExtension<dynamic>? extensionB = b.extensions[id];
|
||||||
|
return MapEntry<Object, ThemeExtension<dynamic>>(id, extensionA.lerp(extensionB, t));
|
||||||
|
});
|
||||||
|
// Add [b]-only extensions.
|
||||||
|
newExtensions.addEntries(b.extensions.entries.where(
|
||||||
|
(MapEntry<Object, ThemeExtension<dynamic>> entry) =>
|
||||||
|
!a.extensions.containsKey(entry.key)));
|
||||||
|
|
||||||
|
return newExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the [extensionsIterable] passed to [ThemeData.new] or [copyWith]
|
||||||
|
/// to the stored [extensions] map, where each entry's key consists of the extension's type.
|
||||||
|
static Map<Object, ThemeExtension<dynamic>> _themeExtensionIterableToMap(Iterable<ThemeExtension<dynamic>> extensionsIterable) {
|
||||||
|
return Map<Object, ThemeExtension<dynamic>>.unmodifiable(<Object, ThemeExtension<dynamic>>{
|
||||||
|
// Strangely, the cast is necessary for tests to run.
|
||||||
|
for (final ThemeExtension<dynamic> extension in extensionsIterable) extension.type: extension as ThemeExtension<ThemeExtension<dynamic>>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Linearly interpolate between two themes.
|
/// Linearly interpolate between two themes.
|
||||||
///
|
///
|
||||||
/// The arguments must not be null.
|
/// The arguments must not be null.
|
||||||
@ -1906,6 +1997,7 @@ class ThemeData with Diagnosticable {
|
|||||||
androidOverscrollIndicator:t < 0.5 ? a.androidOverscrollIndicator : b.androidOverscrollIndicator,
|
androidOverscrollIndicator:t < 0.5 ? a.androidOverscrollIndicator : b.androidOverscrollIndicator,
|
||||||
applyElevationOverlayColor:t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
|
applyElevationOverlayColor:t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
|
||||||
cupertinoOverrideTheme:t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
|
cupertinoOverrideTheme:t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
|
||||||
|
extensions: _lerpThemeExtensions(a, b, t),
|
||||||
inputDecorationTheme:t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
|
inputDecorationTheme:t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
|
||||||
materialTapTargetSize:t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
|
materialTapTargetSize:t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
|
||||||
pageTransitionsTheme:t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
|
pageTransitionsTheme:t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
|
||||||
@ -2006,6 +2098,7 @@ class ThemeData with Diagnosticable {
|
|||||||
other.androidOverscrollIndicator == androidOverscrollIndicator &&
|
other.androidOverscrollIndicator == androidOverscrollIndicator &&
|
||||||
other.applyElevationOverlayColor == applyElevationOverlayColor &&
|
other.applyElevationOverlayColor == applyElevationOverlayColor &&
|
||||||
other.cupertinoOverrideTheme == cupertinoOverrideTheme &&
|
other.cupertinoOverrideTheme == cupertinoOverrideTheme &&
|
||||||
|
mapEquals(other.extensions, extensions) &&
|
||||||
other.inputDecorationTheme == inputDecorationTheme &&
|
other.inputDecorationTheme == inputDecorationTheme &&
|
||||||
other.materialTapTargetSize == materialTapTargetSize &&
|
other.materialTapTargetSize == materialTapTargetSize &&
|
||||||
other.pageTransitionsTheme == pageTransitionsTheme &&
|
other.pageTransitionsTheme == pageTransitionsTheme &&
|
||||||
@ -2103,6 +2196,8 @@ class ThemeData with Diagnosticable {
|
|||||||
androidOverscrollIndicator,
|
androidOverscrollIndicator,
|
||||||
applyElevationOverlayColor,
|
applyElevationOverlayColor,
|
||||||
cupertinoOverrideTheme,
|
cupertinoOverrideTheme,
|
||||||
|
hashList(extensions.keys),
|
||||||
|
hashList(extensions.values),
|
||||||
inputDecorationTheme,
|
inputDecorationTheme,
|
||||||
materialTapTargetSize,
|
materialTapTargetSize,
|
||||||
pageTransitionsTheme,
|
pageTransitionsTheme,
|
||||||
@ -2200,6 +2295,7 @@ class ThemeData with Diagnosticable {
|
|||||||
properties.add(EnumProperty<AndroidOverscrollIndicator>('androidOverscrollIndicator', androidOverscrollIndicator, defaultValue: null, level: DiagnosticLevel.debug));
|
properties.add(EnumProperty<AndroidOverscrollIndicator>('androidOverscrollIndicator', androidOverscrollIndicator, defaultValue: null, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<bool>('applyElevationOverlayColor', applyElevationOverlayColor, level: DiagnosticLevel.debug));
|
properties.add(DiagnosticsProperty<bool>('applyElevationOverlayColor', applyElevationOverlayColor, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));
|
properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));
|
||||||
|
properties.add(IterableProperty<ThemeExtension<dynamic>>('extensions', extensions.values, defaultValue: defaultData.extensions.values, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme, level: DiagnosticLevel.debug));
|
properties.add(DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, level: DiagnosticLevel.debug));
|
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme, level: DiagnosticLevel.debug));
|
properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme, level: DiagnosticLevel.debug));
|
||||||
|
@ -6,6 +6,62 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MyThemeExtensionA extends ThemeExtension<MyThemeExtensionA> {
|
||||||
|
const MyThemeExtensionA({
|
||||||
|
required this.color1,
|
||||||
|
required this.color2,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color? color1;
|
||||||
|
final Color? color2;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyThemeExtensionA copyWith({Color? color1, Color? color2}) {
|
||||||
|
return MyThemeExtensionA(
|
||||||
|
color1: color1 ?? this.color1,
|
||||||
|
color2: color2 ?? this.color2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyThemeExtensionA lerp(ThemeExtension<MyThemeExtensionA>? other, double t) {
|
||||||
|
if (other is! MyThemeExtensionA) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return MyThemeExtensionA(
|
||||||
|
color1: Color.lerp(color1, other.color1, t),
|
||||||
|
color2: Color.lerp(color2, other.color2, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> {
|
||||||
|
const MyThemeExtensionB({
|
||||||
|
required this.textStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyThemeExtensionB copyWith({Color? color, TextStyle? textStyle}) {
|
||||||
|
return MyThemeExtensionB(
|
||||||
|
textStyle: textStyle ?? this.textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyThemeExtensionB lerp(ThemeExtension<MyThemeExtensionB>? other, double t) {
|
||||||
|
if (other is! MyThemeExtensionB) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return MyThemeExtensionB(
|
||||||
|
textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('Theme data control test', () {
|
test('Theme data control test', () {
|
||||||
final ThemeData dark = ThemeData.dark();
|
final ThemeData dark = ThemeData.dark();
|
||||||
@ -377,6 +433,136 @@ void main() {
|
|||||||
expect(expanded.maxHeight, equals(double.infinity));
|
expect(expanded.maxHeight, equals(double.infinity));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Theme extensions', () {
|
||||||
|
const Key containerKey = Key('container');
|
||||||
|
|
||||||
|
testWidgets('can be obtained', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
|
MyThemeExtensionA(
|
||||||
|
color1: Colors.black,
|
||||||
|
color2: Colors.amber,
|
||||||
|
),
|
||||||
|
MyThemeExtensionB(
|
||||||
|
textStyle: TextStyle(fontSize: 50),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
home: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData theme = Theme.of(
|
||||||
|
tester.element(find.byKey(containerKey)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(theme.extension<MyThemeExtensionA>()!.color1, Colors.black);
|
||||||
|
expect(theme.extension<MyThemeExtensionA>()!.color2, Colors.amber);
|
||||||
|
expect(theme.extension<MyThemeExtensionB>()!.textStyle, const TextStyle(fontSize: 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('can use copyWith', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
extensions: <ThemeExtension<dynamic>>{
|
||||||
|
const MyThemeExtensionA(
|
||||||
|
color1: Colors.black,
|
||||||
|
color2: Colors.amber,
|
||||||
|
).copyWith(color1: Colors.blue),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
home: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData theme = Theme.of(
|
||||||
|
tester.element(find.byKey(containerKey)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(theme.extension<MyThemeExtensionA>()!.color1, Colors.blue);
|
||||||
|
expect(theme.extension<MyThemeExtensionA>()!.color2, Colors.amber);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('can lerp', (WidgetTester tester) async {
|
||||||
|
const MyThemeExtensionA extensionA1 = MyThemeExtensionA(
|
||||||
|
color1: Colors.black,
|
||||||
|
color2: Colors.amber,
|
||||||
|
);
|
||||||
|
const MyThemeExtensionA extensionA2 = MyThemeExtensionA(
|
||||||
|
color1: Colors.white,
|
||||||
|
color2: Colors.blue,
|
||||||
|
);
|
||||||
|
const MyThemeExtensionB extensionB1 = MyThemeExtensionB(
|
||||||
|
textStyle: TextStyle(fontSize: 50),
|
||||||
|
);
|
||||||
|
const MyThemeExtensionB extensionB2 = MyThemeExtensionB(
|
||||||
|
textStyle: TextStyle(fontSize: 100),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Both ThemeDatas include both extensions
|
||||||
|
ThemeData lerped = ThemeData.lerp(
|
||||||
|
ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>[
|
||||||
|
extensionA1,
|
||||||
|
extensionB1,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
|
extensionA2,
|
||||||
|
extensionB2,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lerped.extension<MyThemeExtensionA>()!.color1, const Color(0xff7f7f7f));
|
||||||
|
expect(lerped.extension<MyThemeExtensionA>()!.color2, const Color(0xff90ab7d));
|
||||||
|
expect(lerped.extension<MyThemeExtensionB>()!.textStyle, const TextStyle(fontSize: 75));
|
||||||
|
|
||||||
|
// Missing from 2nd ThemeData
|
||||||
|
lerped = ThemeData.lerp(
|
||||||
|
ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
|
extensionA1,
|
||||||
|
extensionB1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
|
extensionB2,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
expect(lerped.extension<MyThemeExtensionA>()!.color1, Colors.black); // Not lerped
|
||||||
|
expect(lerped.extension<MyThemeExtensionA>()!.color2, Colors.amber); // Not lerped
|
||||||
|
expect(lerped.extension<MyThemeExtensionB>()!.textStyle, const TextStyle(fontSize: 75));
|
||||||
|
|
||||||
|
// Missing from 1st ThemeData
|
||||||
|
lerped = ThemeData.lerp(
|
||||||
|
ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
|
extensionA1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ThemeData(
|
||||||
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
|
extensionA2,
|
||||||
|
extensionB2,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
expect(lerped.extension<MyThemeExtensionA>()!.color1, const Color(0xff7f7f7f));
|
||||||
|
expect(lerped.extension<MyThemeExtensionA>()!.color2, const Color(0xff90ab7d));
|
||||||
|
expect(lerped.extension<MyThemeExtensionB>()!.textStyle, const TextStyle(fontSize: 100)); // Not lerped
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('copyWith, ==, hashCode basics', () {
|
test('copyWith, ==, hashCode basics', () {
|
||||||
expect(ThemeData(), ThemeData().copyWith());
|
expect(ThemeData(), ThemeData().copyWith());
|
||||||
expect(ThemeData().hashCode, ThemeData().copyWith().hashCode);
|
expect(ThemeData().hashCode, ThemeData().copyWith().hashCode);
|
||||||
@ -506,6 +692,7 @@ void main() {
|
|||||||
fixTextFieldOutlineLabel: false,
|
fixTextFieldOutlineLabel: false,
|
||||||
useTextSelectionTheme: false,
|
useTextSelectionTheme: false,
|
||||||
androidOverscrollIndicator: null,
|
androidOverscrollIndicator: null,
|
||||||
|
extensions: const <Object, ThemeExtension<dynamic>>{},
|
||||||
);
|
);
|
||||||
|
|
||||||
final SliderThemeData otherSliderTheme = SliderThemeData.fromPrimaryColors(
|
final SliderThemeData otherSliderTheme = SliderThemeData.fromPrimaryColors(
|
||||||
@ -606,6 +793,9 @@ void main() {
|
|||||||
fixTextFieldOutlineLabel: true,
|
fixTextFieldOutlineLabel: true,
|
||||||
useTextSelectionTheme: true,
|
useTextSelectionTheme: true,
|
||||||
androidOverscrollIndicator: AndroidOverscrollIndicator.stretch,
|
androidOverscrollIndicator: AndroidOverscrollIndicator.stretch,
|
||||||
|
extensions: const <Object, ThemeExtension<dynamic>>{
|
||||||
|
MyThemeExtensionB: MyThemeExtensionB(textStyle: TextStyle()),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final ThemeData themeDataCopy = theme.copyWith(
|
final ThemeData themeDataCopy = theme.copyWith(
|
||||||
@ -685,6 +875,7 @@ void main() {
|
|||||||
drawerTheme: otherTheme.drawerTheme,
|
drawerTheme: otherTheme.drawerTheme,
|
||||||
listTileTheme: otherTheme.listTileTheme,
|
listTileTheme: otherTheme.listTileTheme,
|
||||||
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
|
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
|
||||||
|
extensions: otherTheme.extensions.values,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(themeDataCopy.brightness, equals(otherTheme.brightness));
|
expect(themeDataCopy.brightness, equals(otherTheme.brightness));
|
||||||
@ -763,6 +954,7 @@ void main() {
|
|||||||
expect(themeDataCopy.drawerTheme, equals(otherTheme.drawerTheme));
|
expect(themeDataCopy.drawerTheme, equals(otherTheme.drawerTheme));
|
||||||
expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
|
expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
|
||||||
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
|
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
|
||||||
|
expect(themeDataCopy.extensions, equals(otherTheme.extensions));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('ThemeData.toString has less than 200 characters output', (WidgetTester tester) async {
|
testWidgets('ThemeData.toString has less than 200 characters output', (WidgetTester tester) async {
|
||||||
@ -810,6 +1002,7 @@ void main() {
|
|||||||
'androidOverscrollIndicator',
|
'androidOverscrollIndicator',
|
||||||
'applyElevationOverlayColor',
|
'applyElevationOverlayColor',
|
||||||
'cupertinoOverrideTheme',
|
'cupertinoOverrideTheme',
|
||||||
|
'extensions',
|
||||||
'inputDecorationTheme',
|
'inputDecorationTheme',
|
||||||
'materialTapTargetSize',
|
'materialTapTargetSize',
|
||||||
'pageTransitionsTheme',
|
'pageTransitionsTheme',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user