Created
July 21, 2023 13:45
-
-
Save DaisukeNagata/990edc6f6ed9901ede5ad9bd9b4620f1 to your computer and use it in GitHub Desktop.
Expanded Simple Example
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
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
const Duration _kExpand = Duration(milliseconds: 1000); <- at any speed | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: Scaffold( | |
appBar: AppBar( | |
title: const Text('Expanded'), | |
), | |
body: const MyAccordion(), | |
), | |
); | |
} | |
} | |
class MyAccordion extends StatefulWidget { | |
const MyAccordion({super.key}); | |
@override | |
State<MyAccordion> createState() => _MyAccordionState(); | |
} | |
class _MyAccordionState extends State<MyAccordion> { | |
List<Item> _data = []; | |
@override | |
void initState() { | |
super.initState(); | |
_data = generateItems(5); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return ListView.builder( | |
itemCount: _data.length, | |
itemBuilder: (context, index) { | |
return ExpandetWidget( | |
height: 50.0, | |
padding: const EdgeInsets.only( | |
left: 24, | |
right: 24, | |
), | |
childrenPadding: const EdgeInsets.symmetric(horizontal: 24.0), | |
index: index, | |
titleList: _data[index].expandedValue, | |
controlAffinity: null, | |
gestureTapCallback: () { | |
setState(() { | |
_data[index].isExpanded = !_data[index].isExpanded; | |
}); | |
}, | |
title: Container( | |
decoration: BoxDecoration( | |
color: Colors.white, | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.grey.withOpacity(0.5), | |
spreadRadius: 1, | |
blurRadius: 3, | |
offset: const Offset(0, 4), | |
), | |
], | |
), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: [ | |
Text(_data[index].headerValue), | |
const Text('サブタイトル', textAlign: TextAlign.left), | |
Icon( | |
_data[index].isExpanded | |
? Icons.keyboard_arrow_up | |
: Icons.keyboard_arrow_down, | |
), | |
], | |
), | |
), | |
trailing: null, // 矢印を表示しない | |
children: _data[index].isExpanded | |
? [ | |
Text(_data[index].expandedValue), | |
Icon( | |
_data[index].isExpanded | |
? Icons.keyboard_arrow_up | |
: Icons.keyboard_arrow_down, | |
), | |
] | |
: [], | |
); | |
}, | |
); | |
} | |
} | |
class Item { | |
Item({ | |
required this.expandedValue, | |
required this.headerValue, | |
this.isExpanded = false, | |
}); | |
String expandedValue; | |
String headerValue; | |
bool isExpanded; | |
} | |
List<Item> generateItems(int numberOfItems) { | |
var rng = Random(); | |
return List<Item>.generate(numberOfItems, (int index) { | |
String expandedValue = 'This is item number ' * rng.nextInt(100); | |
expandedValue += ' $index'; | |
return Item( | |
headerValue: 'Header $index', | |
expandedValue: expandedValue, | |
); | |
}); | |
} | |
class ExpandetWidget extends StatefulWidget { | |
const ExpandetWidget({ | |
super.key, | |
this.leading, | |
required this.title, | |
required this.height, | |
required this.padding, | |
required this.index, | |
required this.titleList, | |
required this.gestureTapCallback, | |
this.subtitle, | |
this.onExpansionChanged, | |
this.children = const <Widget>[], | |
this.trailing, | |
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.controlAffinity, | |
}) : assert( | |
expandedCrossAxisAlignment != CrossAxisAlignment.baseline, | |
'error', | |
); | |
final Widget? leading; | |
final Widget title; | |
final double height; | |
final EdgeInsets padding; | |
final int index; | |
final String titleList; | |
final GestureLongPressCallback gestureTapCallback; | |
final Widget? subtitle; | |
final ValueChanged<bool>? onExpansionChanged; | |
final List<Widget> children; | |
final Color? backgroundColor; | |
final Color? collapsedBackgroundColor; | |
final Widget? trailing; | |
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 ListTileControlAffinity? controlAffinity; | |
@override | |
State<ExpandetWidget> createState() => _ExpandetWidgetState(); | |
} | |
class _ExpandetWidgetState extends State<ExpandetWidget> | |
with SingleTickerProviderStateMixin { | |
static final Animatable<double> _easeInTween = | |
CurveTween(curve: Curves.easeIn); | |
static final Animatable<double> _halfTween = | |
Tween<double>(begin: 0, end: 0.5); | |
final ColorTween _borderColorTween = ColorTween(); | |
final ColorTween _headerColorTween = ColorTween(); | |
final ColorTween _iconColorTween = ColorTween(); | |
final ColorTween _backgroundColorTween = ColorTween(); | |
late AnimationController _controller; | |
late Animation<double> _iconTurns; | |
late Animation<double> _heightFactor; | |
late Animation<Color?> _headerColor; | |
late Animation<Color?> _iconColor; | |
late bool _isExpanded = true; | |
@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)); | |
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween)); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
void _handleTap() { | |
setState(() { | |
if (_isExpanded) { | |
_isExpanded = !_isExpanded; | |
widget.gestureTapCallback(); | |
_controller.forward(); | |
} else { | |
_controller.reverse().whenComplete(() => { | |
_isExpanded = !_isExpanded, | |
widget.gestureTapCallback(), | |
}); | |
} | |
}); | |
} | |
// Platform or null affinity defaults to trailing. | |
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 (widget.controlAffinity == null) { | |
return null; | |
} | |
if (_effectiveAffinity(widget.controlAffinity) != | |
ListTileControlAffinity.leading) { | |
return null; | |
} | |
return _buildIcon(context); | |
} | |
Widget? _buildTrailingIcon(BuildContext context) { | |
if (widget.controlAffinity == null) { | |
return null; | |
} | |
if (_effectiveAffinity(widget.controlAffinity) != | |
ListTileControlAffinity.trailing) { | |
return null; | |
} | |
return _buildIcon(context); | |
} | |
Widget _buildChildren(BuildContext context, Widget? child) { | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Stack( | |
alignment: Alignment.center, | |
children: [ | |
ListTileTheme.merge( | |
iconColor: _iconColor.value, | |
textColor: _headerColor.value, | |
child: ListTile( | |
contentPadding: widget.padding, | |
leading: widget.leading ?? _buildLeadingIcon(context), | |
title: widget.title, | |
subtitle: widget.subtitle, | |
trailing: widget.trailing ?? _buildTrailingIcon(context), | |
), | |
), | |
Positioned( | |
left: widget.padding.left, | |
top: widget.padding.top, | |
right: widget.padding.right, | |
bottom: widget.padding.bottom, | |
child: Material( | |
color: Colors.transparent, | |
child: InkWell( | |
onTap: _handleTap, | |
borderRadius: BorderRadius.circular(0.0), | |
), | |
), | |
), | |
], | |
), | |
ClipRect( | |
child: Align( | |
alignment: widget.expandedAlignment ?? Alignment.center, | |
heightFactor: _heightFactor.value, | |
child: child, | |
), | |
), | |
], | |
); | |
} | |
@override | |
void didChangeDependencies() { | |
final theme = Theme.of(context); | |
final colorScheme = theme.colorScheme; | |
_borderColorTween.end = theme.dividerColor; | |
_headerColorTween | |
..begin = widget.collapsedTextColor ?? theme.textTheme.titleMedium!.color | |
..end = widget.textColor ?? colorScheme.primary; | |
_iconColorTween | |
..begin = widget.collapsedIconColor ?? theme.unselectedWidgetColor | |
..end = widget.iconColor ?? colorScheme.primary; | |
_backgroundColorTween | |
..begin = widget.collapsedBackgroundColor | |
..end = widget.backgroundColor; | |
super.didChangeDependencies(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final closed = !_isExpanded && _controller.isDismissed; | |
final shouldRemoveChildren = closed && !widget.maintainState; | |
return LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
return AnimatedBuilder( | |
animation: _controller.view, | |
builder: _buildChildren, | |
child: GestureDetector( | |
onTap: _handleTap, | |
child: shouldRemoveChildren | |
? null | |
: Offstage( | |
offstage: closed, | |
child: Padding( | |
padding: widget.childrenPadding ?? EdgeInsets.zero, | |
child: Column( | |
crossAxisAlignment: widget.expandedCrossAxisAlignment ?? | |
CrossAxisAlignment.center, | |
children: widget.children, | |
), | |
), | |
), | |
), | |
); | |
}, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
default.mov