Created
July 30, 2023 03:32
-
-
Save DaisukeNagata/5cd75375dc33860073f2bb674965107e to your computer and use it in GitHub Desktop.
not change 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 'package:flutter/material.dart'; | |
import 'package:flutter_riverpod/flutter_riverpod.dart'; | |
import 'package:flutter_hooks/flutter_hooks.dart'; | |
const Duration _kExpand = Duration(milliseconds: 300); | |
final counterProvider = | |
StateNotifierProvider<CounterNotifier, CounterState>((ref) { | |
return CounterNotifier(); | |
}); | |
final itemProvider = | |
StateNotifierProvider<ItemNotifier, List<Item>>((ref) => ItemNotifier()); | |
class ItemNotifier extends StateNotifier<List<Item>> { | |
ItemNotifier() : super([]); | |
void addItem(Item item) { | |
state = [...state, item]; | |
} | |
void toggleIsExpanded(int index) { | |
var updatedItem = | |
state[index].copyWith(isExpanded: !state[index].isExpanded); | |
state = [ | |
...state.sublist(0, index), | |
updatedItem, | |
...state.sublist(index + 1), | |
]; | |
} | |
} | |
class Item { | |
Item({ | |
required this.expandedValue, | |
required this.headerValue, | |
this.isExpanded = false, | |
}); | |
final String expandedValue; | |
final String headerValue; | |
final bool isExpanded; | |
Item copyWith({ | |
String? expandedValue, | |
String? headerValue, | |
bool? isExpanded, | |
}) { | |
return Item( | |
expandedValue: expandedValue ?? this.expandedValue, | |
headerValue: headerValue ?? this.headerValue, | |
isExpanded: isExpanded ?? this.isExpanded, | |
); | |
} | |
} | |
class CounterState { | |
int count; | |
bool isLoading; | |
CounterState(this.count, this.isLoading); | |
} | |
class CounterNotifier extends StateNotifier<CounterState> { | |
CounterNotifier() : super(CounterState(0, false)); | |
void increment() { | |
state = CounterState(state.count + 1, false); | |
} | |
Future<void> incrementAfterDelay() async { | |
state = CounterState(state.count, true); | |
await Future.delayed(const Duration(seconds: 1)); | |
increment(); | |
} | |
} | |
void main() => runApp(const ProviderScope(child: CounterApp())); | |
class CounterApp extends StatelessWidget { | |
const CounterApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
home: CounterPage(), | |
); | |
} | |
} | |
class CounterPage extends ConsumerWidget { | |
const CounterPage({super.key}); | |
@override | |
Widget build(BuildContext context, WidgetRef ref) { | |
final state = ref.watch(counterProvider); | |
return Scaffold( | |
appBar: AppBar( | |
title: GestureDetector( | |
child: const Text('CounterPage'), | |
onTap: () { | |
Navigator.push( | |
context, | |
MaterialPageRoute( | |
builder: (context) => SecondCounterPage(ref: ref)), | |
); | |
}, | |
), | |
), | |
body: Center( | |
child: state.isLoading | |
? const CircularProgressIndicator() | |
: Text('Count: ${state.count}'), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: () { | |
ref.read(counterProvider.notifier).incrementAfterDelay(); | |
ref.read(itemProvider.notifier).addItem(Item( | |
expandedValue: 'Expanded${state.count}', | |
headerValue: 'Header${state.count}', | |
isExpanded: true, | |
)); | |
}, | |
tooltip: 'Increment', | |
child: const Icon(Icons.add), | |
), | |
); | |
} | |
} | |
class SecondCounterPage extends HookWidget { | |
const SecondCounterPage({super.key, required this.ref}); | |
final WidgetRef ref; | |
@override | |
Widget build(BuildContext context) { | |
final itemNotifier = ref.watch(itemProvider.notifier); | |
final items = ref.watch(itemProvider); | |
final isLoading = useState(false); | |
final state = ref.watch(counterProvider); | |
void incrementCounter() async { | |
isLoading.value = true; | |
ref.read(counterProvider.notifier).incrementAfterDelay(); | |
await Future.delayed(const Duration(seconds: 3)); | |
isLoading.value = false; | |
final newItem = Item( | |
expandedValue: 'Expanded${state.count}', | |
headerValue: 'Header${state.count}', | |
isExpanded: true, | |
); | |
itemNotifier.addItem(newItem); | |
isLoading.value = false; | |
} | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('SecondCounterPage'), | |
), | |
body: Center( | |
child: isLoading.value | |
? const CircularProgressIndicator() | |
: ListView.builder( | |
itemCount: items.length, | |
itemBuilder: (context, index) { | |
final item = itemNotifier.debugState[index]; | |
return ExpandetWidget( | |
height: 50.0, | |
padding: const EdgeInsets.only( | |
left: 24, | |
right: 24, | |
), | |
childrenPadding: | |
const EdgeInsets.symmetric(horizontal: 24.0), | |
index: index, | |
titleList: item.expandedValue, | |
controlAffinity: null, | |
gestureTapCallback: () { | |
itemNotifier.toggleIsExpanded(index); | |
}, | |
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(item.headerValue), | |
const Text('サブタイトル', textAlign: TextAlign.left), | |
Icon( | |
item.isExpanded <- not change | |
? Icons.keyboard_arrow_down | |
: Icons.keyboard_arrow_up, | |
), | |
], | |
), | |
), | |
trailing: null, // 矢印を表示しない | |
children: item.isExpanded | |
? [ | |
Text(item.expandedValue), | |
Icon( | |
item.isExpanded <- not change | |
? Icons.keyboard_arrow_down | |
: Icons.keyboard_arrow_up, | |
), | |
] | |
: [], | |
); | |
}, | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: incrementCounter, | |
tooltip: 'Increment', | |
child: const Icon(Icons.add), | |
), | |
); | |
} | |
} | |
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, | |
), | |
), | |
), | |
), | |
); | |
}, | |
); | |
} | |
} |
2023-07-31.23.50.00.mov
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this is refactor