AhmedLSayed9 / dropdown_button2

Flutter's core Dropdown Button widget with steady dropdown menu and many other features.
https://pub.dev/packages/dropdown_button2
MIT License
264 stars 122 forks source link

Not resetting the search querry before reopening the menu breaks the dropdown menu scroll position calculation algorithm #285

Closed hlvs-apps closed 3 months ago

hlvs-apps commented 3 months ago

The title prety much explains the issue. Here is a example video to visualize the issue: search_index Code to reproduce the video:

import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
//import 'package:flutterfontchooser/flutterfontchooser.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: DropdownButtonHideUnderline(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              Text("Width: ..."),
              Text("Height: 500px"),
              Text("..."),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  Text("Bold"),
                  Text("Italic"),
                ],
              ),
              FontChooserDropdownMenuWithoutServerAccessAndNoFonts(),
              //FontChooserDropdownMenu(),
              //SizedBox(height: 200),
            ],
          ),
        ),
      ),
    );
  }
}

//a dropdown widget that allows the user to choose a font, and displays the font name in the dropdown.
//the fonts get dynamically loaded from the server
class FontChooserDropdownMenuWithoutServerAccessAndNoFonts extends StatefulWidget {
  //const FontChooserDropdownMenu({super.key, super.controller});
  const FontChooserDropdownMenuWithoutServerAccessAndNoFonts({super.key});

  @override
  State<FontChooserDropdownMenuWithoutServerAccessAndNoFonts> createState() =>
      _FontChooserDropdownMenuWithoutServerAccessAndNoFontsState();
}

class _FontChooserDropdownMenuWithoutServerAccessAndNoFontsState
    extends State<FontChooserDropdownMenuWithoutServerAccessAndNoFonts> {
  //final Map<SearchResponseEntryComparable, DropdownItem<SearchResponseEntryComparable>>
  final Map<String, DropdownItem<String>>
  _items = {};

  void _loadItems() {
    /*Set<SearchResponseEntryComparable> oldItems = _items.keys.toSet();
    bool changed = false;
    for (var value in controller.allFontsIfLoaded) {
      SearchResponseEntryComparable comparable =
      SearchResponseEntryComparable(value);
      if (!oldItems.contains(comparable)) {
        changed = true;
        _items[comparable] = DropdownItem<SearchResponseEntryComparable>(
          value: comparable,
          child: DropdownRowWidget(
            key: Key("${value.family}-dropdown-row-widget"),
            selectedFont: selectedFont,
            thisFont: value,
          ),
        );
      }
      oldItems.remove(comparable);
    }
    for (var item in oldItems) {
      _items.remove(item);
    }
    if (changed && mounted) {
        setState(() {});
    }*/
    for(int i=0; i<1500;i++){
      _items["Font $i"] = DropdownItem<String>(
        value: "Font $i",
        child: DropdownRowWidget(
          key: Key("Font $i-dropdown-row-widget"),
          selectedFont: selectedFont,
          thisFont: "Font $i",
        ),
      );
    }
    setState(() {});
  }

  @override
  void initState() {
    super.initState();
    ServicesBinding.instance.keyboard.addHandler(_onKey);
    Future.microtask(_loadItems);
  }

  /*@override
  void onController() {
    var s = searchResponseEntryComparableFromSearchResponseEntry(
        controller.selectedFont?.searchResponseEntry);
    if (s != selectedFont.value) {
      selectedFont.value = s;
    }
    _loadItems();
  }*/

  @override
  void dispose() {
    ServicesBinding.instance.keyboard.removeHandler(_onKey);
    super.dispose();
  }

  //final ValueNotifier<SearchResponseEntryComparable?> selectedFont =
  final ValueNotifier<String?> selectedFont =
  ValueNotifier(null);

  TextEditingController searchController = TextEditingController();

  FocusNode searchFocusNode = FocusNode();
  bool _onKey(KeyEvent event) {
    /*setState(() {
      //set random height
      maxHeight = 200 + (100 * (DateTime.now().second % 5));
    });*/
    if (_menuOpen && !searchFocusNode.hasFocus && event is KeyDownEvent) {
      String? key = event.character;
      if (key != null &&
          event.logicalKey != LogicalKeyboardKey.enter &&
          event.logicalKey != LogicalKeyboardKey.space &&
          event.logicalKey != LogicalKeyboardKey.tab) {
        if (event.logicalKey == LogicalKeyboardKey.backspace) {
          if (searchController.text.isNotEmpty) {
            searchController.text = "";
            searchFocusNode.requestFocus();
            return true;
          }
          searchFocusNode.requestFocus();
          return false;
        }
        searchController.text += key;
        searchFocusNode.requestFocus();
        return true;
      }
    }
    return false;
  }

  bool _menuOpen = false;

  @override
  Widget build(BuildContext context) {
    //return DropdownButton2<SearchResponseEntryComparable>(
    return DropdownButton2<String>(
      valueListenable: selectedFont,
      barrierCoversButton: false,
      onMenuStateChange: (open) {
        _menuOpen = open;
      },
      buttonStyleData: const ButtonStyleData(
        width: 260,
        height: 50,
      ),
      dropdownSearchData: DropdownSearchData(
        searchController: searchController,
        searchBarWidgetHeight: 50,
        searchBarWidget: Container(
          height: 50,
          padding: const EdgeInsets.only(
            top: 8,
            bottom: 4,
            right: 8,
            left: 8,
          ),
          child: TextFormField(
            maxLines: 1,
            controller: searchController,
            focusNode: searchFocusNode,
            decoration: InputDecoration(
              isDense: true,
              contentPadding: const EdgeInsets.symmetric(
                horizontal: 10,
                vertical: 8,
              ),
              hintText: 'Search for a font...',
              hintStyle: const TextStyle(fontSize: 12),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8),
              ),
            ),
          ),
        ),
        noResultsWidget: const Padding(
          padding: EdgeInsets.all(8),
          child: Text('No Font Found!'),
        ),
        searchMatchFn:
            //(DropdownItem<SearchResponseEntryComparable> entry, String search) {
            (DropdownItem<String> entry, String search) {
          //return entry.value?.family
           return entry.value?.toLowerCase()
              .contains(search.toLowerCase()) ??
              false;
        },
      ),
      iconStyleData: const IconStyleData(
        icon: Icon(
          Icons.arrow_drop_down,
          color: Colors.black45,
        ),
      ),
      dropdownStyleData: DropdownStyleData(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(15),
        ),
      ),
      menuItemStyleData: const MenuItemStyleData(
        padding: EdgeInsets.symmetric(horizontal: 16),
      ),
      hint: const Text(
        'Select a font',
        style: TextStyle(fontSize: 14),
      ),
      /*onChanged: (SearchResponseEntryComparable? newValue) {
        selectedFont.value = newValue;
        if (newValue == null) {
          controller.selectedFont = null;
          return;
        }
        Future.microtask(() async {
          controller.selectedFont =
          await LoadedFont.fromSearchResponseEntry(newValue.entry);
        });
      },*/
      onChanged: (String? newValue) {
        selectedFont.value = newValue;
      },
      items: _items.values.toList(),
    );
  }
}

class DropdownRowWidget extends StatefulWidget {
  //final SearchResponseEntry thisFont;
  final String thisFont;
  //final ValueNotifier<SearchResponseEntryComparable?> selectedFont;
  final ValueNotifier<String?> selectedFont;
  final double width;

  const DropdownRowWidget(
      {super.key,
        required this.thisFont,
        this.width = 200,
        required this.selectedFont});

  @override
  State<DropdownRowWidget> createState() => _DropdownRowWidgetState();
}

class _DropdownRowWidgetState extends State<DropdownRowWidget> {
  bool _isFontLoaded = false;
  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        SizedBox(
          width: widget.width,
          child: Text(widget.thisFont,
              /*style: Theme.of(context)
                  .textTheme
                  .apply(fontFamily: widget.thisFont.menuFontFamily)
                  .bodyMedium*/),
        ),
        if (!_isFontLoaded && !_isLoading)
          const Icon(
            Icons.cloud_download_outlined,
            color: Colors.grey,
            size: 24,
          ),
        /*if (!_isFontLoaded && _isLoading)
          LoadingAnimationWidget.dotsTriangle(
            color: Colors.grey,
            size: 24,
          ),*/
      ],
    );
  }

  void _check() {
    if(!mounted){
      return;
    }
    bool s = false;
    //bool i = isFontLoaded(widget.thisFont);
    bool i =false;
    if (i != _isFontLoaded) {
      _isFontLoaded = i;
      s = true;
    }
    //i = isLoadingFont(widget.thisFont);
    i= false;
    if (i != _isLoading) {
      _isLoading = i;
      s = true;
    }
    if (s) {
      setState(() {});
    }
  }

  @override
  void initState() {
    super.initState();
    _check();
    //FontLoaderNotifier.instance.addListener(_check);
    widget.selectedFont.addListener(_check);
  }

  @override
  void didUpdateWidget(DropdownRowWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    _check();
  }

  @override
  void dispose() {
    //FontLoaderNotifier.instance.removeListener(_check);
    super.dispose();
  }
}