chayanforyou

dropdown_overlay_example_2

Aug 11th, 2025
525
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Dart 8.56 KB | None | 0 0
  1. import 'dart:async';
  2.  
  3. import 'package:flutter/material.dart';
  4.  
  5. typedef ItemFetcher<T> = FutureOr<List<T>> Function();
  6. typedef ItemLabel<T> = String Function(T item);
  7. typedef ItemWidgetBuilder<T> = Widget Function(BuildContext context, T item);
  8. typedef ItemSelected<T> = void Function(T item);
  9. typedef DropdownBuilder<T> = Widget Function(
  10.     BuildContext context,
  11.     List<T> items,
  12.     void Function(T) selectItem,
  13.     bool loading,
  14.     String? error,
  15.     );
  16.  
  17. class CustomDropdown<T> extends StatefulWidget {
  18.   final ItemFetcher<T> items;
  19.   final ItemLabel<T> itemLabel;
  20.   final ItemWidgetBuilder<T>? itemBuilder;
  21.   final ItemSelected<T>? onSelected;
  22.   final InputDecoration? decoration;
  23.   final DropdownBuilder<T>? dropdownBuilder;
  24.  
  25.   const CustomDropdown({
  26.     super.key,
  27.     required this.items,
  28.     required this.itemLabel,
  29.     this.itemBuilder,
  30.     this.dropdownBuilder,
  31.     this.onSelected,
  32.     this.decoration,
  33.   }) : assert(
  34.   dropdownBuilder != null || itemBuilder != null,
  35.   'You must provide itemBuilder if dropdownBuilder is null',
  36.   );
  37.  
  38.   @override
  39.   State<CustomDropdown<T>> createState() => _CustomDropdownState<T>();
  40. }
  41.  
  42. class _CustomDropdownState<T> extends State<CustomDropdown<T>> with WidgetsBindingObserver {
  43.   final TextEditingController _controller = TextEditingController();
  44.   final FocusNode _focusNode = FocusNode();
  45.   final GlobalKey _fieldKey = GlobalKey();
  46.  
  47.   OverlayEntry? _overlayEntry;
  48.   List<T> _allItems = [];
  49.   List<T> _filteredItems = [];
  50.   bool _loading = true;
  51.   String? _error;
  52.  
  53.   @override
  54.   void initState() {
  55.     super.initState();
  56.     WidgetsBinding.instance.addObserver(this);
  57.  
  58.     _loadAllItems();
  59.  
  60.     _focusNode.addListener(() {
  61.       if (!_focusNode.hasFocus) {
  62.         _hideOverlay();
  63.       }
  64.     });
  65.   }
  66.  
  67.   Future<void> _loadAllItems() async {
  68.     setState(() {
  69.       _loading = true;
  70.       _error = null;
  71.     });
  72.     try {
  73.       final results = await widget.items();
  74.       if (!mounted) return;
  75.       setState(() {
  76.         _allItems = results;
  77.         _filteredItems = List.from(_allItems);
  78.         _loading = false;
  79.       });
  80.     } catch (e) {
  81.       if (!mounted) return;
  82.       setState(() {
  83.         _error = 'Failed to load items';
  84.         _loading = false;
  85.       });
  86.     }
  87.   }
  88.  
  89.   @override
  90.   void dispose() {
  91.     WidgetsBinding.instance.removeObserver(this);
  92.     _hideOverlay();
  93.     _controller.dispose();
  94.     _focusNode.dispose();
  95.     super.dispose();
  96.   }
  97.  
  98.   @override
  99.   void didChangeMetrics() {
  100.     if (_overlayEntry != null) {
  101.       _overlayEntry!.markNeedsBuild();
  102.     }
  103.   }
  104.  
  105.   void _showOverlay() {
  106.     if (_overlayEntry == null) {
  107.       _overlayEntry = _buildOverlayEntry();
  108.       Overlay.of(context).insert(_overlayEntry!);
  109.     } else {
  110.       _overlayEntry!.markNeedsBuild();
  111.     }
  112.   }
  113.  
  114.   void _hideOverlay() {
  115.     _overlayEntry?.remove();
  116.     _overlayEntry = null;
  117.     _filteredItems = List.from(_allItems);
  118.   }
  119.  
  120.   void _filterItems(String query) {
  121.     setState(() {
  122.       _filteredItems =
  123.           _allItems.where((item) => widget.itemLabel(item).toLowerCase().contains(query.toLowerCase())).toList();
  124.     });
  125.     _overlayEntry?.markNeedsBuild();
  126.   }
  127.  
  128.   void _selectItem(T item) {
  129.     final label = widget.itemLabel(item);
  130.     _controller.text = label;
  131.     _controller.selection = TextSelection.collapsed(offset: label.length);
  132.     widget.onSelected?.call(item);
  133.     _hideOverlay();
  134.   }
  135.  
  136.   OverlayEntry _buildOverlayEntry() {
  137.     return OverlayEntry(
  138.       builder: (overlayContext) {
  139.         if (_fieldKey.currentContext == null) return const SizedBox.shrink();
  140.  
  141.         final RenderBox fieldBox = _fieldKey.currentContext!.findRenderObject() as RenderBox;
  142.         final Size fieldSize = fieldBox.size;
  143.         final Offset fieldOffset = fieldBox.localToGlobal(Offset.zero);
  144.  
  145.         final MediaQueryData mq = MediaQuery.of(overlayContext);
  146.         final double screenHeight = mq.size.height;
  147.         final double keyboardHeight = mq.viewInsets.bottom;
  148.         final double topPadding = mq.padding.top;
  149.         final double bottomPadding = mq.padding.bottom;
  150.  
  151.         const double minOverlayHeight = 80; // minimum height for usability
  152.         const double maxOverlayHeightCap = 400; // max height limit, can be parameterized
  153.  
  154.         double availableHeight;
  155.  
  156.         if (keyboardHeight > 0) {
  157.           // Keyboard visible: available height is from top padding to keyboard top minus some margin
  158.           final double bottomLimit = screenHeight - keyboardHeight - bottomPadding;
  159.           availableHeight = bottomLimit - topPadding - 8; // 8 px margin for safety
  160.         } else {
  161.           // Keyboard hidden: available height from below text field to bottom safe area minus margin
  162.           availableHeight = screenHeight - (fieldOffset.dy + fieldSize.height) - bottomPadding - 8;
  163.         }
  164.  
  165.         // Clamp height between min and max cap
  166.         final double maxHeight = availableHeight.clamp(minOverlayHeight, maxOverlayHeightCap);
  167.  
  168.         double top;
  169.         if (keyboardHeight > 0) {
  170.           // Place overlay just above keyboard
  171.           final double bottomLimit = screenHeight - keyboardHeight - bottomPadding;
  172.           top = bottomLimit - maxHeight;
  173.         } else {
  174.           // Place overlay just below TextField
  175.           top = fieldOffset.dy + fieldSize.height;
  176.         }
  177.  
  178.         return _buildPositionedOverlay(fieldOffset.dx, top, fieldSize.width, maxHeight, overlayContext);
  179.       },
  180.     );
  181.   }
  182.  
  183.   Widget _buildPositionedOverlay(double left, double top, double width, double height, BuildContext overlayContext) {
  184.     Widget dropdownContent;
  185.  
  186.     if (widget.dropdownBuilder != null) {
  187.       dropdownContent = widget.dropdownBuilder!(
  188.         overlayContext,
  189.         _filteredItems,
  190.         _selectItem,
  191.         _loading,
  192.         _error,
  193.       );
  194.     } else {
  195.       if (_loading) {
  196.         dropdownContent = const SizedBox(
  197.           height: 56,
  198.           child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
  199.         );
  200.       } else if (_error != null) {
  201.         dropdownContent = ListTile(
  202.           leading: const Icon(Icons.error_outline),
  203.           title: Text(_error!),
  204.         );
  205.       } else if (_filteredItems.isEmpty) {
  206.         dropdownContent = const ListTile(title: Text('No results'));
  207.       } else {
  208.         dropdownContent = ListView.separated(
  209.           padding: EdgeInsets.zero,
  210.           shrinkWrap: true,
  211.           itemCount: _filteredItems.length,
  212.           separatorBuilder: (_, __) => const Divider(height: 1),
  213.           itemBuilder: (context, index) {
  214.             final item = _filteredItems[index];
  215.             return InkWell(
  216.               onTap: () => _selectItem(item),
  217.               child: widget.itemBuilder!(context, item),
  218.             );
  219.           },
  220.         );
  221.       }
  222.     }
  223.  
  224.     return Positioned.fill(
  225.       child: GestureDetector(
  226.         behavior: HitTestBehavior.translucent,
  227.         onTap: () {
  228.           _hideOverlay();
  229.         },
  230.         child: Stack(
  231.           children: [
  232.             Positioned(
  233.               left: left,
  234.               top: top,
  235.               width: width,
  236.               height: height,
  237.               child: Material(
  238.                 elevation: 6,
  239.                 borderRadius: BorderRadius.circular(8),
  240.                 child: Column(
  241.                   children: [
  242.                     Padding(
  243.                       padding: const EdgeInsets.all(8.0),
  244.                       child: TextField(
  245.                         autofocus: true,
  246.                         decoration: const InputDecoration(
  247.                           hintText: 'Search...',
  248.                           prefixIcon: Icon(Icons.search),
  249.                           border: OutlineInputBorder(),
  250.                         ),
  251.                         onChanged: _filterItems,
  252.                       ),
  253.                     ),
  254.                     Expanded(child: dropdownContent),
  255.                   ],
  256.                 ),
  257.               ),
  258.             ),
  259.           ],
  260.         ),
  261.       ),
  262.     );
  263.   }
  264.  
  265.   @override
  266.   Widget build(BuildContext context) {
  267.     return SizedBox(
  268.       child: TextField(
  269.         key: _fieldKey,
  270.         controller: _controller,
  271.         readOnly: true,
  272.         decoration: widget.decoration ??
  273.             const InputDecoration(
  274.               border: OutlineInputBorder(),
  275.               hintText: 'Select...',
  276.               suffixIcon: Icon(Icons.keyboard_arrow_down),
  277.             ),
  278.         onTap: () {
  279.           if (!_loading && _error == null && _allItems.isNotEmpty) {
  280.             _showOverlay();
  281.           }
  282.         },
  283.       ),
  284.     );
  285.   }
  286. }
Advertisement
Add Comment
Please, Sign In to add comment