작업에 필요한 접고 펼치는 타일을 찾다보니 Flutter에서 제공하는 ExpansionTile을 찾게 되었는데 아쉽게도 우측의 화살표가 너무 거슬렸다.
그래서 ExpansionTile을 그대로 Custom 해보기로 했다.
AnimationController을 생성할때 value에 1 또는 0을 넣도록하였는데 위젯이 생성될때에 펼쳐져있는 상태로 생성할지 닫혀져있는 상태로 생성할지를 뜻한다.
아래의 이미지는 AnimationController에 lowerBound, upperBound가 있는데 따로 값을 설정하지 않으면 0과 1로 설정이 되어있다.
더 아래에 보면 _internalSetValue 가 있는데 초기값 설정을 value로 설정을하거나 value가 null인 경우 lowerBound로 설정하는것으로 되어있다.
import 'package:flutter/material.dart';
import 'package:flutter_app/presentations/components/default_button.dart';
const Duration _kExpand = Duration(milliseconds: 200);
class ExpandableTile extends StatefulWidget {
final Widget titleWidget;
final Widget childWidget;
final EdgeInsets? tilePadding;
final ValueChanged<bool>? onExpansionChanged;
final Widget? trailing;
final bool isExpanded;
const ExpandableTile({
super.key,
required this.titleWidget,
required this.childWidget,
this.onExpansionChanged,
this.trailing,
this.tilePadding,
this.isExpanded = false,
});
@override
State<ExpandableTile> createState() => _ExpandableTileState();
}
class _ExpandableTileState extends State<ExpandableTile> with SingleTickerProviderStateMixin {
late bool _isExpanded;
static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn);
late AnimationController _animationController;
late Animation<double> _heightFactor;
@override
void initState() {
super.initState();
_isExpanded = widget.isExpanded;
_animationController = AnimationController(duration: _kExpand, vsync: this, value: _isExpanded ? 1 : 0);
_heightFactor = _animationController.drive(_easeInTween);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleTap() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse().then<void>((void value) {
if (!mounted) {
return;
}
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.maybeOf(context)?.writeState(context, _isExpanded);
});
widget.onExpansionChanged?.call(_isExpanded);
}
Widget _buildChildren(BuildContext context, Widget? child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
DefaultButton(
padding: widget.tilePadding,
childWidget: widget.titleWidget,
onPressed: _handleTap,
),
ClipRect(
child: Align(
alignment: Alignment.center,
heightFactor: _heightFactor.value,
child: child,
),
),
],
);
}
@override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _animationController.isDismissed;
final bool shouldRemoveChildren = closed;
final Widget result = Offstage(
offstage: closed,
child: TickerMode(
enabled: !closed,
child: widget.childWidget,
),
);
return AnimatedBuilder(
animation: _animationController.view,
builder: _buildChildren,
child: shouldRemoveChildren ? null : result,
);
}
}