mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-02-28 23:40:40 +00:00
deleted ExpandableSectionList
This commit is contained in:
parent
2c9ca45517
commit
beee9d8a6e
3 changed files with 0 additions and 335 deletions
|
@ -1,56 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
|
|
||||||
class ExpandableSectionNameDialog extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final String hint;
|
|
||||||
final String initialValue;
|
|
||||||
|
|
||||||
const ExpandableSectionNameDialog({
|
|
||||||
this.initialValue = '',
|
|
||||||
required this.title,
|
|
||||||
required this.hint,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpandableSectionNameDialog> createState() => _ExpandableSectionNameDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ExpandableSectionNameDialogState extends State<ExpandableSectionNameDialog> {
|
|
||||||
late final _nameController = TextEditingController(text: widget.initialValue);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
icon: const Icon(Icons.edit_outlined),
|
|
||||||
titlePadding: Dimens.dialogIconTitlePadding,
|
|
||||||
title: Text(widget.title),
|
|
||||||
content: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
controller: _nameController,
|
|
||||||
decoration: InputDecoration(hintText: widget.hint),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: Navigator.of(context).pop,
|
|
||||||
child: Text(S.of(context).cancel),
|
|
||||||
),
|
|
||||||
ValueListenableBuilder(
|
|
||||||
valueListenable: _nameController,
|
|
||||||
builder: (_, value, __) => TextButton(
|
|
||||||
onPressed: value.text.isNotEmpty ? () => Navigator.of(context).pop(value.text) : null,
|
|
||||||
child: Text(S.of(context).save),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
|
|
||||||
class ExpandableSectionListItem extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final VoidCallback onTitleTap;
|
|
||||||
final VoidCallback onExpand;
|
|
||||||
final List<IconButton> actions;
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
const ExpandableSectionListItem({
|
|
||||||
required this.title,
|
|
||||||
required this.onTitleTap,
|
|
||||||
required this.onExpand,
|
|
||||||
required this.actions,
|
|
||||||
required this.children,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
static ExpandableSectionListItemState of(BuildContext context) {
|
|
||||||
return context.findAncestorStateOfType<ExpandableSectionListItemState>()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpandableSectionListItem> createState() => ExpandableSectionListItemState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpandableSectionListItemState extends State<ExpandableSectionListItem> with TickerProviderStateMixin {
|
|
||||||
late final AnimationController _controller = AnimationController(
|
|
||||||
duration: Dimens.durationM,
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
bool get _expanded => _controller.isCompleted;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
_AnimatedNameLeading(controller: _controller),
|
|
||||||
const SizedBox(width: Dimens.grid8),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
widget.title,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: _AnimatedArrowButton(
|
|
||||||
controller: _controller,
|
|
||||||
onPressed: () => _expanded ? collapse() : expand(),
|
|
||||||
),
|
|
||||||
onTap: () => _expanded ? widget.onTitleTap() : expand(),
|
|
||||||
),
|
|
||||||
_AnimatedContent(
|
|
||||||
controller: _controller,
|
|
||||||
actions: widget.actions,
|
|
||||||
children: widget.children,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void expand() {
|
|
||||||
widget.onExpand();
|
|
||||||
_controller.forward();
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
|
||||||
Future.delayed(_controller.duration!).then((_) {
|
|
||||||
Scrollable.ensureVisible(
|
|
||||||
context,
|
|
||||||
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
|
||||||
duration: _controller.duration!,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void collapse() {
|
|
||||||
_controller.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedNameLeading extends AnimatedWidget {
|
|
||||||
const _AnimatedNameLeading({required AnimationController controller}) : super(listenable: controller);
|
|
||||||
|
|
||||||
Animation<double> get _progress => listenable as Animation<double>;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(right: _progress.value * Dimens.grid8),
|
|
||||||
child: Icon(
|
|
||||||
Icons.edit_outlined,
|
|
||||||
size: _progress.value * Dimens.grid24,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedArrowButton extends AnimatedWidget {
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
|
|
||||||
const _AnimatedArrowButton({
|
|
||||||
required AnimationController controller,
|
|
||||||
required this.onPressed,
|
|
||||||
}) : super(listenable: controller);
|
|
||||||
|
|
||||||
Animation<double> get _progress => listenable as Animation<double>;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: Transform.rotate(
|
|
||||||
angle: _progress.value * pi,
|
|
||||||
child: const Icon(Icons.keyboard_arrow_down_outlined),
|
|
||||||
),
|
|
||||||
tooltip: _progress.value == 0 ? S.of(context).tooltipExpand : S.of(context).tooltipCollapse,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedContent extends AnimatedWidget {
|
|
||||||
final List<IconButton> actions;
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
const _AnimatedContent({
|
|
||||||
required AnimationController controller,
|
|
||||||
required this.actions,
|
|
||||||
required this.children,
|
|
||||||
}) : super(listenable: controller);
|
|
||||||
|
|
||||||
Animation<double> get _progress => listenable as Animation<double>;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedOverflowBox(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
size: Size(
|
|
||||||
double.maxFinite,
|
|
||||||
_progress.value * Dimens.grid56 * (children.length + 1),
|
|
||||||
),
|
|
||||||
// https://github.com/gskinnerTeam/flutter-folio/pull/62
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _progress.value,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
...children,
|
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: actions,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/shared/expandable_section_list/components/expandable_section_list_item/widget_expandable_section_list_item.dart';
|
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
typedef _WidgetBuilder<W, T extends Identifiable> = W Function(BuildContext context, T value);
|
|
||||||
|
|
||||||
class ExpandableSectionList<T extends Identifiable> extends StatefulWidget {
|
|
||||||
final List<T> values;
|
|
||||||
final VoidCallback onSectionTitleTap;
|
|
||||||
final _WidgetBuilder<List<Widget>, T> contentBuilder;
|
|
||||||
final _WidgetBuilder<List<IconButton>, T> actionsBuilder;
|
|
||||||
|
|
||||||
const ExpandableSectionList({
|
|
||||||
required this.values,
|
|
||||||
required this.onSectionTitleTap,
|
|
||||||
required this.contentBuilder,
|
|
||||||
required this.actionsBuilder,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpandableSectionList> createState() => _ExpandableSectionListState<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ExpandableSectionListState<T extends Identifiable> extends State<ExpandableSectionList<T>> {
|
|
||||||
final Map<String, GlobalKey<ExpandableSectionListItemState>> keysMap = {};
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_updateProfilesKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
final item = widget.values[index];
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Dimens.paddingM,
|
|
||||||
index == 0 ? Dimens.paddingM : 0,
|
|
||||||
Dimens.paddingM,
|
|
||||||
Dimens.paddingM,
|
|
||||||
),
|
|
||||||
child: ExpandableSectionListItem(
|
|
||||||
key: keysMap[item.id],
|
|
||||||
title: item.name,
|
|
||||||
onTitleTap: widget.onSectionTitleTap,
|
|
||||||
onExpand: () => _keepExpandedAt(index),
|
|
||||||
actions: widget.actionsBuilder(context, item),
|
|
||||||
children: widget.contentBuilder(context, item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: widget.values.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _keepExpandedAt(int index) {
|
|
||||||
keysMap.values.toList().getRange(0, index).forEach((element) {
|
|
||||||
element.currentState?.collapse();
|
|
||||||
});
|
|
||||||
keysMap.values.toList().getRange(index + 1, keysMap.length).forEach((element) {
|
|
||||||
element.currentState?.collapse();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateProfilesKeys() {
|
|
||||||
if (widget.values.length > keysMap.length) {
|
|
||||||
// item added
|
|
||||||
final List<String> idsToAdd = [];
|
|
||||||
for (final item in widget.values) {
|
|
||||||
if (!keysMap.keys.contains(item.id)) idsToAdd.add(item.id);
|
|
||||||
}
|
|
||||||
for (final id in idsToAdd) {
|
|
||||||
keysMap[id] = GlobalKey<ExpandableSectionListItemState>(debugLabel: id);
|
|
||||||
}
|
|
||||||
idsToAdd.clear();
|
|
||||||
} else if (widget.values.length < keysMap.length) {
|
|
||||||
// item deleted
|
|
||||||
final List<String> idsToDelete = [];
|
|
||||||
for (final id in keysMap.keys) {
|
|
||||||
if (!widget.values.any((p) => p.id == id)) idsToDelete.add(id);
|
|
||||||
}
|
|
||||||
idsToDelete.forEach(keysMap.remove);
|
|
||||||
idsToDelete.clear();
|
|
||||||
} else {
|
|
||||||
// item updated, no need to updated keys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue