Skip to content

Instantly share code, notes, and snippets.

@DaisukeNagata
Created July 21, 2023 13:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DaisukeNagata/990edc6f6ed9901ede5ad9bd9b4620f1 to your computer and use it in GitHub Desktop.
Save DaisukeNagata/990edc6f6ed9901ede5ad9bd9b4620f1 to your computer and use it in GitHub Desktop.
Expanded Simple Example
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,
),
),
),
),
);
},
);
}
}
@DaisukeNagata
Copy link
Author

default.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment