Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import 'dart:async';
- import 'package:flutter/material.dart';
- typedef ItemFetcher<T> = FutureOr<List<T>> Function();
- typedef ItemLabel<T> = String Function(T item);
- typedef ItemWidgetBuilder<T> = Widget Function(BuildContext context, T item);
- typedef ItemSelected<T> = void Function(T item);
- typedef DropdownBuilder<T> = Widget Function(
- BuildContext context,
- List<T> items,
- void Function(T) selectItem,
- bool loading,
- String? error,
- );
- class CustomDropdown<T> extends StatefulWidget {
- final ItemFetcher<T> items;
- final ItemLabel<T> itemLabel;
- final ItemWidgetBuilder<T>? itemBuilder;
- final ItemSelected<T>? onSelected;
- final InputDecoration? decoration;
- final DropdownBuilder<T>? dropdownBuilder;
- const CustomDropdown({
- super.key,
- required this.items,
- required this.itemLabel,
- this.itemBuilder,
- this.dropdownBuilder,
- this.onSelected,
- this.decoration,
- }) : assert(
- dropdownBuilder != null || itemBuilder != null,
- 'You must provide itemBuilder if dropdownBuilder is null',
- );
- @override
- State<CustomDropdown<T>> createState() => _CustomDropdownState<T>();
- }
- class _CustomDropdownState<T> extends State<CustomDropdown<T>> with WidgetsBindingObserver {
- final TextEditingController _controller = TextEditingController();
- final FocusNode _focusNode = FocusNode();
- final GlobalKey _fieldKey = GlobalKey();
- OverlayEntry? _overlayEntry;
- List<T> _allItems = [];
- List<T> _filteredItems = [];
- bool _loading = true;
- String? _error;
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- _loadAllItems();
- _focusNode.addListener(() {
- if (!_focusNode.hasFocus) {
- _hideOverlay();
- }
- });
- }
- Future<void> _loadAllItems() async {
- setState(() {
- _loading = true;
- _error = null;
- });
- try {
- final results = await widget.items();
- if (!mounted) return;
- setState(() {
- _allItems = results;
- _filteredItems = List.from(_allItems);
- _loading = false;
- });
- } catch (e) {
- if (!mounted) return;
- setState(() {
- _error = 'Failed to load items';
- _loading = false;
- });
- }
- }
- @override
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- _hideOverlay();
- _controller.dispose();
- _focusNode.dispose();
- super.dispose();
- }
- @override
- void didChangeMetrics() {
- if (_overlayEntry != null) {
- _overlayEntry!.markNeedsBuild();
- }
- }
- void _showOverlay() {
- if (_overlayEntry == null) {
- _overlayEntry = _buildOverlayEntry();
- Overlay.of(context).insert(_overlayEntry!);
- } else {
- _overlayEntry!.markNeedsBuild();
- }
- }
- void _hideOverlay() {
- _overlayEntry?.remove();
- _overlayEntry = null;
- _filteredItems = List.from(_allItems);
- }
- void _filterItems(String query) {
- setState(() {
- _filteredItems =
- _allItems.where((item) => widget.itemLabel(item).toLowerCase().contains(query.toLowerCase())).toList();
- });
- _overlayEntry?.markNeedsBuild();
- }
- void _selectItem(T item) {
- final label = widget.itemLabel(item);
- _controller.text = label;
- _controller.selection = TextSelection.collapsed(offset: label.length);
- widget.onSelected?.call(item);
- _hideOverlay();
- }
- OverlayEntry _buildOverlayEntry() {
- return OverlayEntry(
- builder: (overlayContext) {
- if (_fieldKey.currentContext == null) return const SizedBox.shrink();
- final RenderBox fieldBox = _fieldKey.currentContext!.findRenderObject() as RenderBox;
- final Size fieldSize = fieldBox.size;
- final Offset fieldOffset = fieldBox.localToGlobal(Offset.zero);
- final MediaQueryData mq = MediaQuery.of(overlayContext);
- final double screenHeight = mq.size.height;
- final double keyboardHeight = mq.viewInsets.bottom;
- final double topPadding = mq.padding.top;
- final double bottomPadding = mq.padding.bottom;
- const double minOverlayHeight = 80; // minimum height for usability
- const double maxOverlayHeightCap = 400; // max height limit, can be parameterized
- double availableHeight;
- if (keyboardHeight > 0) {
- // Keyboard visible: available height is from top padding to keyboard top minus some margin
- final double bottomLimit = screenHeight - keyboardHeight - bottomPadding;
- availableHeight = bottomLimit - topPadding - 8; // 8 px margin for safety
- } else {
- // Keyboard hidden: available height from below text field to bottom safe area minus margin
- availableHeight = screenHeight - (fieldOffset.dy + fieldSize.height) - bottomPadding - 8;
- }
- // Clamp height between min and max cap
- final double maxHeight = availableHeight.clamp(minOverlayHeight, maxOverlayHeightCap);
- double top;
- if (keyboardHeight > 0) {
- // Place overlay just above keyboard
- final double bottomLimit = screenHeight - keyboardHeight - bottomPadding;
- top = bottomLimit - maxHeight;
- } else {
- // Place overlay just below TextField
- top = fieldOffset.dy + fieldSize.height;
- }
- return _buildPositionedOverlay(fieldOffset.dx, top, fieldSize.width, maxHeight, overlayContext);
- },
- );
- }
- Widget _buildPositionedOverlay(double left, double top, double width, double height, BuildContext overlayContext) {
- Widget dropdownContent;
- if (widget.dropdownBuilder != null) {
- dropdownContent = widget.dropdownBuilder!(
- overlayContext,
- _filteredItems,
- _selectItem,
- _loading,
- _error,
- );
- } else {
- if (_loading) {
- dropdownContent = const SizedBox(
- height: 56,
- child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
- );
- } else if (_error != null) {
- dropdownContent = ListTile(
- leading: const Icon(Icons.error_outline),
- title: Text(_error!),
- );
- } else if (_filteredItems.isEmpty) {
- dropdownContent = const ListTile(title: Text('No results'));
- } else {
- dropdownContent = ListView.separated(
- padding: EdgeInsets.zero,
- shrinkWrap: true,
- itemCount: _filteredItems.length,
- separatorBuilder: (_, __) => const Divider(height: 1),
- itemBuilder: (context, index) {
- final item = _filteredItems[index];
- return InkWell(
- onTap: () => _selectItem(item),
- child: widget.itemBuilder!(context, item),
- );
- },
- );
- }
- }
- return Positioned.fill(
- child: GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- _hideOverlay();
- },
- child: Stack(
- children: [
- Positioned(
- left: left,
- top: top,
- width: width,
- height: height,
- child: Material(
- elevation: 6,
- borderRadius: BorderRadius.circular(8),
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: TextField(
- autofocus: true,
- decoration: const InputDecoration(
- hintText: 'Search...',
- prefixIcon: Icon(Icons.search),
- border: OutlineInputBorder(),
- ),
- onChanged: _filterItems,
- ),
- ),
- Expanded(child: dropdownContent),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
- @override
- Widget build(BuildContext context) {
- return SizedBox(
- child: TextField(
- key: _fieldKey,
- controller: _controller,
- readOnly: true,
- decoration: widget.decoration ??
- const InputDecoration(
- border: OutlineInputBorder(),
- hintText: 'Select...',
- suffixIcon: Icon(Icons.keyboard_arrow_down),
- ),
- onTap: () {
- if (!_loading && _error == null && _allItems.isNotEmpty) {
- _showOverlay();
- }
- },
- ),
- );
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment