skip to Main Content

Ok, So I have made my Custom searchdropdown in flutter. I wanted to make it reusable widget, so I made it in different file. Now, I want that when I tap on outside of my dropdown, my overlay closes. I tried with gesture detector, but I couldn’t get the job done. Here is my customsearchdropdown widget :

extension GlobalKeyExtension on GlobalKey {
  Rect? get globalPaintBounds {
    final renderObject = currentContext?.findRenderObject();
    final translation = renderObject?.getTransformTo(null).getTranslation();
    if (translation != null && renderObject?.paintBounds != null) {
      final offset = Offset(translation.x, translation.y);
      return renderObject!.paintBounds.shift(offset);
    } else {
      return null;
    }
  }
}

class CustomSearchableDropDown extends StatefulWidget {
  final List<AcademicData> originalList;
  final Function(AcademicData)? onTap;

  const CustomSearchableDropDown(
      {super.key,
      required this.originalList,
      this.onTap,
      });

  @override
  State<CustomSearchableDropDown> createState() =>
      _CustomSearchableDropDownState();
}

class _CustomSearchableDropDownState extends State<CustomSearchableDropDown> {
  TextEditingController controller = TextEditingController();
  ValueNotifier<List<AcademicData>> list = ValueNotifier([]);
  AcademicData? _selectedFinancialYear;
  OverlayEntry? _overlayEntry;
  final GlobalKey _key=GlobalKey();

  ValueNotifier<bool> show = ValueNotifier(false);

  double getHeight(entries) {
    if(entries==1) {
      return 48;
    }
    else if(entries==2) {
      return 112;
    }
    else if(entries==3) {
      return ScreenSize.utilHeight * 0.2;
    }
    else {
      return 224;
    }
  }

  OverlayEntry _createOverlayEntry() {

    Rect? bounds = _key.globalPaintBounds;

    return OverlayEntry(
        builder: (context) => Positioned(
          left: bounds?.left,
          top: (bounds?.top ?? 0) + 60 + 5,
          width: bounds?.width,
          child: Material(
            elevation: 4.0,
            child: SizedBox(
              child: ValueListenableBuilder(
                valueListenable: list,
                builder: (context, myList, child) {
                  return SizedBox(
                    height: getHeight(myList.length),
                    child: ListView.builder(
                      padding: EdgeInsets.zero,
                      shrinkWrap: true,
                      itemCount: myList.length,
                      itemBuilder: (context, index) {
                        return InkWell(
                          onTap: () {
                            _selectedFinancialYear = myList[index];
                            show.value = false;
                            _overlayEntry?.remove();
                            widget.onTap!(myList[index]);
                            controller.clear();
                            list.value=widget.originalList;
                          },
                          child: ListTile(
                            title: Text(
                              myList[index].sessionYear ?? "",
                              style: const TextStyle(color: Colors.black),
                            ),
                          ),
                        );
                      },
                    ),
                  );
                },
              ),
            ),
          ),
        ),
    );
  }

  void _filterResults(String query) {
    List<AcademicData> duplicateItems = [];
    duplicateItems = widget.originalList.where((item) {
      if (item.sessionYear != null && item.sessionYear!.contains(query)) {
        return true;
      }
      return false;
    }).toList();
    list.value = duplicateItems;
    debugPrint("Changed");
    for (var v in list.value) {
      debugPrint(v.sessionYear);
    }
  }

  final FocusNode _focusNode = FocusNode();

  @override
  void initState() {
    list.value = widget.originalList;
    if (widget.originalList.isNotEmpty) {
      _selectedFinancialYear = widget.originalList[0];
    }

    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {
        _overlayEntry = _createOverlayEntry();
        debugPrint("Inside");
        if (_overlayEntry != null) {
          debugPrint("Not null");
          Overlay.of(context).insert(_overlayEntry!);
        }
      } else {
        _overlayEntry?.remove();
      }
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: show,
      builder: (context, val, child) {
        return val == false
            ? InkWell(
                onTap: () {
                  show.value = true;
                  //_createOverlayEntry();
                },
                child: SizedBox(
                  height: 52,
                  width: double.infinity,
                  child: InputDecorator(
                    decoration: InputDecoration(
                      labelText: 'Academic Year',
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(4.0),
                      ),
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          _selectedFinancialYear?.sessionYear ?? "No Data",
                        ),
                        const Icon(
                          Icons.arrow_drop_down,
                        ),
                      ],
                    ),
                  ),
                ),
              )
            : SizedBox(
                height: 56,
                width: double.infinity,
                child: TextField(
                  key: _key,
                  controller: controller,
                  focusNode: _focusNode,
                  autofocus: true,
                  decoration: InputDecoration(
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4),
                    ),
                    labelText: "Academic Year",
                  ),
                  onChanged: (value) {
                    _filterResults(value);
                  },
                  onSubmitted: (value) {
                    show.value=false;
                  },
                ),
              );
      },
    );
  }
}

And I am using it like this in my screen:

Expanded(
  child: CustomSearchableDropDown(
     originalList: userP.academicYear??[],
     onTap: (value) {
       setState(() {
         _selectedFinancialYear=value;
       });
     },
  ),
),

In my screen, I have used gesture detector to detect when someone taps outside, can it be helpful in this case?

2

Answers


  1. Chosen as BEST ANSWER

    My people, I found a solution for this. I just wrapped my Material widget (which is in OverlayEntry) inside if a TapRegion and called onTapOutside. It was that simple.

    Here is the code change:

    return OverlayEntry(
            builder: (context) => Positioned(
              left: bounds?.left,
              top: (bounds?.top ?? 0) + 48 + 3,
              width: bounds?.width,
              child: TapRegion(
                onTapOutside: (tap) {
                  _overlayEntry?.remove();
                  show.value=false;
                },
                child: Material(
    

    And one more thing, if someone else is using InputDecorator, try to avoid using child and row that I used. When I was decreasing my height for SizedBox, the text was becoming Invisible. So, instead I used prefix and suffixIcon for the same. Here is the better version code:

    SizedBox(
                height: 48,
                width: double.infinity,
                child: InputDecorator(
                  decoration: InputDecoration(
                    labelText: 'Academic Year',
                    labelStyle: const TextStyle(
                        color: Colors.black
                    ),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(4.0),
                    ),
                    suffixIcon: const Icon(
                      Icons.arrow_drop_down,
                    ),
                    prefix: Padding(
                      padding: const EdgeInsets.only(top: 2),
                      child: Text(
                        _selectedFinancialYear?.sessionYear ?? "No Data",
                        style: const TextStyle(
                          fontSize: 15,
                          color: Colors.black,
                        ),
                      ),
                    ),
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      // Text(
                      //   _selectedFinancialYear?.sessionYear ?? "No Data",
                      // ),
                      // const Icon(
                      //   Icons.arrow_drop_down,
                      // ),
                    ],
                  ),
                ),
              )
    

    I have still kept the commented code so you can understand what I was trying to do.


  2. https://drive.google.com/file/d/1Ca_gH73KzLn5tJifbTfnZVBLqtHsqe0L/view?usp=sharing

      DropdownSearch<CountryModel>(
                                  // maxHeight: Get.height*0.3,
                                  mode: Mode.DIALOG,
                                  selectedItem: CountryModel(
                                      countryId: widget
                                          .clubController.clubDetailModel.countryId,
                                      name: widget
                                          .clubController.clubDetailModel.countryName),
                                  onFind: (_) async {
                                    return await MasterAPI.getCountryList();
                                  },
                                  itemAsString: (op) => op.name ?? "",
                                  popupBackgroundColor: ColorConstants.primary,
                                  dropDownButton: SvgPicture.asset(
                                    AssetsConstants.dropDownIcon,
                                    width: 15,
                                  ),
                                  dropdownSearchBaseStyle: Get.textTheme.bodyLarge
                                      ?.copyWith(color: Colors.white),
                                  dropdownSearchDecoration: InputDecoration(
                                      labelText: "",
                                      contentPadding: const EdgeInsets.symmetric(
                                          horizontal: 12, vertical: 14),
                                      isDense: false,
                                      border: OutlineInputBorder(
                                          borderRadius: BorderRadius.circular(12))),
                                  enabled: true,
                                  onChanged: (value) {
                                    widget.clubController
                                        .changeCountry(value ?? CountryModel());
                                  },
                                  hint: 'Please enter country',
                                  autoValidateMode: AutovalidateMode.onUserInteraction,
                                  validator: (u) => GetUtils.isNullOrBlank(u) ?? true
                                      ? "Please select any value"
                                      : null,
                                  searchBoxStyle: Get.textTheme.bodyLarge?.copyWith(
                                    color: Colors.white,
                                  ),
                                  showSearchBox: true),
    

    download all File and put your Project and check

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search