231 lines
6.1 KiB
Dart
231 lines
6.1 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;
|
|
final TextEditingController controller;
|
|
|
|
const AutoCompleteDropdownButton(
|
|
{Key? key,
|
|
this.selectedItem,
|
|
required this.label,
|
|
required this.items,
|
|
required this.onChanged,
|
|
this.applyQuery,
|
|
required this.controller})
|
|
: super(key: key);
|
|
|
|
@override
|
|
_AutoCompleteDropdownButtonState<T> createState() =>
|
|
_AutoCompleteDropdownButtonState();
|
|
}
|
|
|
|
class _AutoCompleteDropdownButtonState<T>
|
|
extends State<AutoCompleteDropdownButton<T>> {
|
|
late List<T> options;
|
|
late List<T> suggestions;
|
|
|
|
final FocusNode focusNode = FocusNode();
|
|
final LayerLink layerLink = LayerLink();
|
|
OverlayEntry? entry;
|
|
bool isOpen = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
setState(() {
|
|
options = widget.items;
|
|
suggestions = [];
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void toggleOverlay() {
|
|
isOpen ? hideOverlay() : showOverlay();
|
|
}
|
|
|
|
void showOverlay() {
|
|
hideOverlay();
|
|
|
|
List<Widget> items = [];
|
|
Divider? divider;
|
|
|
|
ScrollController _scrollController = ScrollController();
|
|
|
|
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: Scrollbar(
|
|
controller: _scrollController,
|
|
// thumbVisibility: true,
|
|
isAlwaysShown: true,
|
|
child: Material(
|
|
elevation: 8,
|
|
child: SingleChildScrollView(
|
|
controller: _scrollController,
|
|
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: [
|
|
Flexible(
|
|
child: Text(item.toString()),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void hideOverlay() {
|
|
entry?.remove();
|
|
setState(() {
|
|
entry = null;
|
|
isOpen = false;
|
|
});
|
|
}
|
|
|
|
void handleChanged(T? item) {
|
|
widget.onChanged(item);
|
|
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 Focus(
|
|
focusNode: focusNode,
|
|
onFocusChange: (isFocused) {
|
|
if (!isFocused) {
|
|
hideOverlay();
|
|
}
|
|
},
|
|
child: CompositedTransformTarget(
|
|
link: layerLink,
|
|
child: TextFormField(
|
|
onChanged: onChangeQuery,
|
|
onTap: toggleOverlay,
|
|
controller: widget.controller,
|
|
decoration: InputDecoration(
|
|
labelText: widget.label,
|
|
suffixIcon: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
widget.selectedItem != null
|
|
? IconButton(
|
|
onPressed: () => handleChanged(null),
|
|
icon: const Icon(Icons.close),
|
|
iconSize: 20.0,
|
|
)
|
|
: Container(),
|
|
IconButton(
|
|
onPressed: toggleOverlay,
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|