Created
February 24, 2020 01:38
-
-
Save Skylled/7ac0f2f99881f7df2a0a850e60ef2df0 to your computer and use it in GitHub Desktop.
ExpansionTileCard, based on Flutter's ExpansionTile, inspired by the Google Material Theme
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Originally based on ExpansionTile from Flutter. | |
// | |
// 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'; | |
const Duration _kExpand = Duration(milliseconds: 200); | |
/// A single-line [ListTile] with a trailing button that expands or collapses | |
/// the tile to reveal or hide the [children]. | |
/// | |
/// This widget is typically used with [ListView] to create an | |
/// "expand / collapse" list entry. When used with scrolling widgets like | |
/// [ListView], a unique [PageStorageKey] must be specified to enable the | |
/// [ExpansionTileCard] to save and restore its expanded state when it is scrolled | |
/// in and out of view. | |
/// | |
/// See also: | |
/// | |
/// * [ListTile], useful for creating expansion tile [children] when the | |
/// expansion tile represents a sublist. | |
/// * [ExpansionTile], the original widget on which this widget is based. | |
/// * The "Expand/collapse" section of | |
/// <https://material.io/guidelines/components/lists-controls.html>. | |
class ExpansionTileCard extends StatefulWidget { | |
/// Creates a single-line [ListTile] with a trailing button that expands or collapses | |
/// the tile to reveal or hide the [children]. The [initiallyExpanded] property must | |
/// be non-null. | |
const ExpansionTileCard({ | |
Key key, | |
this.leading, | |
@required this.title, | |
this.subtitle, | |
this.onExpansionChanged, | |
this.children = const <Widget>[], | |
this.trailing, | |
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)), | |
this.elevation = 2.0, | |
this.initiallyExpanded = false, | |
}) : assert(initiallyExpanded != null), | |
super(key: key); | |
/// A widget to display before the title. | |
/// | |
/// Typically a [CircleAvatar] widget. | |
final Widget leading; | |
/// The primary content of the list item. | |
/// | |
/// Typically a [Text] widget. | |
final Widget title; | |
/// Additional content displayed below the title. | |
/// | |
/// Typically a [Text] widget. | |
final Widget subtitle; | |
/// Called when the tile expands or collapses. | |
/// | |
/// When the tile starts expanding, this function is called with the value | |
/// true. When the tile starts collapsing, this function is called with | |
/// the value false. | |
final ValueChanged<bool> onExpansionChanged; | |
/// The widgets that are displayed when the tile expands. | |
/// | |
/// Typically [ListTile] widgets. | |
final List<Widget> children; | |
/// A widget to display instead of a rotating arrow icon. | |
final Widget trailing; | |
/// The radius used for the Material widget's border. Only visible once expanded. | |
/// | |
/// Defaults to a circular border with a radius of 8.0. | |
final BorderRadiusGeometry borderRadius; | |
/// The final elevation of the Material widget, once expanded. | |
/// | |
/// Defaults to 2.0. | |
final double elevation; | |
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default). | |
final bool initiallyExpanded; | |
@override | |
_ExpansionTileCardState createState() => _ExpansionTileCardState(); | |
} | |
class _ExpansionTileCardState extends State<ExpansionTileCard> | |
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.0, end: 0.5); | |
final ColorTween _headerColorTween = ColorTween(); | |
final ColorTween _iconColorTween = ColorTween(); | |
final ColorTween _materialColorTween = ColorTween(); | |
AnimationController _controller; | |
Animation<double> _iconTurns; | |
Animation<double> _heightFactor; | |
Animation<double> _elevationFactor; | |
Animation<Color> _headerColor; | |
Animation<Color> _iconColor; | |
Animation<Color> _materialColor; | |
bool _isExpanded = false; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController(duration: _kExpand, vsync: this); | |
_heightFactor = _controller.drive(_easeInTween); | |
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween)); | |
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween)); | |
_materialColor = _controller.drive(_materialColorTween.chain(_easeInTween)); | |
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween)); | |
_isExpanded = PageStorage.of(context)?.readState(context) as bool ?? | |
widget.initiallyExpanded; | |
_elevationFactor = _controller.drive(_easeOutTween); | |
if (_isExpanded) _controller.value = 1.0; | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
void _handleTap() { | |
setState(() { | |
_isExpanded = !_isExpanded; | |
if (_isExpanded) { | |
_controller.forward(); | |
} else { | |
_controller.reverse().then<void>((void value) { | |
if (!mounted) return; | |
setState(() { | |
// Rebuild without widget.children. | |
}); | |
}); | |
} | |
PageStorage.of(context)?.writeState(context, _isExpanded); | |
}); | |
if (widget.onExpansionChanged != null) | |
widget.onExpansionChanged(_isExpanded); | |
} | |
Widget _buildChildren(BuildContext context, Widget child) { | |
return Padding( | |
padding: EdgeInsets.symmetric( | |
vertical: 6.0 * _heightFactor.value, | |
), | |
child: Material( | |
type: MaterialType.card, | |
color: _materialColor.value, | |
borderRadius: widget.borderRadius, | |
elevation: widget.elevation * _elevationFactor.value, | |
child: Container( | |
padding: EdgeInsets.all(2.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
ListTileTheme.merge( | |
iconColor: _iconColor.value, | |
textColor: _headerColor.value, | |
child: ListTile( | |
onTap: _handleTap, | |
leading: widget.leading, | |
title: widget.title, | |
subtitle: widget.subtitle, | |
trailing: widget.trailing ?? | |
RotationTransition( | |
turns: _iconTurns, | |
child: const Icon(Icons.expand_more), | |
), | |
), | |
), | |
ClipRect( | |
child: Align( | |
heightFactor: _heightFactor.value, | |
child: child, | |
), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
@override | |
void didChangeDependencies() { | |
final ThemeData theme = Theme.of(context); | |
_headerColorTween | |
..begin = theme.textTheme.subtitle1.color | |
..end = theme.accentColor; | |
_iconColorTween | |
..begin = theme.unselectedWidgetColor | |
..end = theme.accentColor; | |
_materialColorTween | |
..begin = theme.canvasColor | |
..end = theme.cardColor; | |
super.didChangeDependencies(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final bool closed = !_isExpanded && _controller.isDismissed; | |
return AnimatedBuilder( | |
animation: _controller.view, | |
builder: _buildChildren, | |
child: closed ? null : Column(children: widget.children), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment