Files
superport/lib/screens/equipment/widgets/autocomplete_text_field.dart
2025-07-02 17:45:44 +09:00

173 lines
4.9 KiB
Dart

import 'package:flutter/material.dart';
/// 자동완성 텍스트 필드 위젯
///
/// 입력, 드롭다운, 포커스, 필터링, 선택 기능을 모두 포함한다.
class AutocompleteTextField extends StatefulWidget {
final String label;
final String value;
final List<String> items;
final bool isRequired;
final String hintText;
final void Function(String) onChanged;
final void Function(String) onSelected;
final FocusNode? focusNode;
const AutocompleteTextField({
Key? key,
required this.label,
required this.value,
required this.items,
required this.onChanged,
required this.onSelected,
this.isRequired = false,
this.hintText = '',
this.focusNode,
}) : super(key: key);
@override
State<AutocompleteTextField> createState() => _AutocompleteTextFieldState();
}
class _AutocompleteTextFieldState extends State<AutocompleteTextField> {
late final TextEditingController _controller;
late final FocusNode _focusNode;
late List<String> _filteredItems;
bool _showDropdown = false;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
_focusNode = widget.focusNode ?? FocusNode();
_filteredItems = List.from(widget.items);
_controller.addListener(_onTextChanged);
_focusNode.addListener(() {
setState(() {
if (_focusNode.hasFocus) {
_showDropdown = _filteredItems.isNotEmpty;
} else {
_showDropdown = false;
}
});
});
}
@override
void didUpdateWidget(covariant AutocompleteTextField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
if (widget.items != oldWidget.items) {
_filteredItems = List.from(widget.items);
}
}
@override
void dispose() {
if (widget.focusNode == null) {
_focusNode.dispose();
}
_controller.dispose();
super.dispose();
}
// 입력값 변경 시 필터링
void _onTextChanged() {
final text = _controller.text;
setState(() {
if (text.isEmpty) {
_filteredItems = List.from(widget.items);
} else {
_filteredItems =
widget.items
.where(
(item) => item.toLowerCase().contains(text.toLowerCase()),
)
.toList();
// 시작 부분이 일치하는 항목 우선 정렬
_filteredItems.sort((a, b) {
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return a.compareTo(b);
});
}
_showDropdown = _filteredItems.isNotEmpty && _focusNode.hasFocus;
widget.onChanged(text);
});
}
void _handleSelect(String value) {
setState(() {
_controller.text = value;
_showDropdown = false;
});
widget.onSelected(value);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
TextFormField(
controller: _controller,
focusNode: _focusNode,
decoration: InputDecoration(
labelText: widget.label,
hintText: widget.hintText,
),
validator: (value) {
if (widget.isRequired && (value == null || value.isEmpty)) {
return '${widget.label}을(를) 입력해주세요';
}
return null;
},
onSaved: (value) {
widget.onSelected(value ?? '');
},
),
if (_showDropdown)
Positioned(
top: 50,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
constraints: const BoxConstraints(maxHeight: 200),
child: ListView.builder(
shrinkWrap: true,
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () => _handleSelect(_filteredItems[index]),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(_filteredItems[index]),
),
);
},
),
),
),
],
);
}
}