键盘覆盖可搜索下拉小部件

问题描述 投票:0回答:2

我创建了可搜索的下拉小部件,但我对它们有问题。当我单击输入区域时,它会再次渲染列表视图部分,但键盘覆盖它。 我已经尝试过 SingleRowChild 类,它不适用于我们的情况。

实际行为:

enter image description here

预期行为: 当我将注意力集中在输入区域时,它应该可见。

颤振版本3

enter image description here

import 'dart:ffi';

import 'package:checkout_flutter/lang/app_localizations.dart';
import 'package:checkout_flutter/style/color.dart';
import 'package:checkout_flutter/util.dart';
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';

import 'package:flutter/foundation.dart';
import 'package:flutter_svg/flutter_svg.dart';

typedef Validator(String value);

/// Simple dorpdown whith plain text as a dropdown items.
class MNSearchableDropdownField extends StatelessWidget {
  final String? initialValue;
  final List<String> options;
  final InputDecoration? decoration;
  final DropdownEditingController<String>? controller;
  final void Function(String item)? onChanged;
  final void Function(String?)? onSaved;
  final Validator? validator;
  final bool Function(String item, String str)? filterFn;
  final Future<List<String>> Function(String str)? findFn;
  final double? dropdownHeight;
  final String? labelText;
  String? errorText;

  MNSearchableDropdownField(
      {Key? key,
      required this.options,
      this.decoration,
      this.onSaved,
      this.controller,
      this.onChanged,
      this.validator,
      this.errorText,
      this.findFn,
      this.filterFn,
      this.dropdownHeight,
      this.labelText,
      this.initialValue})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DropdownFormField<String>(
      key: key,
      decoration: decoration,
      onSaved: onSaved,
      controller: controller,
      onChanged: onChanged,
      validator: validator,
      dropdownHeight: dropdownHeight,
      labelText: labelText,
      initialValue: initialValue,
      displayItemFn: (dynamic str) => Text(
        str ?? '',
        style: TextStyle(fontSize: 16),
      ),
      findFn: findFn ?? (dynamic str) async => options,
      filterFn: filterFn ?? (dynamic item, str) => item.toLowerCase().startsWith(str.toLowerCase()),
      dropdownItemFn: (dynamic item, position, focused, selected, onTap) => ListTile(
        title: Text(
          item,
          style: TextStyle(color: Colors.black),
        ),
        tileColor: focused ? Color.fromARGB(20, 0, 0, 0) : Colors.transparent,
        onTap: onTap,
      ),
    );
  }
}

class DropdownEditingController<T> extends ChangeNotifier {
  T? _value;

  DropdownEditingController({T? value}) : _value = value;

  T? get value => _value;

  set value(T? newValue) {
    if (_value == newValue) return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

/// Create a dropdown form field
class DropdownFormField<T> extends StatefulWidget {
  final bool autoFocus;

  /// It will trigger on user search
  final bool Function(T item, String str)? filterFn;

  /// Check item is selectd
  final bool Function(T? item1, T? item2)? selectedFn;

  /// Return list of items what need to list for dropdown.
  /// The list may be offline, or remote data from server.
  final Future<List<T>> Function(String str) findFn;

  /// Build dropdown Items, it get called for all dropdown items
  ///  [item] = [dynamic value] List item to build dropdown Listtile
  /// [lasSelectedItem] = [null | dynamic value] last selected item, it gives user chance to highlight selected item
  /// [position] = [0,1,2...] Index of the list item
  /// [focused] = [true | false] is the item if focused, it gives user chance to highlight focused item
  /// [onTap] = [Function] *important! just assign this function to Listtile.onTap  = onTap, incase you missed this,
  /// the click event if the dropdown item will not work.
  ///
  final ListTile Function(
    T item,
    int position,
    bool focused,
    bool selected,
    Function() onTap,
  ) dropdownItemFn;

  /// Build widget to display selected item inside Form Field
  final Widget Function(T? item) displayItemFn;

  final InputDecoration? decoration;
  final Color? dropdownColor;
  final DropdownEditingController<T>? controller;
  final void Function(String item)? onChanged;
  final void Function(T?)? onSaved;
  final Validator? validator;

  /// height of the dropdown overlay, Default: 240
  final double? dropdownHeight;

  final String? labelText;

  /// Style the search box text
  final TextStyle? searchTextStyle;

  /// Message to disloay if the search dows not match with any item, Default : "No matching found!"
  final String emptyText;

  /// Give action text if you want handle the empty search.
  final String emptyActionText;

  /// this functon triggers on click of emptyAction button
  final Future<void> Function()? onEmptyActionPressed;

  final String? initialValue;

  DropdownFormField(
      {Key? key,
      required this.dropdownItemFn,
      required this.displayItemFn,
      required this.findFn,
      this.filterFn,
      this.autoFocus = false,
      this.controller,
      this.validator,
      this.decoration,
      this.dropdownColor,
      this.onChanged,
      this.onSaved,
      this.dropdownHeight,
      this.searchTextStyle,
      this.emptyText = "No matching found!",
      this.emptyActionText = 'Create new',
      this.onEmptyActionPressed,
      this.selectedFn,
      this.labelText,
      this.initialValue})
      : super(key: key);

  @override
  DropdownFormFieldState createState() => DropdownFormFieldState<T>(key);
}

class DropdownFormFieldState<T> extends State<DropdownFormField> with SingleTickerProviderStateMixin {
  final FocusNode _widgetFocusNode = FocusNode();
  final FocusNode _searchFocusNode = FocusNode();
  final LayerLink _layerLink = LayerLink();
  final ValueNotifier<List<T>> _listItemsValueNotifier = ValueNotifier<List<T>>([]);
  final TextEditingController _searchTextController = TextEditingController();
  final DropdownEditingController<T>? _controller = DropdownEditingController<T>();
  final Key? key;
  final String? initialValue = "";
  Validator? validator;
  String? errorText;

  int numberOfItems = 4;

  bool get _isEmpty => _selectedItem == null;
  bool _isFocused = false;
  bool _isListEnable = false;

  OverlayEntry? _overlayEntry;
  OverlayEntry? _overlayBackdropEntry;
  List<T>? _options;
  int _listItemFocusedPosition = 0;
  T? _selectedItem;
  Widget? _displayItem;
  Timer? _debounce;
  String? _lastSearchString;

  DropdownEditingController<dynamic>? get _effectiveController => widget.controller ?? _controller;

  DropdownFormFieldState(this.key) : super() {}

  @override
  void initState() {
    super.initState();

    if (widget.autoFocus) _widgetFocusNode.requestFocus();
    _selectedItem = widget.initialValue != null ? widget.initialValue : _effectiveController!.value;
    if (_selectedItem == "") _selectedItem = null;

    _searchFocusNode.addListener(() {
      if (!_searchFocusNode.hasFocus && _overlayEntry != null) {
        _removeOverlay();
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _debounce?.cancel();
    _searchTextController.dispose();
  }

  String? validate(String? value) {
    //DISUCSS APPROPRIATE NULL-CHECK?
    if (_selectedItem == null) {
      errorText = AppLocalizations.current!.requiredFieldMessage;
    }
    return errorText;
  }

  @override
  Widget build(BuildContext context) {
    _displayItem = widget.displayItemFn(_selectedItem);

    return Semantics(
        label: Util.keyToString(key!),
        child: CompositedTransformTarget(
            link: this._layerLink,
            child: Column(children: [
              GestureDetector(
                onTap: () {
                  if (_selectedItem == null)
                    _selectedItem = widget.initialValue != null ? widget.initialValue : _effectiveController!.value;

                  //_widgetFocusNode.requestFocus();
                  setState(() {
                    _isFocused = true;
                    _isListEnable = true;
                  });
                  _searchFocusNode.requestFocus();
                  _search("");
                },
                child: Focus(
                  autofocus: widget.autoFocus,
                  focusNode: _widgetFocusNode,
                  onFocusChange: (focused) {
                    setState(() {
                      _isFocused = focused;
                    });
                    if (!_isFocused && (_effectiveController!.value == null || _effectiveController!.value != "")) {
                      setState(() {
                        errorText = validate(_effectiveController!.value);
                      });
                    } else {
                      setState(() {
                        errorText = null;
                      });
                    }
                  },
                  child: FormField(
                    validator: validate,
                    onSaved: (str) {
                      if (widget.onSaved != null) {
                        widget.onSaved!(_effectiveController!.value);
                      }
                    },
                    builder: (state) {
                      return Container(
                          child: InputDecorator(
                        key: Key('searchable-dropdown-input'),
                        decoration: InputDecoration(
                          border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black, width: 2)),
                          focusedBorder: _isListEnable
                              ? OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.black, width: 2),
                                  borderRadius:
                                      BorderRadius.only(topLeft: Radius.circular(5), topRight: Radius.circular(5)))
                              : OutlineInputBorder(
                                  borderSide: BorderSide(
                                      color: errorText == null ? Colors.black : CustomColors.errorRed, width: 2)),
                          disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.black, width: 2)),
                          enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(
                                color: errorText == null ? CustomColors.radioGray : CustomColors.errorRed, width: 2),
                          ),
                          errorBorder:
                              OutlineInputBorder(borderSide: BorderSide(color: CustomColors.errorRed, width: 2)),
                          focusedErrorBorder:
                              OutlineInputBorder(borderSide: BorderSide(color: CustomColors.errorRed, width: 2)),
                          isDense: true,
                          labelStyle: TextStyle(
                              color: _isStateValidate(),
                              fontSize: 16,
                              fontWeight: _isFocused ? FontWeight.w700 : FontWeight.w400,
                              fontFamily: "HankenSans-Regular"),
                          errorText: errorText,
                          errorStyle: TextStyle(
                              fontSize: 12,
                              height: 1.5,
                              color: CustomColors.errorRed,
                              fontWeight: FontWeight.w700,
                              fontFamily: "HankenSans-Medium"),
                          suffixIcon: _isListEnable && _isFocused
                              ? SvgPicture.asset('assets/up_arrow.svg',
                                  key: Key('total-expand-icon'),
                                  color: Colors.black,
                                  fit: BoxFit.scaleDown,
                                  package: 'checkout_flutter')
                              : SvgPicture.asset('assets/down_arrow.svg',
                                  key: Key('total-expan-icon'),
                                  color: Colors.grey,
                                  fit: BoxFit.scaleDown,
                                  package: 'checkout_flutter'),
                          labelText: widget.labelText,
                          hintText: widget.labelText,
                          hintStyle: TextStyle(
                              color: CustomColors.primaryBlack, fontSize: 16, fontFamily: "HankenSans-Regular"),
                        ),
                        isEmpty: _isEmpty,
                        isFocused: _isFocused,
                        child: this._isListEnable && this._isFocused
                            ? EditableText(
                                key: Key('searchable-dropdown-edit-text'),
                                style: TextStyle(color: Colors.black, fontSize: 16, fontFamily: "HankenSans-Medium"),
                                controller: _searchTextController,
                                cursorColor: Colors.black,
                                focusNode: _searchFocusNode,
                                backgroundCursorColor: Colors.transparent,
                                onChanged: (str) {
                                  _onTextChanged(str);
                                },
                                onSubmitted: (str) {
                                  //_searchTextController.value = TextEditingValue(text: "");
                                  _toggleOverlay();
                                  _setValue();
                                  _removeOverlay();
                                  _widgetFocusNode.nextFocus();
                                  setState(() {
                                    errorText = validate(str);
                                  });
                                },
                                onEditingComplete: () {},
                              )
                            : _displayItem ?? Container(),
                      ));
                    },
                  ),
                ),
              ),
              this._isListEnable && _isFocused ? _createListView() : Container(),
            ])));
  }

  Widget _createListView() {
    return Material(
        child: Container(
      decoration: BoxDecoration(
        border: Border(
            right: BorderSide(
              color: Colors.black,
              width: 2,
            ),
            left: BorderSide(
              color: Colors.black,
              width: 2,
            ),
            top: BorderSide.none,
            bottom: BorderSide(color: Colors.black, width: 2)),
      ),
      child: SizedBox(
          height: (45 * numberOfItems).toDouble(),
          child: Container(
              child: ValueListenableBuilder(
                  key: Key('searchable-dropdown-listview'),
                  valueListenable: _listItemsValueNotifier,
                  builder: (context, List<T> items, child) {
                    return _options != null && _options!.length > 0
                        ? Padding(
                            padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
                            child: Scrollbar(
                                key: Key('searchable-dropdown-listview-scroll'),
                                //isAlwaysShown: true,
                                radius: Radius.circular(50),
                                child: Padding(
                                    padding: EdgeInsets.fromLTRB(10, 0, 5, 0),
                                    child: ListView.builder(
                                        shrinkWrap: false,
                                        itemCount: _options!.length,
                                        itemBuilder: (context, position) {
                                          return Container(
                                              decoration: BoxDecoration(
                                                  border: Border(
                                                      bottom: BorderSide(width: 1, color: CustomColors.divider))),
                                              child: TextButton(
                                                  clipBehavior: Clip.none,
                                                  style: TextButton.styleFrom(
                                                      padding: EdgeInsets.zero,
                                                      primary: Colors.black,
                                                      textStyle: const TextStyle(
                                                          fontSize: 14, fontFamily: "HankenSans-Regular")),
                                                  onPressed: () async {
                                                    _listItemFocusedPosition = position;
                                                    _searchTextController.value = TextEditingValue(text: "");
                                                    _setValue();
                                                    _isListEnable = false;
                                                  },
                                                  child: Align(
                                                      alignment: Alignment.topLeft,
                                                      child: Text(
                                                        _options![position].toString(),
                                                      ))));
                                        }))))
                        : Container(
                            padding: EdgeInsets.all(16),
                            child: Column(
                              mainAxisSize: MainAxisSize.max,
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.center,
                              children: [
                                Text(
                                  widget.emptyText,
                                  style: TextStyle(color: Colors.black45),
                                ),
                                if (widget.onEmptyActionPressed != null)
                                  TextButton(
                                    onPressed: () async {
                                      await widget.onEmptyActionPressed!();
                                      _search(_searchTextController.value.text);
                                    },
                                    child: Text(widget.emptyActionText),
                                  ),
                              ],
                            ),
                          );
                  }))),
    ));
  }

  _removeOverlay() {
    if (_overlayEntry != null) {
      _overlayBackdropEntry!.remove();
      _overlayEntry!.remove();
      _overlayEntry = null;
      //_searchTextController.value = TextEditingValue.empty;
      setState(() {});
    }
  }

  _isStateValidate() {
    if (errorText != null) {
      return CustomColors.errorRed;
    } else if (_isFocused) {
      return Colors.black;
    } else {
      return CustomColors.primaryGray;
    }
  }

  _toggleOverlay() {
    _isListEnable = !_isListEnable;
    setState(() {});
  }

  _onTextChanged(String? str) {
    if (_debounce?.isActive ?? false) _debounce!.cancel();
    _debounce = Timer(const Duration(milliseconds: 100), () {
      if (_lastSearchString != str) {
        _lastSearchString = str;
        _search(str ?? "");
      }
    });
    errorText = null;
    if (validator != null) errorText = validate(str);
  }

  _search(String str) async {
    List<T> items = await widget.findFn(str) as List<T>;

    if (str.isNotEmpty && widget.filterFn != null) {
      items = items.where((item) => widget.filterFn!(item, str)).toList();
    }

    if (items.length == 0) {
      return;
    }

    setState(() {
      numberOfItems = items.length >= 4 ? 4 : items.length % 4;
    });

    _options = items;
    _listItemsValueNotifier.value = items;
  }

  _setValue() {
    var item = _options![_listItemFocusedPosition];
    _selectedItem = item;

    _effectiveController!.value = _selectedItem;

    if (widget.onChanged != null) {
      widget.onChanged!(_selectedItem as String);
    }

    _searchTextController.value = TextEditingValue(text: _selectedItem.toString());
    FocusScope.of(context).unfocus();
    setState(() {});
  }


}

flutter dart cross-platform hybrid-mobile-app
2个回答
2
投票

这对我有用:https://stackoverflow.com/a/73047628/12172300

只需在 SingleChildScrollView 上添加

reverse: true
即可。

child: Center(
      child: SingleChildScrollView(
        reverse: true,
        child: Column(

0
投票

我面临同样的问题,但是当我从底部添加填充到 SingleChildScrollView 时,我的问题就解决了。

子:SingleChildScrollView( 填充:EdgeInsets.only(底部:MediaQuery.of(context).viewInsets.bottom), 子项:下拉菜单(

下面是我的完整代码

// DropDownMenuEntry labels and values
enum ColorLabel {
  blue(
      'Blue',
      Colors.blue,
      Icon(
        Icons.bluetooth,
        color: Colors.blue,
      )),
  pink(
      'Pink',
      Colors.pink,
      Icon(
        Icons.piano,
        color: Colors.pink,
      )),
  green(
      'Green',
      Colors.green,
      Icon(
        Icons.grade,
        color: Colors.green,
      )),
  yellow(
      'Orange',
      Colors.orange,
      Icon(
        Icons.onetwothree_rounded,
        color: Colors.orange,
      )),
  grey(
      'Grey',
      Colors.grey,
      Icon(
        Icons.graphic_eq,
        color: Colors.grey,
      ));

  const ColorLabel(this.label, this.color, this.icon);

  final String label;
  final Color color;
  final Icon icon;
}

class CustomDropDownMenu extends StatefulWidget {
  const CustomDropDownMenu({super.key});

  @override
  State<CustomDropDownMenu> createState() => _CustomDropDownMenuState();
}

class _CustomDropDownMenuState extends State<CustomDropDownMenu> {
  final TextEditingController colorController = TextEditingController();
  ColorLabel? selectedColor;

  void onSelection(ColorLabel? color) {
    if (color != null) {
      setState(() {
        selectedColor = color;
        FocusScope.of(context).requestFocus(new FocusNode());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
      home: Scaffold(
        body: SafeArea(
          child: GestureDetector(
            onTap: () {
              FocusScope.of(context).requestFocus(FocusNode());
            },
            child: Container(
              height: double.infinity,
              width: double.infinity,
              color: selectedColor?.color,
              child: Center(
                child: SingleChildScrollView(
                  padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
                  child: DropdownMenu<ColorLabel>(
                    menuStyle: const MenuStyle(
                      backgroundColor: MaterialStatePropertyAll(Colors.white)
                    ),
                    onSelected: onSelection,
                    leadingIcon: const Icon(Icons.search),
                    label: const Text('Colors'),
                    hintText: "Search",
                    // initialSelection: ColorLabel.green,
                    enableSearch: true,
                    // enableFilter: true,
                    controller: colorController,
                    requestFocusOnTap: true,
                    dropdownMenuEntries: ColorLabel.values
                        .map(
                            (ColorLabel color) => DropdownMenuEntry<ColorLabel>(
                                value: color,
                                label: color.label,
                                leadingIcon: color.icon,
                                enabled: color.label != 'Orange',
                                style: MenuItemButton.styleFrom(
                                  foregroundColor: color.color,
                                )))
                        .toList(),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.