Files
superport/lib/screens/equipment/widgets/custom_dropdown_field.dart

178 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
/// 드롭다운 기능이 있는 재사용 가능한 TextFormField 위젯
class CustomDropdownField extends StatefulWidget {
final String label;
final String hint;
final bool required;
final TextEditingController controller;
final FocusNode focusNode;
final List<String> items;
final Function(String) onChanged;
final Function(String)? onFieldSubmitted;
final String? Function(String)? getAutocompleteSuggestion;
final VoidCallback onDropdownPressed;
final LayerLink layerLink;
final GlobalKey fieldKey;
const CustomDropdownField({
super.key,
required this.label,
required this.hint,
required this.required,
required this.controller,
required this.focusNode,
required this.items,
required this.onChanged,
this.onFieldSubmitted,
this.getAutocompleteSuggestion,
required this.onDropdownPressed,
required this.layerLink,
required this.fieldKey,
});
@override
State<CustomDropdownField> createState() => _CustomDropdownFieldState();
}
class _CustomDropdownFieldState extends State<CustomDropdownField> {
bool _isProgrammaticChange = false;
OverlayEntry? _overlayEntry;
@override
void dispose() {
_removeDropdown();
super.dispose();
}
void _showDropdown() {
_removeDropdown();
final RenderBox renderBox = widget.fieldKey.currentContext!.findRenderObject() as RenderBox;
final size = renderBox.size;
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: widget.layerLink,
showWhenUnlinked: false,
offset: const Offset(0, 45),
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(4),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: widget.items.map((item) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
_isProgrammaticChange = true;
widget.controller.text = item;
});
widget.onChanged(item);
WidgetsBinding.instance.addPostFrameCallback((_) {
_isProgrammaticChange = false;
});
_removeDropdown();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
width: double.infinity,
child: Text(item),
),
);
}).toList(),
),
),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
void _removeDropdown() {
if (_overlayEntry != null) {
_overlayEntry!.remove();
_overlayEntry = null;
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CompositedTransformTarget(
link: widget.layerLink,
child: TextFormField(
key: widget.fieldKey,
controller: widget.controller,
focusNode: widget.focusNode,
decoration: InputDecoration(
labelText: widget.label,
hintText: widget.hint,
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {
widget.onDropdownPressed();
_showDropdown();
},
),
),
onChanged: (value) {
if (!_isProgrammaticChange) {
widget.onChanged(value);
}
},
onFieldSubmitted: widget.onFieldSubmitted,
),
),
// 자동완성 후보 표시
if (widget.getAutocompleteSuggestion != null)
Builder(
builder: (context) {
final suggestion = widget.getAutocompleteSuggestion!(widget.controller.text);
if (suggestion != null && suggestion.length > widget.controller.text.length) {
return Padding(
padding: const EdgeInsets.only(left: 12, top: 2),
child: Text(
suggestion,
style: const TextStyle(
color: Color(0xFF1976D2),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
);
}
return const SizedBox.shrink();
},
),
],
);
}
}