Reputation: 1255
I am using a Expansion tile, but I do not want to have any trailing space occupied at the end of the widget. Event though i remove the trailing ICON with sized box, still the space for the ICON is occupied, I do not want the entire space for the content rather then the trailing ICON.
Below is my code:
ExpansionTile(
initiallyExpanded: expanded,
trailing: const SizedBox.shrink(),
textColor: Colors.teal,
collapsedTextColor: Colors.teal,
title: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Center(
child: Text(title,
style: const TextStyle(
color: Colors.teal,
)))),
iconColor: Colors.green,
collapsedIconColor: Colors.teal,
children: body)
Below is the image:
Upvotes: 1
Views: 3495
Reputation: 11
I've found a way to resolve this. All you have to do is go to the ExpansionTile Widget and make a copy of it. Create a new file and edit the widget's name to your desired name. Then remove the trailing property from the ListTile widget inside this. Now you got a ExpansionTile widget without the trailing gap. I'll attach an example.
Thanks to Satheesh Kumar for this wonderful idea
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const Duration _kExpand = Duration(milliseconds: 200);
class ExpansionTileController {
ExpansionTileController();
_MyExpansionTileState? _state;
bool get isExpanded {
assert(_state != null, 'line 14');
return _state!._isExpanded;
}
void expand() {
assert(_state != null, 'line 19');
if (!isExpanded) {
_state!._toggleExpansion();
}
}
void collapse() {
assert(_state != null, 'line 27');
if (isExpanded) {
_state!._toggleExpansion();
}
}
static ExpansionTileController of(BuildContext context) {
final result = context.findAncestorStateOfType<_MyExpansionTileState>();
if (result != null) {
return result._tileController;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'ExpansionTileController.of() called with a context that does not contain a ExpansionTile.',
),
ErrorDescription(
'No ExpansionTile ancestor could be found starting from the context that was passed to ExpansionTileController.of(). '
'This usually happens when the context provided is from the same StatefulWidget as that '
'whose build function actually creates the ExpansionTile widget being sought.',
),
ErrorHint(
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
'context that is "under" the ExpansionTile. For an example of this, please see the '
'documentation for ExpansionTileController.of():\n'
' https://api.flutter.dev/flutter/material/ExpansionTile/of.html',
),
ErrorHint(
'A more efficient solution is to split your build function into several widgets. This '
'introduces a new context from which you can obtain the ExpansionTile. In this solution, '
'you would have an outer widget that creates the ExpansionTile populated by instances of '
'your new inner widgets, and then in these inner widgets you would use ExpansionTileController.of().\n'
'An other solution is assign a GlobalKey to the ExpansionTile, '
'then use the key.currentState property to obtain the ExpansionTile rather than '
'using the ExpansionTileController.of() function.',
),
context.describeElement('The context used was'),
]);
}
static ExpansionTileController? maybeOf(BuildContext context) {
return context
.findAncestorStateOfType<_MyExpansionTileState>()
?._tileController;
}
}
class MyExpansionTile extends StatefulWidget {
const MyExpansionTile({
required this.title,
super.key,
this.leading,
this.subtitle,
this.onExpansionChanged,
this.children = const <Widget>[],
this.initiallyExpanded = false,
this.maintainState = false,
this.tilePadding,
this.expandedCrossAxisAlignment,
this.expandedAlignment,
this.childrenPadding,
this.backgroundColor,
this.collapsedBackgroundColor,
this.textColor,
this.collapsedTextColor,
this.iconColor,
this.collapsedIconColor,
this.shape,
this.collapsedShape,
this.clipBehavior,
this.controlAffinity,
this.controller,
this.dense,
this.visualDensity,
this.enableFeedback = true,
this.enabled = true,
this.expansionAnimationStyle,
}) : assert(
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
'CrossAxisAlignment.baseline is not supported since the expanded children '
'are aligned in a column, not a row. Try to use another constant.',
);
final Widget? leading;
final Widget title;
final Widget? subtitle;
final ValueChanged<bool>? onExpansionChanged;
final List<Widget> children;
final Color? backgroundColor;
final Color? collapsedBackgroundColor;
final bool initiallyExpanded;
final bool maintainState;
final EdgeInsetsGeometry? tilePadding;
final Alignment? expandedAlignment;
final CrossAxisAlignment? expandedCrossAxisAlignment;
final EdgeInsetsGeometry? childrenPadding;
final Color? iconColor;
final Color? collapsedIconColor;
final Color? textColor;
final Color? collapsedTextColor;
final ShapeBorder? shape;
final ShapeBorder? collapsedShape;
final Clip? clipBehavior;
final ListTileControlAffinity? controlAffinity;
final ExpansionTileController? controller;
final bool? dense;
final VisualDensity? visualDensity;
final bool? enableFeedback;
final bool enabled;
final AnimationStyle? expansionAnimationStyle;
@override
State<MyExpansionTile> createState() => _MyExpansionTileState();
}
class _MyExpansionTileState extends State<MyExpansionTile>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeOutTween =
CurveTween(curve: Curves.easeOut);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0, end: 0.5);
final ShapeBorderTween _borderTween = ShapeBorderTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
final CurveTween _heightFactorTween = CurveTween(curve: Curves.easeIn);
late AnimationController _animationController;
late Animation<double> _iconTurns;
late Animation<double> _heightFactor;
late Animation<ShapeBorder?> _border;
late Animation<Color?> _headerColor;
late Animation<Color?> _iconColor;
late Animation<Color?> _backgroundColor;
bool _isExpanded = false;
late ExpansionTileController _tileController;
@override
void initState() {
super.initState();
_animationController = AnimationController(duration: _kExpand, vsync: this);
_heightFactor = _animationController.drive(_heightFactorTween);
_iconTurns = _animationController.drive(_halfTween.chain(_easeInTween));
_border = _animationController.drive(_borderTween.chain(_easeOutTween));
_headerColor =
_animationController.drive(_headerColorTween.chain(_easeInTween));
_iconColor =
_animationController.drive(_iconColorTween.chain(_easeInTween));
_backgroundColor =
_animationController.drive(_backgroundColorTween.chain(_easeOutTween));
_isExpanded = PageStorage.maybeOf(context)?.readState(context) as bool? ??
widget.initiallyExpanded;
if (_isExpanded) {
_animationController.value = 1.0;
}
assert(widget.controller?._state == null, 'line 190');
_tileController = widget.controller ?? ExpansionTileController();
_tileController._state = this;
}
@override
void dispose() {
_tileController._state = null;
_animationController.dispose();
super.dispose();
}
void _toggleExpansion() {
final textDirection = WidgetsLocalizations.of(context).textDirection;
final localizations = MaterialLocalizations.of(context);
final stateHint =
_isExpanded ? localizations.expandedHint : localizations.collapsedHint;
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse().then<void>((void value) {
if (!mounted) {
return;
}
setState(() {});
});
}
PageStorage.maybeOf(context)?.writeState(context, _isExpanded);
});
widget.onExpansionChanged?.call(_isExpanded);
SemanticsService.announce(stateHint, textDirection);
}
void _handleTap() {
_toggleExpansion();
}
ListTileControlAffinity _effectiveAffinity(
ListTileControlAffinity? affinity,
) {
switch (affinity ?? ListTileControlAffinity.trailing) {
case ListTileControlAffinity.leading:
return ListTileControlAffinity.leading;
case ListTileControlAffinity.trailing:
case ListTileControlAffinity.platform:
return ListTileControlAffinity.trailing;
}
}
Widget? _buildIcon(BuildContext context) {
return RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
);
}
Widget? _buildLeadingIcon(BuildContext context) {
if (_effectiveAffinity(widget.controlAffinity) !=
ListTileControlAffinity.leading) {
return null;
}
return _buildIcon(context);
}
Widget _buildChildren(BuildContext context, Widget? child) {
final theme = Theme.of(context);
final expansionTileTheme = ExpansionTileTheme.of(context);
final expansionTileBorder = _border.value ??
const Border(
top: BorderSide(color: Colors.transparent),
bottom: BorderSide(color: Colors.transparent),
);
final clipBehavior =
widget.clipBehavior ?? expansionTileTheme.clipBehavior ?? Clip.none;
final localizations = MaterialLocalizations.of(context);
final onTapHint = _isExpanded
? localizations.expansionTileExpandedTapHint
: localizations.expansionTileCollapsedTapHint;
String? semanticsHint;
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
semanticsHint = _isExpanded
? '${localizations.collapsedHint}\n ${localizations.expansionTileExpandedHint}'
: '${localizations.expandedHint}\n ${localizations.expansionTileCollapsedHint}';
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
}
return Container(
clipBehavior: clipBehavior,
decoration: ShapeDecoration(
color: _backgroundColor.value ??
expansionTileTheme.backgroundColor ??
Colors.transparent,
shape: expansionTileBorder,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Semantics(
hint: semanticsHint,
onTapHint: onTapHint,
child: ListTileTheme.merge(
iconColor: _iconColor.value ?? expansionTileTheme.iconColor,
textColor: _headerColor.value,
child: ListTile(
enabled: widget.enabled,
onTap: _handleTap,
dense: widget.dense,
visualDensity: widget.visualDensity,
enableFeedback: widget.enableFeedback,
contentPadding:
widget.tilePadding ?? expansionTileTheme.tilePadding,
leading: widget.leading ?? _buildLeadingIcon(context),
title: widget.title,
subtitle: widget.subtitle,
),
),
),
ClipRect(
child: Align(
alignment: widget.expandedAlignment ??
expansionTileTheme.expandedAlignment ??
Alignment.center,
heightFactor: _heightFactor.value,
child: child,
),
),
],
),
);
}
@override
void didUpdateWidget(covariant MyExpansionTile oldWidget) {
super.didUpdateWidget(oldWidget);
final theme = Theme.of(context);
final expansionTileTheme = ExpansionTileTheme.of(context);
final defaults = theme.useMaterial3
? _ExpansionTileDefaultsM3(context)
: _ExpansionTileDefaultsM2(context);
if (widget.collapsedShape != oldWidget.collapsedShape ||
widget.shape != oldWidget.shape) {
_updateShapeBorder(expansionTileTheme, theme);
}
if (widget.collapsedTextColor != oldWidget.collapsedTextColor ||
widget.textColor != oldWidget.textColor) {
_updateHeaderColor(expansionTileTheme, defaults);
}
if (widget.collapsedIconColor != oldWidget.collapsedIconColor ||
widget.iconColor != oldWidget.iconColor) {
_updateIconColor(expansionTileTheme, defaults);
}
if (widget.backgroundColor != oldWidget.backgroundColor ||
widget.collapsedBackgroundColor != oldWidget.collapsedBackgroundColor) {
_updateBackgroundColor(expansionTileTheme);
}
if (widget.expansionAnimationStyle != oldWidget.expansionAnimationStyle) {
_updateAnimationDuration(expansionTileTheme);
_updateHeightFactorCurve(expansionTileTheme);
}
}
@override
void didChangeDependencies() {
final theme = Theme.of(context);
final expansionTileTheme = ExpansionTileTheme.of(context);
final defaults = theme.useMaterial3
? _ExpansionTileDefaultsM3(context)
: _ExpansionTileDefaultsM2(context);
_updateAnimationDuration(expansionTileTheme);
_updateShapeBorder(expansionTileTheme, theme);
_updateHeaderColor(expansionTileTheme, defaults);
_updateIconColor(expansionTileTheme, defaults);
_updateBackgroundColor(expansionTileTheme);
_updateHeightFactorCurve(expansionTileTheme);
super.didChangeDependencies();
}
void _updateAnimationDuration(ExpansionTileThemeData expansionTileTheme) {
_animationController.duration = widget.expansionAnimationStyle?.duration ??
expansionTileTheme.expansionAnimationStyle?.duration ??
_kExpand;
}
void _updateShapeBorder(
ExpansionTileThemeData expansionTileTheme,
ThemeData theme,
) {
_borderTween
..begin = widget.collapsedShape ??
expansionTileTheme.collapsedShape ??
const Border(
top: BorderSide(color: Colors.transparent),
bottom: BorderSide(color: Colors.transparent),
)
..end = widget.shape ??
expansionTileTheme.shape ??
Border(
top: BorderSide(color: theme.dividerColor),
bottom: BorderSide(color: theme.dividerColor),
);
}
void _updateHeaderColor(
ExpansionTileThemeData expansionTileTheme,
ExpansionTileThemeData defaults,
) {
_headerColorTween
..begin = widget.collapsedTextColor ??
expansionTileTheme.collapsedTextColor ??
defaults.collapsedTextColor
..end = widget.textColor ??
expansionTileTheme.textColor ??
defaults.textColor;
}
void _updateIconColor(
ExpansionTileThemeData expansionTileTheme,
ExpansionTileThemeData defaults,
) {
_iconColorTween
..begin = widget.collapsedIconColor ??
expansionTileTheme.collapsedIconColor ??
defaults.collapsedIconColor
..end = widget.iconColor ??
expansionTileTheme.iconColor ??
defaults.iconColor;
}
void _updateBackgroundColor(ExpansionTileThemeData expansionTileTheme) {
_backgroundColorTween
..begin = widget.collapsedBackgroundColor ??
expansionTileTheme.collapsedBackgroundColor
..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
}
void _updateHeightFactorCurve(ExpansionTileThemeData expansionTileTheme) {
_heightFactorTween.curve = widget.expansionAnimationStyle?.curve ??
expansionTileTheme.expansionAnimationStyle?.curve ??
Curves.easeIn;
}
@override
Widget build(BuildContext context) {
final expansionTileTheme = ExpansionTileTheme.of(context);
final closed = !_isExpanded && _animationController.isDismissed;
final shouldRemoveChildren = closed && !widget.maintainState;
final Widget result = Offstage(
offstage: closed,
child: TickerMode(
enabled: !closed,
child: Padding(
padding: widget.childrenPadding ??
expansionTileTheme.childrenPadding ??
EdgeInsets.zero,
child: Column(
crossAxisAlignment:
widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center,
children: widget.children,
),
),
),
);
return AnimatedBuilder(
animation: _animationController.view,
builder: _buildChildren,
child: shouldRemoveChildren ? null : result,
);
}
}
class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData {
_ExpansionTileDefaultsM2(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colorScheme = _theme.colorScheme;
@override
Color? get textColor => _colorScheme.primary;
@override
Color? get iconColor => _colorScheme.primary;
@override
Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color;
@override
Color? get collapsedIconColor => _theme.unselectedWidgetColor;
}
class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData {
_ExpansionTileDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
Color? get textColor => _colors.onSurface;
@override
Color? get iconColor => _colors.primary;
@override
Color? get collapsedTextColor => _colors.onSurface;
@override
Color? get collapsedIconColor => _colors.onSurfaceVariant;
}
Upvotes: 1
Reputation: 6455
As per your desired behavior, you can just use a ListTile:
ListTile(
trailing: const SizedBox.shrink(),
textColor: Colors.teal,
title: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Center(
child: Text(title,
style: const TextStyle(
color: Colors.teal,
)
)
)
),
children: body)
Note: You might need to wrap the ListTile inside of an Expanded widget if it is within a Row/Column
Upvotes: -1