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
|
||||
|
||||
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"
|
||||
|
||||
dependencies:
|
||||
|
@ -53,6 +53,36 @@ import 'typography.dart';
|
||||
|
||||
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
|
||||
// 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
|
||||
@ -243,6 +273,7 @@ class ThemeData with Diagnosticable {
|
||||
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
||||
bool? applyElevationOverlayColor,
|
||||
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
|
||||
Iterable<ThemeExtension<dynamic>>? extensions,
|
||||
InputDecorationTheme? inputDecorationTheme,
|
||||
MaterialTapTargetSize? materialTapTargetSize,
|
||||
PageTransitionsTheme? pageTransitionsTheme,
|
||||
@ -390,6 +421,7 @@ class ThemeData with Diagnosticable {
|
||||
}) {
|
||||
// GENERAL CONFIGURATION
|
||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||
extensions ??= <ThemeExtension<dynamic>>[];
|
||||
inputDecorationTheme ??= const InputDecorationTheme();
|
||||
platform ??= defaultTargetPlatform;
|
||||
switch (platform) {
|
||||
@ -562,6 +594,7 @@ class ThemeData with Diagnosticable {
|
||||
androidOverscrollIndicator: androidOverscrollIndicator,
|
||||
applyElevationOverlayColor: applyElevationOverlayColor,
|
||||
cupertinoOverrideTheme: cupertinoOverrideTheme,
|
||||
extensions: _themeExtensionIterableToMap(extensions),
|
||||
inputDecorationTheme: inputDecorationTheme,
|
||||
materialTapTargetSize: materialTapTargetSize,
|
||||
pageTransitionsTheme: pageTransitionsTheme,
|
||||
@ -665,6 +698,7 @@ class ThemeData with Diagnosticable {
|
||||
required this.androidOverscrollIndicator,
|
||||
required this.applyElevationOverlayColor,
|
||||
required this.cupertinoOverrideTheme,
|
||||
required this.extensions,
|
||||
required this.inputDecorationTheme,
|
||||
required this.materialTapTargetSize,
|
||||
required this.pageTransitionsTheme,
|
||||
@ -807,6 +841,7 @@ class ThemeData with Diagnosticable {
|
||||
required this.primaryColorBrightness,
|
||||
}) : // GENERAL CONFIGURATION
|
||||
assert(applyElevationOverlayColor != null),
|
||||
assert(extensions != null),
|
||||
assert(inputDecorationTheme != null),
|
||||
assert(materialTapTargetSize != null),
|
||||
assert(pageTransitionsTheme != null),
|
||||
@ -1053,6 +1088,32 @@ class ThemeData with Diagnosticable {
|
||||
/// can be overridden using attributes of this [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],
|
||||
/// and [TextFormField] are based on this theme.
|
||||
///
|
||||
@ -1588,6 +1649,7 @@ class ThemeData with Diagnosticable {
|
||||
AndroidOverscrollIndicator? androidOverscrollIndicator,
|
||||
bool? applyElevationOverlayColor,
|
||||
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
|
||||
Iterable<ThemeExtension<dynamic>>? extensions,
|
||||
InputDecorationTheme? inputDecorationTheme,
|
||||
MaterialTapTargetSize? materialTapTargetSize,
|
||||
PageTransitionsTheme? pageTransitionsTheme,
|
||||
@ -1736,6 +1798,7 @@ class ThemeData with Diagnosticable {
|
||||
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
|
||||
applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
|
||||
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
|
||||
extensions: (extensions != null) ? _themeExtensionIterableToMap(extensions) : this.extensions,
|
||||
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
|
||||
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
|
||||
pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
|
||||
@ -1889,6 +1952,34 @@ class ThemeData with Diagnosticable {
|
||||
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.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
@ -1906,6 +1997,7 @@ class ThemeData with Diagnosticable {
|
||||
androidOverscrollIndicator:t < 0.5 ? a.androidOverscrollIndicator : b.androidOverscrollIndicator,
|
||||
applyElevationOverlayColor:t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
|
||||
cupertinoOverrideTheme:t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
|
||||
extensions: _lerpThemeExtensions(a, b, t),
|
||||
inputDecorationTheme:t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
|
||||
materialTapTargetSize:t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
|
||||
pageTransitionsTheme:t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
|
||||
@ -2006,6 +2098,7 @@ class ThemeData with Diagnosticable {
|
||||
other.androidOverscrollIndicator == androidOverscrollIndicator &&
|
||||
other.applyElevationOverlayColor == applyElevationOverlayColor &&
|
||||
other.cupertinoOverrideTheme == cupertinoOverrideTheme &&
|
||||
mapEquals(other.extensions, extensions) &&
|
||||
other.inputDecorationTheme == inputDecorationTheme &&
|
||||
other.materialTapTargetSize == materialTapTargetSize &&
|
||||
other.pageTransitionsTheme == pageTransitionsTheme &&
|
||||
@ -2103,6 +2196,8 @@ class ThemeData with Diagnosticable {
|
||||
androidOverscrollIndicator,
|
||||
applyElevationOverlayColor,
|
||||
cupertinoOverrideTheme,
|
||||
hashList(extensions.keys),
|
||||
hashList(extensions.values),
|
||||
inputDecorationTheme,
|
||||
materialTapTargetSize,
|
||||
pageTransitionsTheme,
|
||||
@ -2200,6 +2295,7 @@ class ThemeData with Diagnosticable {
|
||||
properties.add(EnumProperty<AndroidOverscrollIndicator>('androidOverscrollIndicator', androidOverscrollIndicator, defaultValue: null, 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(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<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, 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_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() {
|
||||
test('Theme data control test', () {
|
||||
final ThemeData dark = ThemeData.dark();
|
||||
@ -377,6 +433,136 @@ void main() {
|
||||
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', () {
|
||||
expect(ThemeData(), ThemeData().copyWith());
|
||||
expect(ThemeData().hashCode, ThemeData().copyWith().hashCode);
|
||||
@ -506,6 +692,7 @@ void main() {
|
||||
fixTextFieldOutlineLabel: false,
|
||||
useTextSelectionTheme: false,
|
||||
androidOverscrollIndicator: null,
|
||||
extensions: const <Object, ThemeExtension<dynamic>>{},
|
||||
);
|
||||
|
||||
final SliderThemeData otherSliderTheme = SliderThemeData.fromPrimaryColors(
|
||||
@ -606,6 +793,9 @@ void main() {
|
||||
fixTextFieldOutlineLabel: true,
|
||||
useTextSelectionTheme: true,
|
||||
androidOverscrollIndicator: AndroidOverscrollIndicator.stretch,
|
||||
extensions: const <Object, ThemeExtension<dynamic>>{
|
||||
MyThemeExtensionB: MyThemeExtensionB(textStyle: TextStyle()),
|
||||
},
|
||||
);
|
||||
|
||||
final ThemeData themeDataCopy = theme.copyWith(
|
||||
@ -685,6 +875,7 @@ void main() {
|
||||
drawerTheme: otherTheme.drawerTheme,
|
||||
listTileTheme: otherTheme.listTileTheme,
|
||||
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
|
||||
extensions: otherTheme.extensions.values,
|
||||
);
|
||||
|
||||
expect(themeDataCopy.brightness, equals(otherTheme.brightness));
|
||||
@ -763,6 +954,7 @@ void main() {
|
||||
expect(themeDataCopy.drawerTheme, equals(otherTheme.drawerTheme));
|
||||
expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
|
||||
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
|
||||
expect(themeDataCopy.extensions, equals(otherTheme.extensions));
|
||||
});
|
||||
|
||||
testWidgets('ThemeData.toString has less than 200 characters output', (WidgetTester tester) async {
|
||||
@ -810,6 +1002,7 @@ void main() {
|
||||
'androidOverscrollIndicator',
|
||||
'applyElevationOverlayColor',
|
||||
'cupertinoOverrideTheme',
|
||||
'extensions',
|
||||
'inputDecorationTheme',
|
||||
'materialTapTargetSize',
|
||||
'pageTransitionsTheme',
|
||||
|
Loading…
x
Reference in New Issue
Block a user