在颤动中创建自定义下拉列表 - 或者如何将自定义下拉选项放在其他所有内容之上的图层中

use*_*307 4 drop-down-menu flutter flutter-layout

我正在寻找一种创建自定义下拉列表的方法,以便我可以自己设置样式。

我遇到了这个似乎非常有用的答案

/sf/answers/4421605541/

问题是如果容器小于下拉菜单,flutter 会抱怨像素溢出。我怎样才能让这个下拉菜单位于页面中其他元素的顶部,所以我没有收到这个警告?或者是否有另一种方法可以在没有此问题的情况下重新创建自定义下拉列表?

我找到的所有答案都是关于内置 DropdownButton

下面,上面链接的答案,带有版本


首先,创建一个名为 的 dart 文件drop_list_model.dart

import 'package:flutter/material.dart';

class DropListModel {
  DropListModel(this.listOptionItems);

  final List<OptionItem> listOptionItems;
}

class OptionItem {
  final String id;
  final String title;

  OptionItem({@required this.id, @required this.title});
}
Run Code Online (Sandbox Code Playgroud)

接下来,创建文件file select_drop_list.dart

import 'package:flutter/material.dart';
import 'package:time_keeping/model/drop_list_model.dart';
import 'package:time_keeping/widgets/src/core_internal.dart';

class SelectDropList extends StatefulWidget {
  final OptionItem itemSelected;
  final DropListModel dropListModel;
  final Function(OptionItem optionItem) onOptionSelected;

  SelectDropList(this.itemSelected, this.dropListModel, this.onOptionSelected);

  @override
  _SelectDropListState createState() => _SelectDropListState(itemSelected, dropListModel);
}

class _SelectDropListState extends State<SelectDropList> with SingleTickerProviderStateMixin {

  OptionItem optionItemSelected;
  final DropListModel dropListModel;

  AnimationController expandController;
  Animation<double> animation;

  bool isShow = false;

  _SelectDropListState(this.optionItemSelected, this.dropListModel);

  @override
  void initState() {
    super.initState();
    expandController = AnimationController(
        vsync: this,
        duration: Duration(milliseconds: 350)
    );
    animation = CurvedAnimation(
      parent: expandController,
      curve: Curves.fastOutSlowIn,
    );
    _runExpandCheck();
  }

  void _runExpandCheck() {
    if(isShow) {
      expandController.forward();
    } else {
      expandController.reverse();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          Container(
            padding: const EdgeInsets.symmetric(
                horizontal: 15, vertical: 17),
            decoration: new BoxDecoration(
              borderRadius: BorderRadius.circular(20.0),
              color: Colors.white,
              boxShadow: [
                BoxShadow(
                    blurRadius: 10,
                    color: Colors.black26,
                    offset: Offset(0, 2))
              ],
            ),
            child: new Row(
              mainAxisSize: MainAxisSize.max,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Icon(Icons.card_travel, color: Color(0xFF307DF1),),
                SizedBox(width: 10,),
              
                  child: GestureDetector(
                    onTap: () {
                      this.isShow = !this.isShow;
                      _runExpandCheck();
                      setState(() {

                      });
                    },
                    child: Text(optionItemSelected.title, style: TextStyle(
                        color: Color(0xFF307DF1),
                        fontSize: 16),),
                  ),
               
                Align(
                  alignment: Alignment(1, 0),
                  child: Icon(
                    isShow ? Icons.arrow_drop_down : Icons.arrow_right,
                    color: Color(0xFF307DF1),
                    size: 15,
                  ),
                ),
              ],
            ),
          ),
          SizeTransition(
              axisAlignment: 1.0,
              sizeFactor: animation,
              child: Container(
                margin: const EdgeInsets.only(bottom: 10),
                  padding: const EdgeInsets.only(bottom: 10),
                  decoration: new BoxDecoration(
                    borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
                    color: Colors.white,
                    boxShadow: [
                      BoxShadow(
                          blurRadius: 4,
                          color: Colors.black26,
                          offset: Offset(0, 4))
                    ],
                  ),
                  child: _buildDropListOptions(dropListModel.listOptionItems, context)
              )
          ),
//          Divider(color: Colors.grey.shade300, height: 1,)
        ],
      ),
    );
  }

  Column _buildDropListOptions(List<OptionItem> items, BuildContext context) {
    return Column(
      children: items.map((item) => _buildSubMenu(item, context)).toList(),
    );
  }

  Widget _buildSubMenu(OptionItem item, BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 26.0, top: 5, bottom: 5),
      child: GestureDetector(
        child: Row(
          children: <Widget>[
            
              child: Container(
                padding: const EdgeInsets.only(top: 20),
                decoration: BoxDecoration(
                  border: Border(top: BorderSide(color: Colors.grey[200], width: 1)),
                ),
                child: Text(item.title,
                    style: TextStyle(
                        color: Color(0xFF307DF1),
                        fontWeight: FontWeight.w400,
                        fontSize: 14),
                    maxLines: 3,
                    textAlign: TextAlign.start,
                    overflow: TextOverflow.ellipsis),
              ),
            
          ],
        ),
        onTap: () {
          this.optionItemSelected = item;
          isShow = false;
          expandController.reverse();
          widget.onOptionSelected(item);
        },
      ),
    );
  }

}
Run Code Online (Sandbox Code Playgroud)

初始化值:

DropListModel dropListModel = DropListModel([OptionItem(id: "1", title: "Option 1"), OptionItem(id: "2", title: "Option 2")]);
OptionItem optionItemSelected = OptionItem(id: null, title: "Ch?n quy?n truy c?p");
Run Code Online (Sandbox Code Playgroud)

最后使用它:

Container(height: 47, child: SelectDropList(
           this.optionItemSelected, 
           this.dropListModel, 
           (optionItem){
                 optionItemSelected = optionItem;
                    setState(() {
  
                    });
               },
            ))
Run Code Online (Sandbox Code Playgroud)

M12*_*123 12

下拉决定

我建议使用标准 Flutter 下拉菜单。因为它非常健壮,易于编写并且已经过尝试和测试。您说您想自己设计下拉菜单,我怀疑这就是您决定反对该标准的原因。但情况并非一定如此。标准的下拉菜单可以设计得很好。下面详细介绍一下

示例代码

  String dropdownValue = 'One';

  Widget build(BuildContext context) {
    return DropdownButton<String>(
      value: dropdownValue,
      icon: Icon(Icons.arrow_downward),
      iconSize: 24,
      elevation: 16,
      style: TextStyle(color: Colors.deepPurple),
      underline: Container(
        height: 2,
        color: Colors.deepPurpleAccent,
      ),
      onChanged: (String newValue) {
        setState(() {
          dropdownValue = newValue;
        });
      },
      items: <String>['One', 'Two', 'Free', 'Four']
          .map<DropdownMenuItem<String>>((String value) {
        return DropdownMenuItem<String>(
          value: value,
          child: Text(value),
        );
      }).toList(),
    );
Run Code Online (Sandbox Code Playgroud)

风格

DropdownMenuItem将遵循您的 ThemeData 类。它不仅backgroundColor会匹配 ThemeData 类中的 canvasColor,而且会遵循相同的TextStyle.

主题数据必须在 Material App 中初始化:

return MaterialApp(
    //....
    theme: new ThemeData(
            fontFamily: "Encode Sans", //my custom font
            canvasColor: _turquoise, //my custom color
            //other theme data
            ),
     //.....
),
Run Code Online (Sandbox Code Playgroud)

如果您不想或无法使用主题数据,这可能适合您。DropdownButton 类有一个名为的内置变量dropdownColor,可以直接分配您需要的任何颜色,而无需更改任何颜色ThemeData. 也会自动更改下拉菜单项的颜色。

例如,如果您想更改下拉列表中的 With,您可以为其子属性添加一个新属性Container并添加所需的width. 只需确保使用合适的宽度,以便稍后在更复杂的布局中使用菜单时不会出现溢出问题。我仍然建议将宽度保留为动态。

另外,它DropDownButton具有扩展能力,这意味着它占据了所有它能得到的空间

DropdownButton<String>(
   isExpanded: true,
)
Run Code Online (Sandbox Code Playgroud)


小智 7

按钮下方的自定义下拉菜单

我知道内置下拉菜单效果很好,但对于某些用例,我需要一些不同的东西。例如,如果我只有几个项目,我希望下拉菜单出现在按钮下方或完全控制下拉菜单的显示位置。我还没有找到一个好的选择,所以我试着自己做。我已经建立在@M123 在覆盖中提到的内容的基础上,并尝试以类似于内置下拉菜单的方式实现它。我发现这个来自开发人员的中等帖子flutter_typeahead非常有用。 https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9

自定义下拉列表的动画 gif

该按钮使用叠加创建全屏堆栈。这样我们就可以在下拉菜单后面添加一个全屏手势检测器,以便在用户点击屏幕上的任意位置时关闭它。

叠加层使用 aLayerLinkCompositedTransformFollower小部件链接到按钮。

我们还使用RenderBox renderBox = context.findRenderObject();来轻松获取按钮的位置和大小。然后相应地定位下拉菜单。

下拉文件

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';

class CustomDropdown<T> extends StatefulWidget {
  /// the child widget for the button, this will be ignored if text is supplied
  final Widget child;

  /// onChange is called when the selected option is changed.;
  /// It will pass back the value and the index of the option.
  final void Function(T, int) onChange;

  /// list of DropdownItems
  final List<DropdownItem<T>> items;
  final DropdownStyle dropdownStyle;

  /// dropdownButtonStyles passes styles to OutlineButton.styleFrom()
  final DropdownButtonStyle dropdownButtonStyle;

  /// dropdown button icon defaults to caret
  final Icon icon;
  final bool hideIcon;

  /// if true the dropdown icon will as a leading icon, default to false
  final bool leadingIcon;
  CustomDropdown({
    Key key,
    this.hideIcon = false,
    @required this.child,
    @required this.items,
    this.dropdownStyle = const DropdownStyle(),
    this.dropdownButtonStyle = const DropdownButtonStyle(),
    this.icon,
    this.leadingIcon = false,
    this.onChange,
  }) : super(key: key);

  @override
  _CustomDropdownState<T> createState() => _CustomDropdownState<T>();
}

class _CustomDropdownState<T> extends State<CustomDropdown<T>>
    with TickerProviderStateMixin {
  final LayerLink _layerLink = LayerLink();
  OverlayEntry _overlayEntry;
  bool _isOpen = false;
  int _currentIndex = -1;
  AnimationController _animationController;
  Animation<double> _expandAnimation;
  Animation<double> _rotateAnimation;

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

    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 200));
    _expandAnimation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    );
    _rotateAnimation = Tween(begin: 0.0, end: 0.5).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ));
  }

  @override
  Widget build(BuildContext context) {
    var style = widget.dropdownButtonStyle;
    // link the overlay to the button
    return CompositedTransformTarget(
      link: this._layerLink,
      child: Container(
        width: style.width,
        height: style.height,
        child: OutlinedButton(
          style: OutlinedButton.styleFrom(
            padding: style.padding,
            backgroundColor: style.backgroundColor,
            elevation: style.elevation,
            primary: style.primaryColor,
            shape: style.shape,
          ),
          onPressed: _toggleDropdown,
          child: Row(
            mainAxisAlignment:
                style.mainAxisAlignment ?? MainAxisAlignment.center,
            textDirection:
                widget.leadingIcon ? TextDirection.rtl : TextDirection.ltr,
            mainAxisSize: MainAxisSize.min,
            children: [
              if (_currentIndex == -1) ...[
                widget.child,
              ] else ...[
                widget.items[_currentIndex],
              ],
              if (!widget.hideIcon)
                RotationTransition(
                  turns: _rotateAnimation,
                  child: widget.icon ?? Icon(FontAwesomeIcons.caretDown),
                ),
            ],
          ),
        ),
      ),
    );
  }

  OverlayEntry _createOverlayEntry() {
    // find the size and position of the current widget
    RenderBox renderBox = context.findRenderObject();
    var size = renderBox.size;
    
    var offset = renderBox.localToGlobal(Offset.zero);
    var topOffset = offset.dy + size.height + 5;
    return OverlayEntry(
      // full screen GestureDetector to register when a
      // user has clicked away from the dropdown
      builder: (context) => GestureDetector(
        onTap: () => _toggleDropdown(close: true),
        behavior: HitTestBehavior.translucent,
        // full screen container to register taps anywhere and close drop down
        child: Container(
          height: MediaQuery.of(context).size.height,
          width: MediaQuery.of(context).size.width,
          child: Stack(
            children: [
              Positioned(
                left: offset.dx,
                top: topOffset,
                width: widget.dropdownStyle.width ?? size.width,
                child: CompositedTransformFollower(
                  offset:
                      widget.dropdownStyle.offset ?? Offset(0, size.height + 5),
                  link: this._layerLink,
                  showWhenUnlinked: false,
                  child: Material(
                    elevation: widget.dropdownStyle.elevation ?? 0,
                    borderRadius:
                        widget.dropdownStyle.borderRadius ?? BorderRadius.zero,
                    color: widget.dropdownStyle.color,
                    child: SizeTransition(
                      axisAlignment: 1,
                      sizeFactor: _expandAnimation,
                      child: ConstrainedBox(
                        constraints: widget.dropdownStyle.constraints ??
                            BoxConstraints(
                              maxHeight: MediaQuery.of(context).size.height -
                                  topOffset -
                                  15,
                            ),
                        child: ListView(
                          padding:
                              widget.dropdownStyle.padding ?? EdgeInsets.zero,
                          shrinkWrap: true,
                          children: widget.items.asMap().entries.map((item) {
                            return InkWell(
                              onTap: () {
                                setState(() => _currentIndex = item.key);
                                widget.onChange(item.value.value, item.key);
                                _toggleDropdown();
                              },
                              child: item.value,
                            );
                          }).toList(),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _toggleDropdown({bool close = false}) async {
    if (_isOpen || close) {
      await _animationController.reverse();
      this._overlayEntry.remove();
      setState(() {
        _isOpen = false;
      });
    } else {
      this._overlayEntry = this._createOverlayEntry();
      Overlay.of(context).insert(this._overlayEntry);
      setState(() => _isOpen = true);
      _animationController.forward();
    }
  }
}

/// DropdownItem is just a wrapper for each child in the dropdown list.\n
/// It holds the value of the item.
class DropdownItem<T> extends StatelessWidget {
  final T value;
  final Widget child;

  const DropdownItem({Key key, this.value, this.child}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return child;
  }
}

class DropdownButtonStyle {
  final MainAxisAlignment mainAxisAlignment;
  final ShapeBorder shape;
  final double elevation;
  final Color backgroundColor;
  final EdgeInsets padding;
  final BoxConstraints constraints;
  final double width;
  final double height;
  final Color primaryColor;
  const DropdownButtonStyle({
    this.mainAxisAlignment,
    this.backgroundColor,
    this.primaryColor,
    this.constraints,
    this.height,
    this.width,
    this.elevation,
    this.padding,
    this.shape,
  });
}

class DropdownStyle {
  final BorderRadius borderRadius;
  final double elevation;
  final Color color;
  final EdgeInsets padding;
  final BoxConstraints constraints;

  /// position of the top left of the dropdown relative to the top left of the button
  final Offset offset;

  ///button width must be set for this to take effect
  final double width;

  const DropdownStyle({
    this.constraints,
    this.offset,
    this.width,
    this.elevation,
    this.color,
    this.padding,
    this.borderRadius,
  });
}
Run Code Online (Sandbox Code Playgroud)

使用下拉菜单

我尝试使用类似于内置下拉菜单的自定义下拉菜单,并增加了能够设置实际下拉菜单和按钮样式的额外好处。

Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: CustomDropdown<int>(
        child: Text(
          'dropdown',
        ),
        onChange: (int value, int index) => print(value),
        dropdownButtonStyle: DropdownButtonStyle(
          width: 170,
          height: 40,
          elevation: 1,
          backgroundColor: Colors.white,
          primaryColor: Colors.black87,
        ),
        dropdownStyle: DropdownStyle(
          borderRadius: BorderRadius.circular(8),
          elevation: 6,
          padding: EdgeInsets.all(5),
        ),
        items: [
          'item 1',
          'item 2',
          'item 3',
          'item 4',
        ]
            .asMap()
            .entries
            .map(
              (item) => DropdownItem<int>(
            value: item.key + 1,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(item.value),
            ),
          ),
        )
            .toList(),
      ),
    ),
  );
}

Run Code Online (Sandbox Code Playgroud)

我相信那里会有一些需要改进的地方。但目前它对我有用。


M12*_*123 6

我找到了一种使用 Overlay 构建自定义下拉菜单的新方法。

文件

通过将独立的子窗口小部件插入到叠加层的堆栈中,叠加层可以让独立的子窗口小部件将视觉元素“浮动”在其他窗口小部件的顶部。覆盖层允许每个小部件使用 OverlayEntry 对象管理它们在覆盖层中的参与。

这为您提供了所有的设计自由,因为每种类型的孩子都是允许的。如何移动我在代码中作为注释编写的 DropDown。

这是一个小示例,说明如何使用它。

OverlayEntry floatingDropdown;


AnyButton(
   //...
   onTap: () {
        setState(() {
          if (isDropdownOpened) {
            floatingDropdown.remove();
          } else {
            findDropdownData();
            floatingDropdown = _createFloatingDropdown();
            Overlay.of(context).insert(floatingDropdown);
          }

          isDropdownOpened = !isDropdownOpened;
        });
      },
);




  OverlayEntry _createFloatingDropdown() {
    return OverlayEntry(builder: (context) {
      return Positioned(
        // You can change the position here
        left: xPosition,
        width: width,
        top: yPosition + height,
        height: 4 * height + 40,
        // Any child
        child: Container(
          color: Colors.black,
          height: height,
          child: Text('Hallo'),
        ),
      );
    });
  }
Run Code Online (Sandbox Code Playgroud)

可以在此处找到完整设计的示例。