198 lines
5.2 KiB
Dart
198 lines
5.2 KiB
Dart
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class AutoCompleteDropdownButton<T> extends StatefulWidget {
|
|
final String label;
|
|
final T? selectedItem;
|
|
final List<T> items;
|
|
final void Function(T? value) onChanged;
|
|
final List<T> Function(String? value)? applyQuery;
|
|
|
|
const AutoCompleteDropdownButton(
|
|
{Key? key,
|
|
this.selectedItem,
|
|
required this.label,
|
|
required this.items,
|
|
required this.onChanged,
|
|
this.applyQuery})
|
|
: super(key: key);
|
|
|
|
@override
|
|
_AutoCompleteDropdownButtonState<T> createState() =>
|
|
_AutoCompleteDropdownButtonState();
|
|
}
|
|
|
|
class _AutoCompleteDropdownButtonState<T>
|
|
extends State<AutoCompleteDropdownButton<T>> {
|
|
TextEditingController controller = TextEditingController(text: '');
|
|
late List<T> options;
|
|
late List<T> suggestions;
|
|
|
|
final LayerLink layerLink = LayerLink();
|
|
OverlayEntry? entry;
|
|
bool isOpen = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
setState(() {
|
|
controller.text = widget.selectedItem == null
|
|
? ''
|
|
: widget.selectedItem!.toString();
|
|
options = widget.items;
|
|
suggestions = [];
|
|
});
|
|
}
|
|
|
|
void toggleOverlay() {
|
|
isOpen ? hideOverlay() : showOverlay();
|
|
}
|
|
|
|
void showOverlay() {
|
|
hideOverlay();
|
|
|
|
List<Widget> items = [];
|
|
Divider? divider;
|
|
|
|
final overlay = Overlay.of(context)!;
|
|
final renderBox = context.findRenderObject() as RenderBox;
|
|
|
|
if (widget.selectedItem != null) {
|
|
items.add(buildListTile(widget.selectedItem!));
|
|
}
|
|
|
|
if (suggestions.isNotEmpty) {
|
|
items.addAll(suggestions
|
|
.where((item) => item != widget.selectedItem)
|
|
.map((item) => buildListTile(item)));
|
|
}
|
|
|
|
if ((widget.selectedItem != null || suggestions.isNotEmpty) &&
|
|
(options.length -
|
|
suggestions.length -
|
|
(widget.selectedItem == null ? 0 : 1) >
|
|
0)) {
|
|
divider = const Divider(height: 10);
|
|
items.add(divider);
|
|
}
|
|
|
|
items.addAll(options
|
|
.where((item) =>
|
|
!(widget.selectedItem != null &&
|
|
item.toString() ==
|
|
widget.selectedItem!.toString()) &&
|
|
!suggestions.contains(item))
|
|
.map((item) => buildListTile(item))
|
|
.toList());
|
|
|
|
final screenHeight = MediaQuery.of(context).size.height;
|
|
final neededHeight =
|
|
renderBox.size.height * (items.length - 1) + (divider?.height ??
|
|
renderBox.size.height);
|
|
final availableHeight = screenHeight -
|
|
(renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height);
|
|
bool displayAbove = neededHeight > availableHeight &&
|
|
screenHeight - availableHeight > availableHeight;
|
|
final height = min(neededHeight,
|
|
max(availableHeight, screenHeight - availableHeight - 55) - 55);
|
|
|
|
entry = OverlayEntry(
|
|
builder: (context) => Positioned(
|
|
width: renderBox.size.width,
|
|
height: height,
|
|
child: CompositedTransformFollower(
|
|
link: layerLink,
|
|
targetAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
|
|
followerAnchor:
|
|
displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
|
|
offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)),
|
|
showWhenUnlinked: false,
|
|
child: Material(
|
|
elevation: 8,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: items,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
overlay.insert(entry!);
|
|
isOpen = true;
|
|
}
|
|
|
|
ListTile buildListTile(T item) {
|
|
return ListTile(
|
|
onTap: () => handleChanged(item),
|
|
selected: item == widget.selectedItem,
|
|
title: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(item.toString()),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void hideOverlay() {
|
|
entry?.remove();
|
|
entry = null;
|
|
isOpen = false;
|
|
}
|
|
|
|
void handleChanged(item) {
|
|
widget.onChanged(item);
|
|
controller.text = item.toString();
|
|
hideOverlay();
|
|
}
|
|
|
|
void onChangeQuery(String value) {
|
|
if (value.trim() == '' ||
|
|
(widget.selectedItem != null &&
|
|
value == widget.selectedItem!.toString())) {
|
|
setState(() {
|
|
suggestions = [];
|
|
});
|
|
} else {
|
|
if (widget.applyQuery == null) {
|
|
setState(() {
|
|
suggestions = widget.items.where((item) {
|
|
String itemString = item.toString().toLowerCase();
|
|
String valueLowercase = value.toLowerCase();
|
|
return itemString.contains(valueLowercase);
|
|
}).toList();
|
|
});
|
|
} else {
|
|
setState(() {
|
|
suggestions = widget.applyQuery!(value)
|
|
.where((item) => item != widget.selectedItem)
|
|
.toList();
|
|
});
|
|
}
|
|
showOverlay();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CompositedTransformTarget(
|
|
link: layerLink,
|
|
child: TextFormField(
|
|
onChanged: onChangeQuery,
|
|
onTap: toggleOverlay,
|
|
controller: controller,
|
|
decoration: InputDecoration(
|
|
labelText: widget.label,
|
|
suffixIcon: IconButton(
|
|
onPressed: toggleOverlay,
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|