Flutter 2.0 - Autocomplete小部件从右侧溢出屏幕

35
我该如何使自动完成框与没有特定宽度的TextField大小相同,而是占据最大宽度?
如何使自动完成框与没有特定宽度的TextField大小相同,而是占据最大宽度?
              Autocomplete(
                optionsBuilder: (TextEditingValue textEditingValue) {
                  if (textEditingValue.text == '') {
                    return ['aa', 'bb', 'cc', 'aa', 'bb', 'cc'];
                  }
                  return ['aa', 'bb', 'cc', 'aa', 'bb', 'cc']
                      .where((String option) {
                    return option
                        .toString()
                        .contains(textEditingValue.text.toLowerCase());
                  });
                },
                onSelected: (option) {
                  print(option);
                },
              ),

enter image description here


能否展示更多的代码? - Andrej
@Andrej,你可以试试 Flutter 文档中的自动完成部分。在 AutocompleteBasicExample() 小部件上添加 Padding。文档链接:https://api.flutter.dev/flutter/material/Autocomplete-class.html - Ibrahim Ali
我尝试了一下,发现“Padding”把它搞砸了。这很可能是一个bug。 - Andrej
是的,这是一个错误。 - user3670859
10个回答

35

当我尝试基于RawAutocomplete制作自己的自动完成小部件时,我遇到了同样的问题。我使用了布局构建器来获取选项视图生成器中完美的容器宽度大小:

LayoutBuilder(
  builder: (context, constraints) => RawAutocomplete<String>(
    focusNode: focusNode,
    fieldViewBuilder: fieldViewBuilder,
    optionsViewBuilder: (context, onSelected, options) => Align(
      alignment: Alignment.topLeft,
      child: Material(
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.vertical(bottom: Radius.circular(4.0)),
        ),
        child: Container(
          height: 52.0 * options.length,
          width: constraints.biggest.width, // <-- Right here !
          child: ListView.builder(
            padding: EdgeInsets.zero,
            itemCount: options.length,
            shrinkWrap: false,
            itemBuilder: (BuildContext context, int index) {
              final String option = options.elementAt(index);
              return InkWell(
                onTap: () => onSelected(option),
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(option),
                ),
              );
            },
          ),
        ),
      ),
    ),
    optionsBuilder: (textEditingValue) =>
        suggestions.where((element) => element.contains(textEditingValue.text.trim().toUpperCase())),
    textEditingController: controller,
  ),
)

结果: 我的组件期望的结果

完美运行! - Shahood ul Hassan

14

我也经常遇到这个问题,而且选项构建器对于大小或填充要放置在层次结构的位置非常具体。以下是我的工作示例:

optionsViewBuilder: (context, onAutoCompleteSelect, options) {
  return Align(
    alignment: Alignment.topLeft,
       child: Material(
         color: Theme.of(context).primaryColorLight,
           elevation: 4.0,
           // size works, when placed here below the Material widget
           child: Container(
              // I have the text field wrapped in a container with
              // EdgeInsets.all(20) so subtract 40 from the width for the width
              // of the text box. You could also just use a padding widget
              // with EdgeInsets.only(right: 20)
              width: MediaQuery.of(context).size.width - 40,
                child: ListView.separated(
                   shrinkWrap: true,
                   padding: const EdgeInsets.all(8.0),
                   itemCount: options.length,
                   separatorBuilder: (context, i) {
                     return Divider();
                   },
                   itemBuilder: (BuildContext context, int index) {
                     // some child here
                   },
                 )
              ),
           )
        );
     }


8
此处所指出,在编写本文时,最佳选择似乎是修改包含在默认_AutoCompleteOptions代码中的BoxConstraints宽度和高度(可以在这里找到),并将其作为AutocompleteoptionsViewBuilder参数传递。 不过,这需要您能够提前计算所需的宽度。

仅供参考,附上我的代码:

Autocomplete<Animal>(
  optionsBuilder: (textEditingValue) {
    if (textEditingValue.text == '') {
      return const Iterable<Animal>.empty();
    }
    return animalsList.where((Animal option) {
      return "${option.id}".contains(textEditingValue.text.toLowerCase()) ||
          option.nome!.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
          option.ownerCF!.toLowerCase().contains(textEditingValue.text.toLowerCase());
    });
  },
  optionsViewBuilder: (context, onSelected, options) {
    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4.0,
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 200, maxWidth: 600), //RELEVANT CHANGE: added maxWidth
          child: ListView.builder(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            itemCount: options.length,
            itemBuilder: (BuildContext context, int index) {
              final Animal option = options.elementAt(index);
              return InkWell(
                onTap: () {
                  onSelected(option);
                },
                child: Builder(builder: (BuildContext context) {
                  final bool highlight = AutocompleteHighlightedOption.of(context) == index;
                  if (highlight) {
                    SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
                      Scrollable.ensureVisible(context, alignment: 0.5);
                    });
                  }
                  return Container(
                    color: highlight ? Theme.of(context).focusColor : null,
                    padding: const EdgeInsets.all(16.0),
                    child: Text(option.getInfo() + " - " + option.getOwnerInfo()),
                  );
                }),
              );
            },
          ),
        ),
      ),
    );
  },
  displayStringForOption: (option) => option.getInfo() + " - " + option.getOwnerInfo(), 
  onSelected: (option) => selected = option,
);

但是如何提前计算所需的宽度呢?maxWidth = 600并不能达到目的。 - Shahood ul Hassan
@ShahoodulHassan 我是凭感觉选择的,因为它对我的应用程序和自动完成内容来说已经足够好了。我知道这不是最佳方法:您可以将AutoComplete包装在LayoutBuilder中,在运行时了解可用空间,然后决定哪种大小最适合您的情况。 - il_boga

5

我遇到了同样的问题,并通过使用 ValueListenableBuilder 解决了它。

Material小部件的宽度取决于field ViewBuilder返回的小部件的宽度(默认为TextFormField),因此一种解决方案是在一帧完成后获取TextFormField的宽度并通知你的自定义optionsViewBuilder

示例代码:

import 'package:flutter/material.dart';

class AutocompleteText extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => AutocompleteTextState();
}

class AutocompleteTextState extends State<AutocompleteText> {
  final ValueNotifier<double?> optionsViewWidthNotifier = ValueNotifier(null);

  static const List<User> _userOptions = <User>[
    User(name: 'Alice', email: 'alice@example.com'),
    User(name: 'Bob', email: 'bob@example.com'),
    User(name: 'Charlie', email: 'charlie123@gmail.com'),
  ];

  static String _displayStringForOption(User option) => option.name;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Padding(
      padding: EdgeInsets.all(20),
      child: Autocomplete<User>(
        displayStringForOption: _displayStringForOption,
        optionsBuilder: (TextEditingValue textEditingValue) {
          if (textEditingValue.text == '') {
            return const Iterable<User>.empty();
          }
          return _userOptions.where((User option) {
            return option
                .toString()
                .contains(textEditingValue.text.toLowerCase());
          });
        },
        onSelected: (User selection) {
          print('You just selected ${_displayStringForOption(selection)}');
        },
        fieldViewBuilder: (BuildContext context,
            TextEditingController textEditingController,
            FocusNode focusNode,
            VoidCallback onFieldSubmitted) {
          return OrientationBuilder(builder: (context, orientation) {
            WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
              optionsViewWidthNotifier.value =
                  (context.findRenderObject() as RenderBox).size.width;
            });
            return TextFormField(
              controller: textEditingController,
              focusNode: focusNode,
              onFieldSubmitted: (String value) {
                onFieldSubmitted();
              },
            );
          });
        },
        optionsViewBuilder: (
          BuildContext context,
          AutocompleteOnSelected<User> onSelected,
          Iterable<User> options,
        ) {
          return ValueListenableBuilder<double?>(
              valueListenable: optionsViewWidthNotifier,
              builder: (context, width, _child) {
                return Align(
                  alignment: Alignment.topLeft,
                  child: Material(
                    elevation: 4.0,
                    child: SizedBox(
                      width: width,
                      height: 200.0,
                      child: ListView.builder(
                        padding: EdgeInsets.zero,
                        itemCount: options.length,
                        itemBuilder: (BuildContext context, int index) {
                          final User option = options.elementAt(index);
                          return InkWell(
                            onTap: () {
                              onSelected(option);
                            },
                            child: Padding(
                              padding: const EdgeInsets.all(16.0),
                              child: Text(_displayStringForOption(option)),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                );
              });
        },
      ),
    ));
  }

  @override
  void dispose() {
    optionsViewWidthNotifier.dispose();
    super.dispose();
  }
}

@immutable
class User {
  const User({
    required this.email,
    required this.name,
  });

  final String email;
  final String name;

  @override
  String toString() {
    return '$name, $email';
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is User && other.name == name && other.email == email;
  }

  @override
  int get hashCode => hashValues(email, name);
}

谢谢 - 这是唯一一个不需要修改底层依赖代码或硬编码宽度就能正常工作的解决方案。对我很有效(我正在使用chips_input,它包装了自动完成小部件)。我知道这有点像黑客,但我会接受它。希望他们能修复潜在的错误 - Rik
完美运行! - Shahood ul Hassan

3
在我的情况下,我只需要用 Align 将我返回的小部件包装在 optionsBuilder 中。这样做之后,它开始按预期工作,创建菜单并不会超出屏幕。我还使用了 LayoutBuilderconstraints.maxWidth 定义了菜单的宽度。
示例:
LayoutBuilder(
  builder: (context, constraints) {
    return RawAutocomplete<String>(
      optionsViewBuilder: (context, onSelected, options) {
        return Align(
          alignment: Alignment.topLeft,
          child: SizedBox(
            width: constraints.maxWidth,
            child: Material(
              elevation: 4,
              clipBehavior: Clip.antiAlias,
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(10)),
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: options.map((e) {
                  return ListTile(title: Text(e));
                }).toList(),
              ),
            ),
          ),
        );
      },
    );
  },
);

enter image description here


2

我看过源代码autocomplete.dart,发现缺少了参数width,但是height存在,我手动添加了width,结果它正常工作了。

class Autocomplete<T extends Object> extends StatelessWidget {
/// Creates an instance of [Autocomplete].
const Autocomplete({
Key? key,
required this.optionsBuilder,
this.displayStringForOption = RawAutocomplete.defaultStringForOption,
this.fieldViewBuilder = _defaultFieldViewBuilder,
this.onSelected,
this.optionsMaxHeight = 200.0,
this.optionsMaxWidth = 285.0 // I added this line
this.optionsViewBuilder,
this.initialValue,

/// The default value is set to 200.
final double optionsMaxHeight;
final double optionsMaxWidth; // Added this line

 @override
Widget build(BuildContext context) {
return RawAutocomplete<T>(
  displayStringForOption: displayStringForOption,
  fieldViewBuilder: fieldViewBuilder,
  initialValue: initialValue,
  optionsBuilder: optionsBuilder,
  optionsViewBuilder: optionsViewBuilder ?? (BuildContext context, AutocompleteOnSelected<T> onSelected, Iterable<T> options) {
    return _AutocompleteOptions<T>(
      displayStringForOption: displayStringForOption,
      onSelected: onSelected,
      options: options,
      maxOptionsHeight: optionsMaxHeight,
      maxOptionsWidth: optionsMaxWidth, // Added this line
    );
  },
  onSelected: onSelected,
);
}


// The default Material-style Autocomplete options.
 class _AutocompleteOptions<T extends Object> extends StatelessWidget {
  const _AutocompleteOptions({
  Key? key,
  required this.displayStringForOption,
  required this.onSelected,
  required this.options,
required this.maxOptionsHeight,
required this.maxOptionsWidth // Added this line
}) : super(key: key);

final AutocompleteOptionToString<T> displayStringForOption;

final AutocompleteOnSelected<T> onSelected;

final Iterable<T> options;
final double maxOptionsHeight;
final double maxOptionsWidth;

@override
Widget build(BuildContext context) {
return Align(
  alignment: Alignment.topLeft,
  child: Material(
    elevation: 4.0,
    child: ConstrainedBox(
      constraints: BoxConstraints(maxHeight: maxOptionsHeight, maxWidth: maxOptionsWidth), // Added maxWidth which was missing here
      child: ListView.builder(
        padding: EdgeInsets.zero,
        shrinkWrap: true,

一个固定宽度为285?它怎么能在所有情况下都工作? - Shahood ul Hassan

1

1

0
你尝试将整个自动完成小部件放入一个有大小的框中了吗?如果大小调整小部件无法正常工作,那可能是一个错误。

0
我只是像默认的构建器源代码一样操作,它正常工作。我认为出现错误是因为没有父级配置。
optionsViewBuilder: (context, onSelected, options) {
    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        elevation: 4.0,
        child: ConstrainedBox(
          constraints: BoxConstraints(maxHeight: 100.r),
          child: Container(
            width: 100.r,
            height: 100.r,
            decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(12.r),
                border: Border.all(
                  color: context.oldTheme.lightGrey,
                )),
            child: ListView.builder(
              itemBuilder: (context, index) {
                return Text(
                  options.toList()[index],
                  style: context.oldTheme.textTheme.body2,
                );
              },
              itemCount: options.length,
            ),
          ),
        ),
      ),
    );
  },

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接