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