From 60f039cb1c840e6cb924a273c3e5f08f4d90cc0a Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Tue, 15 Feb 2022 18:44:33 +0200 Subject: [PATCH] Add `ExpansionTileTheme` (#98405) --- packages/flutter/lib/material.dart | 1 + .../lib/src/material/expansion_tile.dart | 96 ++++++- .../src/material/expansion_tile_theme.dart | 217 +++++++++++++++ .../flutter/lib/src/material/theme_data.dart | 15 + .../material/expansion_tile_theme_test.dart | 257 ++++++++++++++++++ .../test/material/theme_data_test.dart | 3 + 6 files changed, 575 insertions(+), 14 deletions(-) create mode 100644 packages/flutter/lib/src/material/expansion_tile_theme.dart create mode 100644 packages/flutter/test/material/expansion_tile_theme_test.dart diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 525047ff44..2a6ae2539d 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -73,6 +73,7 @@ export 'src/material/elevation_overlay.dart'; export 'src/material/expand_icon.dart'; export 'src/material/expansion_panel.dart'; export 'src/material/expansion_tile.dart'; +export 'src/material/expansion_tile_theme.dart'; export 'src/material/feedback.dart'; export 'src/material/flat_button.dart'; export 'src/material/flexible_space_bar.dart'; diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index c81a5de163..d341c0ab33 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'color_scheme.dart'; import 'colors.dart'; +import 'expansion_tile_theme.dart'; import 'icons.dart'; import 'list_tile.dart'; import 'theme.dart'; @@ -107,9 +108,25 @@ class ExpansionTile extends StatefulWidget { final List children; /// The color to display behind the sublist when expanded. + /// + /// If this property is null then [ExpansionTileThemeData.backgroundColor] is used. If that + /// is also null then Colors.transparent is used. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Color? backgroundColor; /// When not null, defines the background color of tile when the sublist is collapsed. + /// + /// If this property is null then [ExpansionTileThemeData.collapsedBackgroundColor] is used. + /// If that is also null then Colors.transparent is used. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Color? collapsedBackgroundColor; /// A widget to display after the title. @@ -134,7 +151,13 @@ class ExpansionTile extends StatefulWidget { /// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset /// the expanded [children] widgets. /// - /// When the value is null, the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`. + /// If this property is null then [ExpansionTileThemeData.tilePadding] is used. If that + /// is also null then the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final EdgeInsetsGeometry? tilePadding; /// Specifies the alignment of [children], which are arranged in a column when @@ -150,7 +173,13 @@ class ExpansionTile extends StatefulWidget { /// /// The width of the column is the width of the widest child widget in [children]. /// - /// When the value is null, the value of `expandedAlignment` is [Alignment.center]. + /// If this property is null then [ExpansionTileThemeData.expandedAlignment]is used. If that + /// is also null then the value of `expandedAlignment` is [Alignment.center]. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Alignment? expandedAlignment; /// Specifies the alignment of each child within [children] when the tile is expanded. @@ -171,12 +200,26 @@ class ExpansionTile extends StatefulWidget { /// Specifies padding for [children]. /// - /// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero]. + /// If this property is null then [ExpansionTileThemeData.childrenPadding] is used. If that + /// is also null then the value of `childrenPadding` is [EdgeInsets.zero]. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final EdgeInsetsGeometry? childrenPadding; /// The icon color of tile's expansion arrow icon when the sublist is expanded. /// /// Used to override to the [ListTileThemeData.iconColor]. + /// + /// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that + /// is also null then the value of [ListTileThemeData.iconColor] is used. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Color? iconColor; /// The icon color of tile's expansion arrow icon when the sublist is collapsed. @@ -188,11 +231,27 @@ class ExpansionTile extends StatefulWidget { /// The color of the tile's titles when the sublist is expanded. /// /// Used to override to the [ListTileThemeData.textColor]. + /// + /// If this property is null then [ExpansionTileThemeData.textColor] is used. If that + /// is also null then the value of [ListTileThemeData.textColor] is used. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Color? textColor; /// The color of the tile's titles when the sublist is collapsed. /// /// Used to override to the [ListTileThemeData.textColor]. + /// + /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that + /// is also null then the value of [ListTileThemeData.textColor] is used. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Color? collapsedTextColor; /// Typically used to force the expansion arrow icon to the tile's leading or trailing edge. @@ -297,11 +356,12 @@ class _ExpansionTileState extends State with SingleTickerProvider } Widget _buildChildren(BuildContext context, Widget? child) { + final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context); final Color borderSideColor = _borderColor.value ?? Colors.transparent; return Container( decoration: BoxDecoration( - color: _backgroundColor.value ?? Colors.transparent, + color: _backgroundColor.value ?? expansionTileTheme.backgroundColor ?? Colors.transparent, border: Border( top: BorderSide(color: borderSideColor), bottom: BorderSide(color: borderSideColor), @@ -311,11 +371,11 @@ class _ExpansionTileState extends State with SingleTickerProvider mainAxisSize: MainAxisSize.min, children: [ ListTileTheme.merge( - iconColor: _iconColor.value, + iconColor: _iconColor.value ?? expansionTileTheme.iconColor, textColor: _headerColor.value, child: ListTile( onTap: _handleTap, - contentPadding: widget.tilePadding, + contentPadding: widget.tilePadding ?? expansionTileTheme.tilePadding, leading: widget.leading ?? _buildLeadingIcon(context), title: widget.title, subtitle: widget.subtitle, @@ -324,7 +384,9 @@ class _ExpansionTileState extends State with SingleTickerProvider ), ClipRect( child: Align( - alignment: widget.expandedAlignment ?? Alignment.center, + alignment: widget.expandedAlignment + ?? expansionTileTheme.expandedAlignment + ?? Alignment.center, heightFactor: _heightFactor.value, child: child, ), @@ -337,22 +399,28 @@ class _ExpansionTileState extends State with SingleTickerProvider @override void didChangeDependencies() { final ThemeData theme = Theme.of(context); + final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context); final ColorScheme colorScheme = theme.colorScheme; _borderColorTween.end = theme.dividerColor; _headerColorTween - ..begin = widget.collapsedTextColor ?? theme.textTheme.subtitle1!.color - ..end = widget.textColor ?? colorScheme.primary; + ..begin = widget.collapsedTextColor + ?? expansionTileTheme.collapsedTextColor + ?? theme.textTheme.subtitle1!.color + ..end = widget.textColor ?? expansionTileTheme.textColor ?? colorScheme.primary; _iconColorTween - ..begin = widget.collapsedIconColor ?? theme.unselectedWidgetColor - ..end = widget.iconColor ?? colorScheme.primary; + ..begin = widget.collapsedIconColor + ?? expansionTileTheme.collapsedIconColor + ?? theme.unselectedWidgetColor + ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? colorScheme.primary; _backgroundColorTween - ..begin = widget.collapsedBackgroundColor - ..end = widget.backgroundColor; + ..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor + ..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor; super.didChangeDependencies(); } @override Widget build(BuildContext context) { + final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context); final bool closed = !_isExpanded && _controller.isDismissed; final bool shouldRemoveChildren = closed && !widget.maintainState; @@ -361,7 +429,7 @@ class _ExpansionTileState extends State with SingleTickerProvider child: TickerMode( enabled: !closed, child: Padding( - padding: widget.childrenPadding ?? EdgeInsets.zero, + padding: widget.childrenPadding ?? expansionTileTheme.childrenPadding ?? EdgeInsets.zero, child: Column( crossAxisAlignment: widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center, children: widget.children, diff --git a/packages/flutter/lib/src/material/expansion_tile_theme.dart b/packages/flutter/lib/src/material/expansion_tile_theme.dart new file mode 100644 index 0000000000..d2c22a6e99 --- /dev/null +++ b/packages/flutter/lib/src/material/expansion_tile_theme.dart @@ -0,0 +1,217 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'theme.dart'; + +/// Used with [ExpansionTileTheme] to define default property values for +/// descendant [ExpansionTile] widgets. +/// +/// Descendant widgets obtain the current [ExpansionTileThemeData] object +/// using `ExpansionTileTheme.of(context)`. Instances of +/// [ExpansionTileThemeData] can be customized with +/// [ExpansionTileThemeData.copyWith]. +/// +/// A [ExpansionTileThemeData] is often specified as part of the +/// overall [Theme] with [ThemeData.expansionTileTheme]. +/// +/// All [ExpansionTileThemeData] properties are `null` by default. +/// When a theme property is null, the [ExpansionTile] will provide its own +/// default based on the overall [Theme]'s textTheme and +/// colorScheme. See the individual [ExpansionTile] properties for details. +/// +/// See also: +/// +/// * [ThemeData], which describes the overall theme information for the +/// application. +/// * [ExpansionTileTheme] which overrides the default [ExpansionTileTheme] +/// of its [ExpansionTile] descendants. +/// * [ThemeData.textTheme], text with a color that contrasts with the card +/// and canvas colors. +/// * [ThemeData.colorScheme], the thirteen colors that most Material widget +/// default colors are based on. +@immutable +class ExpansionTileThemeData with Diagnosticable { + /// Creates a [ExpansionTileThemeData]. + const ExpansionTileThemeData ({ + this.backgroundColor, + this.collapsedBackgroundColor, + this.tilePadding, + this.expandedAlignment, + this.childrenPadding, + this.iconColor, + this.collapsedIconColor, + this.textColor, + this.collapsedTextColor, + }); + + /// Overrides the default value of [ExpansionTile.backgroundColor]. + final Color? backgroundColor; + + /// Overrides the default value of [ExpansionTile.collapsedBackgroundColor]. + final Color? collapsedBackgroundColor; + + /// Overrides the default value of [ExpansionTile.tilePadding]. + final EdgeInsetsGeometry? tilePadding; + + /// Overrides the default value of [ExpansionTile.expandedAlignment]. + final AlignmentGeometry? expandedAlignment; + + /// Overrides the default value of [ExpansionTile.childrenPadding]. + final EdgeInsetsGeometry? childrenPadding; + + /// Overrides the default value of [ExpansionTile.iconColor]. + final Color? iconColor; + + /// Overrides the default value of [ExpansionTile.collapsedIconColor]. + final Color? collapsedIconColor; + + /// Overrides the default value of [ExpansionTile.textColor]. + final Color? textColor; + + /// Overrides the default value of [ExpansionTile.collapsedTextColor]. + final Color? collapsedTextColor; + + /// Creates a copy of this object with the given fields replaced with the + /// new values. + ExpansionTileThemeData copyWith({ + Color? backgroundColor, + Color? collapsedBackgroundColor, + EdgeInsetsGeometry? tilePadding, + AlignmentGeometry? expandedAlignment, + EdgeInsetsGeometry? childrenPadding, + Color? iconColor, + Color? collapsedIconColor, + Color? textColor, + Color? collapsedTextColor, + }) { + return ExpansionTileThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + collapsedBackgroundColor: collapsedBackgroundColor ?? this.collapsedBackgroundColor, + tilePadding: tilePadding ?? this.tilePadding, + expandedAlignment: expandedAlignment ?? this.expandedAlignment, + childrenPadding: childrenPadding ?? this.childrenPadding, + iconColor: iconColor ?? this.iconColor, + collapsedIconColor: collapsedIconColor ?? this.collapsedIconColor, + textColor: textColor ?? this.textColor, + collapsedTextColor: collapsedTextColor ?? this.collapsedTextColor, + ); + } + + /// Linearly interpolate between ExpansionTileThemeData objects. + static ExpansionTileThemeData? lerp(ExpansionTileThemeData? a, ExpansionTileThemeData? b, double t) { + assert (t != null); + if (a == null && b == null) + return null; + return ExpansionTileThemeData( + backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + collapsedBackgroundColor: Color.lerp(a?.collapsedBackgroundColor, b?.collapsedBackgroundColor, t), + tilePadding: EdgeInsetsGeometry.lerp(a?.tilePadding, b?.tilePadding, t), + expandedAlignment: AlignmentGeometry.lerp(a?.expandedAlignment, b?.expandedAlignment, t), + childrenPadding: EdgeInsetsGeometry.lerp(a?.childrenPadding, b?.childrenPadding, t), + iconColor: Color.lerp(a?.iconColor, b?.iconColor, t), + collapsedIconColor: Color.lerp(a?.collapsedIconColor, b?.collapsedIconColor, t), + textColor: Color.lerp(a?.textColor, b?.textColor, t), + collapsedTextColor: Color.lerp(a?.collapsedTextColor, b?.collapsedTextColor, t), + ); + } + + @override + int get hashCode { + return hashValues( + backgroundColor, + collapsedBackgroundColor, + tilePadding, + expandedAlignment, + childrenPadding, + iconColor, + collapsedIconColor, + textColor, + collapsedTextColor, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is ExpansionTileThemeData + && other.backgroundColor == backgroundColor + && other.collapsedBackgroundColor == collapsedBackgroundColor + && other.tilePadding == tilePadding + && other.expandedAlignment == expandedAlignment + && other.childrenPadding == childrenPadding + && other.iconColor == iconColor + && other.collapsedIconColor == collapsedIconColor + && other.textColor == textColor + && other.collapsedTextColor == collapsedTextColor; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null)); + properties.add(ColorProperty('collapsedBackgroundColor', collapsedBackgroundColor, defaultValue: null)); + properties.add(DiagnosticsProperty('tilePadding', tilePadding, defaultValue: null)); + properties.add(DiagnosticsProperty('expandedAlignment', expandedAlignment, defaultValue: null)); + properties.add(DiagnosticsProperty('childrenPadding', childrenPadding, defaultValue: null)); + properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); + properties.add(ColorProperty('collapsedIconColor', collapsedIconColor, defaultValue: null)); + properties.add(ColorProperty('textColor', textColor, defaultValue: null)); + properties.add(ColorProperty('collapsedTextColor', collapsedTextColor, defaultValue: null)); + } +} + +/// Overrides the default [ExpansionTileTheme] of its [ExpansionTile] descendants. +/// +/// See also: +/// +/// * [ExpansionTileThemeData], which is used to configure this theme. +/// * [ThemeData.expansionTileTheme], which can be used to override the default +/// [ExpansionTileTheme] for [ExpansionTile]s below the overall [Theme]. +class ExpansionTileTheme extends InheritedTheme { + /// Applies the given theme [data] to [child]. + /// + /// The [data] and [child] arguments must not be null. + const ExpansionTileTheme({ + Key? key, + required this.data, + required Widget child, + }) : assert(child != null), + assert(data != null), + super(key: key, child: child); + + /// Specifies color, alignment, and text style values for + /// descendant [ExpansionTile] widgets. + final ExpansionTileThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ExpansionTileTheme] widget, then + /// [ThemeData.expansionTileTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ExpansionTileThemeData theme = ExpansionTileTheme.of(context); + /// ``` + static ExpansionTileThemeData of(BuildContext context) { + final ExpansionTileTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType(); + return inheritedTheme?.data ?? Theme.of(context).expansionTileTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) { + return ExpansionTileTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(ExpansionTileTheme oldWidget) => data != oldWidget.data; + } diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index cf288a1520..ef4bcd2f78 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -24,6 +24,7 @@ import 'dialog_theme.dart'; import 'divider_theme.dart'; import 'drawer_theme.dart'; import 'elevated_button_theme.dart'; +import 'expansion_tile_theme.dart'; import 'floating_action_button_theme.dart'; import 'ink_splash.dart'; import 'ink_well.dart' show InteractiveInkFeatureFactory; @@ -321,6 +322,7 @@ class ThemeData with Diagnosticable { TimePickerThemeData? timePickerTheme, ToggleButtonsThemeData? toggleButtonsTheme, TooltipThemeData? tooltipTheme, + ExpansionTileThemeData? expansionTileTheme, // DEPRECATED (newest deprecations at the bottom) @Deprecated( 'No longer used by the framework, please remove any reference to it. ' @@ -541,6 +543,7 @@ class ThemeData with Diagnosticable { timePickerTheme ??= const TimePickerThemeData(); toggleButtonsTheme ??= const ToggleButtonsThemeData(); tooltipTheme ??= const TooltipThemeData(); + expansionTileTheme ??= const ExpansionTileThemeData(); // DEPRECATED (newest deprecations at the bottom) useTextSelectionTheme ??= true; @@ -630,6 +633,7 @@ class ThemeData with Diagnosticable { timePickerTheme: timePickerTheme, toggleButtonsTheme: toggleButtonsTheme, tooltipTheme: tooltipTheme, + expansionTileTheme: expansionTileTheme, // DEPRECATED (newest deprecations at the bottom) useTextSelectionTheme: useTextSelectionTheme, textSelectionColor: textSelectionColor, @@ -735,6 +739,7 @@ class ThemeData with Diagnosticable { required this.timePickerTheme, required this.toggleButtonsTheme, required this.tooltipTheme, + required this.expansionTileTheme, // DEPRECATED (newest deprecations at the bottom) @Deprecated( 'No longer used by the framework, please remove any reference to it. ' @@ -873,6 +878,7 @@ class ThemeData with Diagnosticable { assert(timePickerTheme != null), assert(toggleButtonsTheme != null), assert(tooltipTheme != null), + assert(expansionTileTheme != null), // DEPRECATED (newest deprecations at the bottom) assert(useTextSelectionTheme != null), assert(textSelectionColor != null), @@ -1409,6 +1415,9 @@ class ThemeData with Diagnosticable { /// This is the value returned from [TooltipTheme.of]. final TooltipThemeData tooltipTheme; + /// A theme for customizing the visual properties of [ExpansionTile]s. + final ExpansionTileThemeData expansionTileTheme; + // DEPRECATED (newest deprecations at the bottom) /// A temporary flag that was used to opt-in to the new [TextSelectionTheme] @@ -1634,6 +1643,7 @@ class ThemeData with Diagnosticable { TimePickerThemeData? timePickerTheme, ToggleButtonsThemeData? toggleButtonsTheme, TooltipThemeData? tooltipTheme, + ExpansionTileThemeData? expansionTileTheme, // DEPRECATED (newest deprecations at the bottom) @Deprecated( 'No longer used by the framework, please remove any reference to it. ' @@ -1777,6 +1787,7 @@ class ThemeData with Diagnosticable { timePickerTheme: timePickerTheme ?? this.timePickerTheme, toggleButtonsTheme: toggleButtonsTheme ?? this.toggleButtonsTheme, tooltipTheme: tooltipTheme ?? this.tooltipTheme, + expansionTileTheme: expansionTileTheme ?? this.expansionTileTheme, // DEPRECATED (newest deprecations at the bottom) useTextSelectionTheme: useTextSelectionTheme ?? this.useTextSelectionTheme, textSelectionColor: textSelectionColor ?? this.textSelectionColor, @@ -1946,6 +1957,7 @@ class ThemeData with Diagnosticable { timePickerTheme: TimePickerThemeData.lerp(a.timePickerTheme, b.timePickerTheme, t), toggleButtonsTheme: ToggleButtonsThemeData.lerp(a.toggleButtonsTheme, b.toggleButtonsTheme, t)!, tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t)!, + expansionTileTheme: ExpansionTileThemeData.lerp(a.expansionTileTheme, b.expansionTileTheme, t)!, // DEPRECATED (newest deprecations at the bottom) useTextSelectionTheme: t < 0.5 ? a.useTextSelectionTheme : b.useTextSelectionTheme, textSelectionColor: Color.lerp(a.textSelectionColor, b.textSelectionColor, t)!, @@ -2045,6 +2057,7 @@ class ThemeData with Diagnosticable { other.timePickerTheme == timePickerTheme && other.toggleButtonsTheme == toggleButtonsTheme && other.tooltipTheme == tooltipTheme && + other.expansionTileTheme == expansionTileTheme && // DEPRECATED (newest deprecations at the bottom) other.useTextSelectionTheme == useTextSelectionTheme && other.textSelectionColor == textSelectionColor && @@ -2141,6 +2154,7 @@ class ThemeData with Diagnosticable { timePickerTheme, toggleButtonsTheme, tooltipTheme, + expansionTileTheme, // DEPRECATED (newest deprecations at the bottom) useTextSelectionTheme, textSelectionColor, @@ -2237,6 +2251,7 @@ class ThemeData with Diagnosticable { properties.add(DiagnosticsProperty('timePickerTheme', timePickerTheme, defaultValue: defaultData.timePickerTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('toggleButtonsTheme', toggleButtonsTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('tooltipTheme', tooltipTheme, level: DiagnosticLevel.debug)); + properties.add(DiagnosticsProperty('expansionTileTheme', expansionTileTheme, level: DiagnosticLevel.debug)); // DEPRECATED (newest deprecations at the bottom) properties.add(DiagnosticsProperty('useTextSelectionTheme', useTextSelectionTheme, level: DiagnosticLevel.debug)); properties.add(ColorProperty('textSelectionColor', textSelectionColor, defaultValue: defaultData.textSelectionColor, level: DiagnosticLevel.debug)); diff --git a/packages/flutter/test/material/expansion_tile_theme_test.dart b/packages/flutter/test/material/expansion_tile_theme_test.dart new file mode 100644 index 0000000000..cbe0f2477e --- /dev/null +++ b/packages/flutter/test/material/expansion_tile_theme_test.dart @@ -0,0 +1,257 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class TestIcon extends StatefulWidget { + const TestIcon({Key? key}) : super(key: key); + + @override + TestIconState createState() => TestIconState(); +} + +class TestIconState extends State { + late IconThemeData iconTheme; + + @override + Widget build(BuildContext context) { + iconTheme = IconTheme.of(context); + return const Icon(Icons.expand_more); + } +} +class TestText extends StatefulWidget { + const TestText(this.text, {Key? key}) : super(key: key); + + final String text; + + @override + TestTextState createState() => TestTextState(); +} + +class TestTextState extends State { + late TextStyle textStyle; + + @override + Widget build(BuildContext context) { + textStyle = DefaultTextStyle.of(context).style; + return Text(widget.text); + } +} + +void main() { + test('ExpansionTileThemeData copyWith, ==, hashCode basics', () { + expect(const ExpansionTileThemeData(), const ExpansionTileThemeData().copyWith()); + expect(const ExpansionTileThemeData().hashCode, const ExpansionTileThemeData().copyWith().hashCode); + }); + + test('ExpansionTileThemeData defaults', () { + const ExpansionTileThemeData theme = ExpansionTileThemeData(); + expect(theme.backgroundColor, null); + expect(theme.collapsedBackgroundColor, null); + expect(theme.tilePadding, null); + expect(theme.expandedAlignment, null); + expect(theme.childrenPadding, null); + expect(theme.iconColor, null); + expect(theme.collapsedIconColor, null); + expect(theme.textColor, null); + expect(theme.collapsedTextColor, null); + }); + + testWidgets('Default ExpansionTileThemeData debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const TooltipThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('ExpansionTileThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const ExpansionTileThemeData( + backgroundColor: Color(0xff000000), + collapsedBackgroundColor: Color(0xff6f83fc), + tilePadding: EdgeInsets.all(20.0), + expandedAlignment: Alignment.bottomCenter, + childrenPadding: EdgeInsets.all(10.0), + iconColor: Color(0xffa7c61c), + collapsedIconColor: Color(0xffdd0b1f), + textColor: Color(0xffffffff), + collapsedTextColor: Color(0xff522bab), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'backgroundColor: Color(0xff000000)', + 'collapsedBackgroundColor: Color(0xff6f83fc)', + 'tilePadding: EdgeInsets.all(20.0)', + 'expandedAlignment: Alignment.bottomCenter', + 'childrenPadding: EdgeInsets.all(10.0)', + 'iconColor: Color(0xffa7c61c)', + 'collapsedIconColor: Color(0xffdd0b1f)', + 'textColor: Color(0xffffffff)', + 'collapsedTextColor: Color(0xff522bab)', + ]); + }); + + testWidgets('ExpansionTileTheme - collapsed', (WidgetTester tester) async { + final Key tileKey = UniqueKey(); + final Key titleKey = UniqueKey(); + final Key iconKey = UniqueKey(); + const Color backgroundColor = Colors.orange; + const Color collapsedBackgroundColor = Colors.red; + const Color iconColor = Colors.green; + const Color collapsedIconColor = Colors.blue; + const Color textColor = Colors.black; + const Color collapsedTextColor = Colors.white; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + expansionTileTheme: const ExpansionTileThemeData( + backgroundColor: backgroundColor, + collapsedBackgroundColor: collapsedBackgroundColor, + tilePadding: EdgeInsets.fromLTRB(8, 12, 4, 10), + expandedAlignment: Alignment.centerRight, + childrenPadding: EdgeInsets.all(20.0), + iconColor: iconColor, + collapsedIconColor: collapsedIconColor, + textColor: textColor, + collapsedTextColor: collapsedTextColor, + ), + ), + home: Material( + child: Center( + child: ExpansionTile( + key: tileKey, + title: TestText('Collapsed Tile', key: titleKey), + trailing: TestIcon(key: iconKey), + children: const [Text('Tile 1')], + ), + ), + ), + ), + ); + + final BoxDecoration boxDecoration = tester.firstWidget(find.descendant( + of: find.byKey(tileKey), + matching: find.byType(Container), + )).decoration! as BoxDecoration; + // Check the tile's collapsed background color when collapsedBackgroundColor is applied. + expect(boxDecoration.color, collapsedBackgroundColor); + + final Rect titleRect = tester.getRect(find.text('Collapsed Tile')); + final Rect trailingRect = tester.getRect(find.byIcon(Icons.expand_more)); + final Rect listTileRect = tester.getRect(find.byType(ListTile)); + final Rect tallerWidget = titleRect.height > trailingRect.height ? titleRect : trailingRect; + + // Check the positions of title and trailing Widgets, after padding is applied. + expect(listTileRect.left, titleRect.left - 8); + expect(listTileRect.right, trailingRect.right + 4); + + // Calculate the remaining height of ListTile from the default height. + final double remainingHeight = 56 - tallerWidget.height; + expect(listTileRect.top, tallerWidget.top - remainingHeight / 2 - 12); + expect(listTileRect.bottom, tallerWidget.bottom + remainingHeight / 2 + 10); + + Color getIconColor() => tester.state(find.byType(TestIcon)).iconTheme.color!; + Color getTextColor() => tester.state(find.byType(TestText)).textStyle.color!; + + // Check the collapsed icon color when iconColor is applied. + expect(getIconColor(), collapsedIconColor); + // Check the collapsed text color when textColor is applied. + expect(getTextColor(), collapsedTextColor); + }); + + testWidgets('ExpansionTileTheme - expanded', (WidgetTester tester) async { + final Key tileKey = UniqueKey(); + final Key titleKey = UniqueKey(); + final Key iconKey = UniqueKey(); + const Color backgroundColor = Colors.orange; + const Color collapsedBackgroundColor = Colors.red; + const Color iconColor = Colors.green; + const Color collapsedIconColor = Colors.blue; + const Color textColor = Colors.black; + const Color collapsedTextColor = Colors.white; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + expansionTileTheme: const ExpansionTileThemeData( + backgroundColor: backgroundColor, + collapsedBackgroundColor: collapsedBackgroundColor, + tilePadding: EdgeInsets.fromLTRB(8, 12, 4, 10), + expandedAlignment: Alignment.centerRight, + childrenPadding: EdgeInsets.all(20.0), + iconColor: iconColor, + collapsedIconColor: collapsedIconColor, + textColor: textColor, + collapsedTextColor: collapsedTextColor, + ), + ), + home: Material( + child: Center( + child: ExpansionTile( + key: tileKey, + initiallyExpanded: true, + title: TestText('Expanded Tile', key: titleKey), + trailing: TestIcon(key: iconKey), + children: const [Text('Tile 1')], + ), + ), + ), + ), + ); + + final BoxDecoration boxDecoration = tester.firstWidget(find.descendant( + of: find.byKey(tileKey), + matching: find.byType(Container), + )).decoration! as BoxDecoration; + // Check the tile's background color when backgroundColor is applied. + expect(boxDecoration.color, backgroundColor); + + final Rect titleRect = tester.getRect(find.text('Expanded Tile')); + final Rect trailingRect = tester.getRect(find.byIcon(Icons.expand_more)); + final Rect listTileRect = tester.getRect(find.byType(ListTile)); + final Rect tallerWidget = titleRect.height > trailingRect.height ? titleRect : trailingRect; + + // Check the positions of title and trailing Widgets, after padding is applied. + expect(listTileRect.left, titleRect.left - 8); + expect(listTileRect.right, trailingRect.right + 4); + + // Calculate the remaining height of ListTile from the default height. + final double remainingHeight = 56 - tallerWidget.height; + expect(listTileRect.top, tallerWidget.top - remainingHeight / 2 - 12); + expect(listTileRect.bottom, tallerWidget.bottom + remainingHeight / 2 + 10); + + Color getIconColor() => tester.state(find.byType(TestIcon)).iconTheme.color!; + Color getTextColor() => tester.state(find.byType(TestText)).textStyle.color!; + + // Check the expanded icon color when iconColor is applied. + expect(getIconColor(), iconColor); + // Check the expanded text color when textColor is applied. + expect(getTextColor(), textColor); + + // Check the child position when expandedAlignment is applied. + final Rect childRect = tester.getRect(find.text('Tile 1')); + expect(childRect.right, 800 - 20); + expect(childRect.left, 800 - childRect.width - 20); + + // Check the child padding when childrenPadding is applied. + final Rect paddingRect = tester.getRect(find.byType(Padding).last); + expect(childRect.top, paddingRect.top + 20); + expect(childRect.left, paddingRect.left + 20); + expect(childRect.right, paddingRect.right - 20); + expect(childRect.bottom, paddingRect.bottom - 20); + }); +} diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index 8899744d8d..fe20c48446 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -430,6 +430,7 @@ void main() { sliderTheme: sliderTheme, tabBarTheme: const TabBarTheme(labelColor: Colors.black), tooltipTheme: const TooltipThemeData(height: 100), + expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black), cardTheme: const CardTheme(color: Colors.black), chipTheme: chipTheme, platform: TargetPlatform.iOS, @@ -529,6 +530,7 @@ void main() { sliderTheme: otherSliderTheme, tabBarTheme: const TabBarTheme(labelColor: Colors.white), tooltipTheme: const TooltipThemeData(height: 100), + expansionTileTheme: const ExpansionTileThemeData(backgroundColor: Colors.black), cardTheme: const CardTheme(color: Colors.white), chipTheme: otherChipTheme, platform: TargetPlatform.android, @@ -841,6 +843,7 @@ void main() { 'timePickerTheme', 'toggleButtonsTheme', 'tooltipTheme', + 'expansionTileTheme', // DEPRECATED (newest deprecations at the bottom) 'useTextSelectionTheme', 'textSelectionColor',