medical file changes
parent
0f17a655ed
commit
19365375f6
@ -0,0 +1,301 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||||
|
|
||||||
|
// ==================== MAIN CUSTOM WIDGET ====================
|
||||||
|
|
||||||
|
class CustomExpandableList extends StatefulWidget {
|
||||||
|
final List<ExpandableListItem> items;
|
||||||
|
final ExpansionMode expansionMode;
|
||||||
|
final EdgeInsetsGeometry? itemPadding;
|
||||||
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
final Color? dividerColor;
|
||||||
|
final double dividerHeight;
|
||||||
|
final bool showDividers;
|
||||||
|
final Duration animationDuration;
|
||||||
|
final Curve animationCurve;
|
||||||
|
final ExpandableListTheme? theme;
|
||||||
|
final Function(int index, bool isExpanded)? onItemToggled;
|
||||||
|
|
||||||
|
const CustomExpandableList({
|
||||||
|
Key? key,
|
||||||
|
required this.items,
|
||||||
|
this.expansionMode = ExpansionMode.multiple,
|
||||||
|
this.itemPadding,
|
||||||
|
this.contentPadding,
|
||||||
|
this.dividerColor,
|
||||||
|
this.dividerHeight = 1.0,
|
||||||
|
this.showDividers = true,
|
||||||
|
this.animationDuration = const Duration(milliseconds: 300),
|
||||||
|
this.animationCurve = Curves.easeInOut,
|
||||||
|
this.theme,
|
||||||
|
this.onItemToggled,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CustomExpandableListState createState() => _CustomExpandableListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomExpandableListState extends State<CustomExpandableList> {
|
||||||
|
late List<bool> _expandedStates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_expandedStates = List<bool>.generate(
|
||||||
|
widget.items.length,
|
||||||
|
(index) => widget.items[index].initiallyExpanded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleItem(int index) {
|
||||||
|
setState(() {
|
||||||
|
if (widget.expansionMode == ExpansionMode.exactlyOne) {
|
||||||
|
// Close all others, open this one
|
||||||
|
for (int i = 0; i < _expandedStates.length; i++) {
|
||||||
|
_expandedStates[i] = (i == index) ? !_expandedStates[i] : false;
|
||||||
|
}
|
||||||
|
} else if (widget.expansionMode == ExpansionMode.atMostOne) {
|
||||||
|
// Toggle this one, close if opening another
|
||||||
|
bool wasExpanded = _expandedStates[index];
|
||||||
|
for (int i = 0; i < _expandedStates.length; i++) {
|
||||||
|
if (i == index) {
|
||||||
|
_expandedStates[i] = !wasExpanded;
|
||||||
|
} else if (!wasExpanded) {
|
||||||
|
_expandedStates[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Multiple - just toggle the clicked item
|
||||||
|
_expandedStates[index] = !_expandedStates[index];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.onItemToggled?.call(index, _expandedStates[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = widget.theme ?? ExpandableListTheme.defaultTheme(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: List.generate(widget.items.length, (index) {
|
||||||
|
final item = widget.items[index];
|
||||||
|
final isExpanded = _expandedStates[index];
|
||||||
|
final isLast = index == widget.items.length - 1;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_ExpandableListItemWidget(
|
||||||
|
title: item.title,
|
||||||
|
isExpanded: isExpanded,
|
||||||
|
onTap: () => _toggleItem(index),
|
||||||
|
padding: widget.itemPadding,
|
||||||
|
contentPadding: widget.contentPadding,
|
||||||
|
theme: theme,
|
||||||
|
leading: item.leading,
|
||||||
|
trailing: item.trailing,
|
||||||
|
backgroundColor: item.backgroundColor,
|
||||||
|
expandedBackgroundColor: item.expandedBackgroundColor,
|
||||||
|
animationDuration: widget.animationDuration,
|
||||||
|
animationCurve: widget.animationCurve,
|
||||||
|
children: item.children,
|
||||||
|
),
|
||||||
|
if (widget.showDividers && !isLast)
|
||||||
|
Divider(
|
||||||
|
height: widget.dividerHeight,
|
||||||
|
color: widget.dividerColor ?? theme.dividerColor,
|
||||||
|
).paddingSymmetrical(12.h,0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SUPPORTING WIDGETS ====================
|
||||||
|
|
||||||
|
class _ExpandableListItemWidget extends StatelessWidget {
|
||||||
|
final Widget title;
|
||||||
|
final List<Widget> children;
|
||||||
|
final bool isExpanded;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
final ExpandableListTheme theme;
|
||||||
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Color? expandedBackgroundColor;
|
||||||
|
final Duration animationDuration;
|
||||||
|
final Curve animationCurve;
|
||||||
|
|
||||||
|
const _ExpandableListItemWidget({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.children,
|
||||||
|
required this.isExpanded,
|
||||||
|
required this.onTap,
|
||||||
|
this.padding,
|
||||||
|
this.contentPadding,
|
||||||
|
required this.theme,
|
||||||
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.expandedBackgroundColor,
|
||||||
|
required this.animationDuration,
|
||||||
|
required this.animationCurve,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: isExpanded
|
||||||
|
? (expandedBackgroundColor ?? theme.expandedBackgroundColor)
|
||||||
|
: (backgroundColor ?? theme.backgroundColor),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: padding ?? theme.itemPadding,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (leading != null) ...[
|
||||||
|
leading!,
|
||||||
|
SizedBox(width: theme.leadingSpacing),
|
||||||
|
],
|
||||||
|
Expanded(child: title),
|
||||||
|
if (trailing != null)
|
||||||
|
trailing!
|
||||||
|
else
|
||||||
|
AnimatedRotation(
|
||||||
|
turns: isExpanded ? 0.5 : 0.0,
|
||||||
|
duration: animationDuration,
|
||||||
|
curve: animationCurve,
|
||||||
|
child: theme.defaultTrailingIcon,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
AnimatedSize(
|
||||||
|
duration: animationDuration,
|
||||||
|
curve: animationCurve,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: isExpanded ? 0.0 : 0.0,
|
||||||
|
maxHeight: isExpanded ? double.infinity : 0.0,
|
||||||
|
),
|
||||||
|
child: isExpanded
|
||||||
|
? Padding(
|
||||||
|
padding: contentPadding ?? theme.contentPadding,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DATA MODELS ====================
|
||||||
|
|
||||||
|
class ExpandableListItem {
|
||||||
|
final Widget title;
|
||||||
|
final List<Widget> children;
|
||||||
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
|
final bool initiallyExpanded;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Color? expandedBackgroundColor;
|
||||||
|
|
||||||
|
const ExpandableListItem({
|
||||||
|
required this.title,
|
||||||
|
required this.children,
|
||||||
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
|
this.initiallyExpanded = false,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.expandedBackgroundColor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== THEME ====================
|
||||||
|
|
||||||
|
class ExpandableListTheme {
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color expandedBackgroundColor;
|
||||||
|
final Color dividerColor;
|
||||||
|
final Widget defaultTrailingIcon;
|
||||||
|
final EdgeInsetsGeometry itemPadding;
|
||||||
|
final EdgeInsetsGeometry contentPadding;
|
||||||
|
final double leadingSpacing;
|
||||||
|
|
||||||
|
const ExpandableListTheme({
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.expandedBackgroundColor,
|
||||||
|
required this.dividerColor,
|
||||||
|
required this.defaultTrailingIcon,
|
||||||
|
required this.itemPadding,
|
||||||
|
required this.contentPadding,
|
||||||
|
this.leadingSpacing = 12.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ExpandableListTheme.defaultTheme(BuildContext context) {
|
||||||
|
final textColor = Theme.of(context).textTheme.bodyLarge?.color ?? Colors.black;
|
||||||
|
|
||||||
|
return ExpandableListTheme(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
expandedBackgroundColor: Colors.grey.shade50,
|
||||||
|
dividerColor: Colors.grey.shade300,
|
||||||
|
defaultTrailingIcon: Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: textColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
itemPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ExpandableListTheme.custom({
|
||||||
|
Color? backgroundColor,
|
||||||
|
Color? expandedBackgroundColor,
|
||||||
|
Color? dividerColor,
|
||||||
|
Widget? defaultTrailingIcon,
|
||||||
|
EdgeInsetsGeometry? itemPadding,
|
||||||
|
EdgeInsetsGeometry? contentPadding,
|
||||||
|
double? leadingSpacing,
|
||||||
|
}) {
|
||||||
|
return ExpandableListTheme(
|
||||||
|
backgroundColor: backgroundColor ?? Colors.transparent,
|
||||||
|
expandedBackgroundColor: expandedBackgroundColor ?? Colors.grey.shade50,
|
||||||
|
dividerColor: dividerColor ?? Colors.grey.shade300,
|
||||||
|
defaultTrailingIcon: defaultTrailingIcon ??
|
||||||
|
Icon(Icons.keyboard_arrow_down, color: Colors.black, size: 24),
|
||||||
|
itemPadding: itemPadding ?? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
contentPadding: contentPadding ?? const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
|
||||||
|
leadingSpacing: leadingSpacing ?? 12.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ENUMS ====================
|
||||||
|
|
||||||
|
enum ExpansionMode {
|
||||||
|
multiple, // Multiple items can be expanded
|
||||||
|
exactlyOne, // Exactly one item expanded at a time
|
||||||
|
atMostOne, // Zero or one item expanded at a time
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue