Skip to content

Instantly share code, notes, and snippets.

@DaisukeNagata
Created July 30, 2023 03:32
Show Gist options
  • Save DaisukeNagata/5cd75375dc33860073f2bb674965107e to your computer and use it in GitHub Desktop.
Save DaisukeNagata/5cd75375dc33860073f2bb674965107e to your computer and use it in GitHub Desktop.
not change example
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,
),
),
),
),
);
},
);
}
}
@DaisukeNagata
Copy link
Author

DaisukeNagata commented Jul 31, 2023

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