Flutter第十四章(PopupMenuButton,DropdownButton,拓展PopupMenuDivider ,拓展DropdownButton)

版权声明:本文为作者原创书籍。转载请注明作者和出处,未经授权,严禁私自转载,侵权必究!!!

情感语录: 自己想要的东西,要么奋力直追,要么干脆放弃。别总是逢人就喋喋不休的表决心或者哀怨不断,做别人茶余饭后的笑点

欢迎来到本章节,上一章节介绍了数据库的使用,知识点回顾 戳这里 Flutter基础第十三章

前面章节介绍了有关弹窗的(Dialog)的使用,Dialog在发开中能解决大部分的弹出选择类问题,但是有些特殊的定位弹窗场景就不太友好了,本章主要介绍另外一种弹窗 PopupMenuButton和DropdownButton 它们的效果如同Android 原生中的 PopWindow 一样。

本章简要:

1、PopupMenuButton组件

2、DropdownButton组件

3、拓展 PopupMenuDivider组件

4、拓展 DropdownButton组件

一、PopupMenuButton 组件

PopupMenuButton 常和 PopupMenuItem 、 PopupMenuEntry 或者 继承自 PopupMenuEntry 的子类使用。

构造函数:

  const PopupMenuButton({
    Key key,
    @required this.itemBuilder,
    this.initialValue,
    this.onSelected,
    this.onCanceled,
    this.tooltip,
    this.elevation = 8.0,
    this.padding = const EdgeInsets.all(8.0),
    this.child,
    this.icon,
    this.offset = Offset.zero,
    this.enabled = true,
  }) 
常用属性介绍:
    属性                    描述

    itemBuilder            item子项

    initialValue           初始值

    onSelected             选中时的回调

    onCanceled             取消时回调

    tooltip                提示

    elevation              阴影

    child                  子控件,不能和icon都设置

    icon                   IconButton子控件, 不能和child都设置

    offset                 可设置控件偏移位置

    enabled                是否启用(false 表示不能点击)

简单运用:

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_learn/util/ToastUtil.dart';
    
    class PopViewPage extends StatefulWidget {
      PopViewPage({Key key}) : super(key: key);
    
      _PopViewPageState createState() => _PopViewPageState();
    }
    
    class _PopViewPageState extends State<PopViewPage> {

      @override
      Widget build(BuildContext context) {
 
        return Scaffold(
            appBar: AppBar(
              title: Text("PopViewPage"),
              actions: <Widget>[
                _NormalPopMenu(),
              ],
            ),
            body: Text(""));
    
      }
    
      Widget _NormalPopMenu() {
        return PopupMenuButton<String>(
            icon: Icon(Icons.settings),
            itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
                  PopupMenuItem<String>(value: 'Item01', child: Text('Item One')),
                  PopupMenuItem<String>(value: 'Item02', child: Text('Item Two')),
                  PopupMenuItem<String>(value: 'Item03', child: Text('Item Three')),
                  PopupMenuItem<String>(value: 'Item04', child: Text('Item Four'))
                ],
            onSelected: (String value) {
              ToastUtil.show(value.toString());
            });
      }
    }

效果如下:

PopupMenuButton.gif

可以看到点击设置按钮后弹出了一个 pop 窗口,选择某一行后也能接受到选择的信息;但有一个不好的地方弹出窗口后我们的菜单按钮都被遮挡了,它直接在屏幕的最右顶端就显示了,这种体验并不好,我期望的是它在该按钮下面显示出来,或者是在我们的导航栏下面显示。如果要在导航栏下显示,肯定就要将 pop 窗口往下移动一段距离,而移动多少距离合适呢?可以看出这段距离就是 AppBar的高度,当然这还需要考虑到横竖屏甚至是平板上的AppBar 高度都不一致的情况,下面追溯到 AppBar 中去看看:

    // TODO(eseidel): Toolbar needs to change size based on orientation:
    // https://material.io/design/components/app-bars-top.html#specs
    // Mobile Landscape: 48dp
    // Mobile Portrait: 56dp
    // Tablet/Desktop: 64dp

在 AppBar 中看到有这么一段描述:横屏 AppBar 高度为 48dp,竖屏 AppBar 高度为 56dp,平板的 AppBar 为64dp。为了同时适用于手机和平板那就需要将 pop 窗口往下偏移64个单位。下面来试试:

  Widget _NormalPopMenu() {
    return PopupMenuButton<String>(
        //dy 方向向下移动64个单位
        offset: Offset(0, 64),
        icon: Icon(Icons.settings),
        itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
              PopupMenuItem<String>(value: 'Item01', child: Text('Item One')),
              PopupMenuItem<String>(value: 'Item02', child: Text('Item Two')),
              PopupMenuItem<String>(value: 'Item03', child: Text('Item Three')),
              PopupMenuItem<String>(value: 'Item04', child: Text('Item Four'))
            ],
        onSelected: (String value) {
          ToastUtil.show(value.toString());
        });
  }

效果如下:

PopupMenuButton_Offset.gif

可以看到 pop 弹出后确实在 AppBar 下面了,说明Offset 设置的补偿值生效了,而这种效果应该才是开发中常见的吧!

PopupMenuButton 还可以配置一些简单的样式,如分割线,是否选中等。下面结合 PopupMenuDivider 和 CheckedPopupMenuItem组件使用下。

   //添加分割线和选中样式
   Widget _DividerPopMenu() {
    return PopupMenuButton<String>(
        offset: Offset(0, 64),
        itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
              PopupMenuItem<String>(
                  value: 'Item01',
                  child: CheckedPopupMenuItem<String>(
                      checked: false, value: 'Item01', child: Text('Item One'))),
              PopupMenuDivider(height: 1.0),
              PopupMenuItem<String>(
                  value: 'Item02',
                  child: CheckedPopupMenuItem<String>(
                      checked: true, value: 'Item02', child: Text('Item Two'))),
              PopupMenuDivider(height: 1.0),
              PopupMenuItem<String>(
                  value: 'Item03',
                  child: CheckedPopupMenuItem<String>(
                      checked: false,
                      value: 'Item03',
                      child: Text('Item Three'))),
              PopupMenuDivider(height: 1.0),
              PopupMenuItem<String>(
                  value: 'Item04',
                  child: CheckedPopupMenuItem<String>(
                      checked: false,
                      value: 'Item04',
                      child: Text('Item Four')))
            ],
        onSelected: (String value) {
          ToastUtil.show(value.toString());
        });
  }

效果如下:

PopupMenuButton_divider.gif

模拟器效果有点差哈,分割线截图后看不到了;真机上不会存在这样的问题。但可以看到配置的默认选中项是生效了的。Flutter 中提供的 PopupMenuDivider 和 CheckedPopupMenuItem 两个控件还是不够人性化。PopupMenuDivider 只能设置高度,不可以设置颜色... ;CheckedPopupMenuItem 显示的图标和文字之间的间距又那么大,还不能修改。总的来说效果并不理想,现在怎么解决上面的这些问题呢? 首先要解决 PopupMenuDivider 可以自定义分割线颜色问题,其次解决 CheckedPopupMenuItem 中的一些间距问题。

自定义 PopupMenuDivider

要自定义 PopupMenuDivider 首先得看看它内部是怎么实现的!! 跟进源码内部,发现 PopupMenuDivider 代码极其简单,且内部也是再构建的一个 Divider 组件 ,如下:

自定义divider.png

而 Divider 组件正好可以设置分割线颜色。真是太好了,看来这个自定义就没什么难度了;直接依葫芦画瓢即可。

新建一个 ExpandPopupMenuDivider 类 同样也去继承 PopupMenuEntry 类, 然后添加我们的颜色属性。

    import 'package:flutter/material.dart';

    const double _kMenuDividerHeight = 16.0;

    class ExpandPopupMenuDivider<T> extends PopupMenuEntry<T> {
      /// Creates a horizontal divider for a popup menu.
      ///
      /// By default, the divider has a height of 16 logical pixels.
      const ExpandPopupMenuDivider({
        Key key,
        this.height = _kMenuDividerHeight,
        this.color,
      })
          : super(key: key);

      /// The height of the divider entry.
      ///
      /// Defaults to 16 pixels.
      final double height;

      final Color color;

      @override
      bool represents(void value) => false;

    @override
    _ExpandPopupMenuDividerState createState() => _ExpandPopupMenuDividerState(height,color);
    }

    class _ExpandPopupMenuDividerState extends State<ExpandPopupMenuDivider> {

       double height;

       Color color;

      _ExpandPopupMenuDividerState(this.height,this.color);

      @override
      Widget build(BuildContext context) => Divider(height: height,color: color);
    }

这样分割线颜色问题解决掉O(∩_∩)O~ ,间距问题就更简单了 因为 PopupMenuItem 中 child 属性接收的是一个 Widget ,那自然可以换成其他布局方式了,如:ListTile 、Row 等,为了灵活性这里使用 Row 布局方式,下面来看自定义一个效果。

  //自定义分割线 和 Item 样式
  Widget _CustomPopMenu() {
    return PopupMenuButton<String>(
        offset: Offset(0, 64),
        itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
              PopupMenuItem<String>(
                  value: 'Item01',
                  child: Row(children: <Widget>[
                    Padding( padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
                        child: Icon(Icons.search,color: Colors.blueAccent)),
                    Text('Item One')
                  ]),),
              ExpandPopupMenuDivider<String>(height: 2.0,color: Colors.red),
              PopupMenuItem<String>(
                  value: 'Item02',
                  child: Row(children: <Widget>[
                    Padding( padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
                        child: Icon(Icons.home,color: Colors.blueAccent)),
                    Text('Item Two')
                  ])),
          ExpandPopupMenuDivider<String>(height: 2.0,color: Colors.red),
              PopupMenuItem<String>(
                  value: 'Item03',
                  child: Row(children: <Widget>[
                    Padding( padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
                        child: Icon(Icons.person,color: Colors.blueAccent)),
                    Text('Item Three')
                  ])),
          ExpandPopupMenuDivider<String>(height: 2.0,color: Colors.red),
              PopupMenuItem<String>(
                  value: 'Item04',
                  child: Row(children: <Widget>[
                    Padding( padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
                        child: Icon(Icons.local_grocery_store,color: Colors.blueAccent)),
                    Text('Item Four')
                  ]))
            ],
        onSelected: (String value) {
          ToastUtil.show(value.toString());
        });
  }

效果如下:

pop自定义样式.gif

完美,O(∩_∩)O哈哈~,模拟器分割线颜色看起来不均匀(真机上不会有该问题)

PopupMenu 默认的弹框位置都是在右上角,如果有需要在其他位置弹框就需要借助 showMenu,通过 position 属性定位弹框,我觉得Flutter 中的提供的这种方式并没很好的支持,弹出的位置会根据内容的长度也会有变化。它并没原生中的 PopWind 通过参照物 View 实现定位的方式好用。

    var raisedButton = RaisedButton(
            child: Text('Pop弹出窗'),
            onPressed: () {
              showMenu(
                  context: context,
                  position: RelativeRect.fromSize(Rect.fromLTRB(150, 130, 0, 0),Size(100, 200)),
                  elevation: 10,
                  items: <PopupMenuItem<String>>[
                    PopupMenuItem<String>(value: 'Item01', child: Text('Item One'),
                    ),
                    PopupMenuItem<String>(value: 'Item02', child: Text('Item Two')),
                    PopupMenuItem<String>(
                        value: 'Item03', child: Text('Item Three')),
                    PopupMenuItem<String>(value: 'Item04', child: Text('Item Four'))
                  ]).then((value) {
                if (null == value) {
                  return;
                }
                ToastUtil.show(value.toString());
              });
            });

效果如下:

showMenu.gif

showMenu 这里提一下就好了,因为我觉得它是真心不好用,如果你有更多好的玩法可以留言讨论哦O(∩_∩)O哈哈~

二、DropdownButton 组件

DropdownButton 也可以实现一个弹窗效果,一些简单的点击选择 业务场景使用它的情况相对较多。它常常和 DropdownMenuItem 配合一起使用。

构造函数:

  DropdownButton({
    Key key,
    @required this.items,
    this.value,
    this.hint,
    this.disabledHint,
    @required this.onChanged,
    this.elevation = 8,
    this.style,
    this.underline,
    this.icon,
    this.iconDisabledColor,
    this.iconEnabledColor,
    this.iconSize = 24.0,
    this.isDense = false,
    this.isExpanded = false,
  })
常用属性
    属性            描述

    items                下拉列表 Item

    value                当前选中的值

    hint                 value = null 时显示

    disabledHint         禁用时提示

    onChanged            选项发生变化时回调

    elevation            阴影值

    style                字体样式

    icon                 右侧图标

    iconSize             右侧图标大小

    isDense              是否添加稠密效果,false会添加 8个单位值的距离

    isExpanded           是否填充父组件
    
    iconEnabledColor     启用时图标的颜色
    
    iconDisabledColor    未启用时图标的颜色
    
    underline            底部下划线

简单运用:

   //使用一个父组件包裹  DropdownButton
          Container(
              width: 100,
              child: DropdownButton<String>(
                value: content,
                //填充父组件
                isExpanded: true,
                iconEnabledColor: Colors.yellow,
                //添加下划线
                underline: Container(
                  color: Colors.red,
                  height: 1,
                ),
                //监听改变的值
                onChanged: (String value) {
                  setState(() {
                    content = value;
                  });
                },
                items: <String>["One", 'Two', 'Free', 'Four']
                    .map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              )),

效果如下:

DropdownButton.gif

这段代码是直接从官方例子中拿出来简单修改了下,可以看到 DropdownButton 确实能实现一个简单的弹窗效果。但是有个问题你肯定发现了,在我每次选择不同 Item 后,DropdownButton 弹出的位置都发生了变化......

很显然这种效果是我们大多数情况不能接受的,我期望的是每次弹出总是在 下划线以下显示。面对这样的需求那该怎么办呢? 一顿源码狂找,发现Flutter 中并没有提供相应的解决方案......

分析问题,每次点击不同的 Item 后弹出的位置都在变化,点击同一 Item 时弹出的位置则在上一次的位置,很明显内部在维护一个选中时的索引值,然后再通过选中的索引值后计算显示的位置。

带着疑问进入 DropdownButton 内部源码,发现 DropdownButton 继承的是 StatefulWidget 组件,既然继承的是 StatefulWidget 那直接跳转到 createState()方法去查看 它创建了一个什么 State!!!!

发现它创建了 _DropdownButtonState ,其内部源码如下:

    class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindingObserver {
      int _selectedIndex;
      _DropdownRoute<T> _dropdownRoute;

      @override
      void initState() {
        super.initState();
        _updateSelectedIndex();
        WidgetsBinding.instance.addObserver(this);
      }

      @override
      void dispose() {
        WidgetsBinding.instance.removeObserver(this);
        _removeDropdownRoute();
        super.dispose();
      }

      // Typically called because the device's orientation has changed.
      // Defined by WidgetsBindingObserver
      @override
      void didChangeMetrics() {
        _removeDropdownRoute();
      }

      void _removeDropdownRoute() {
        _dropdownRoute?._dismiss();
        _dropdownRoute = null;
      }

      @override
      void didUpdateWidget(DropdownButton<T> oldWidget) {
        super.didUpdateWidget(oldWidget);
        _updateSelectedIndex();
      }

      void _updateSelectedIndex() {
        if (!_enabled) {
          return;
        }

        assert(widget.value == null ||
          widget.items.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1);
        _selectedIndex = null;
        for (int itemIndex = 0; itemIndex < widget.items.length; itemIndex++) {
          if (widget.items[itemIndex].value == widget.value) {
            _selectedIndex = itemIndex;
            return;
          }
        }
      }

      TextStyle get _textStyle => widget.style ?? Theme.of(context).textTheme.subhead;

      void _handleTap() {
        final RenderBox itemBox = context.findRenderObject();
        final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
        final TextDirection textDirection = Directionality.of(context);
        final EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown
          ?_kAlignedMenuMargin
          : _kUnalignedMenuMargin;

        assert(_dropdownRoute == null);
        _dropdownRoute = _DropdownRoute<T>(
          items: widget.items,
          buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
          padding: _kMenuItemPadding.resolve(textDirection),
          selectedIndex: _selectedIndex ?? 0,
          elevation: widget.elevation,
          theme: Theme.of(context, shadowThemeOnly: true),
          style: _textStyle,
          barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
        );

        Navigator.push(context, _dropdownRoute).then<void>((_DropdownRouteResult<T> newValue) {
          _dropdownRoute = null;
          if (!mounted || newValue == null)
            return;
          if (widget.onChanged != null)
            widget.onChanged(newValue.result);
        });
      }

      // When isDense is true, reduce the height of this button from _kMenuItemHeight to
      // _kDenseButtonHeight, but don't make it smaller than the text that it contains.
      // Similarly, we don't reduce the height of the button so much that its icon
      // would be clipped.
      double get _denseButtonHeight {
        final double fontSize = _textStyle.fontSize ?? Theme.of(context).textTheme.subhead.fontSize;
        return math.max(fontSize, math.max(widget.iconSize, _kDenseButtonHeight));
      }

      Color get _iconColor {
        // These colors are not defined in the Material Design spec.
        if (_enabled) {
          if (widget.iconEnabledColor != null) {
            return widget.iconEnabledColor;
          }

          switch(Theme.of(context).brightness) {
            case Brightness.light:
              return Colors.grey.shade700;
            case Brightness.dark:
              return Colors.white70;
          }
        } else {
          if (widget.iconDisabledColor != null) {
            return widget.iconDisabledColor;
          }

          switch(Theme.of(context).brightness) {
            case Brightness.light:
              return Colors.grey.shade400;
            case Brightness.dark:
              return Colors.white10;
          }
        }

        assert(false);
        return null;
      }

      bool get _enabled => widget.items != null && widget.items.isNotEmpty && widget.onChanged != null;

      @override
      Widget build(BuildContext context) {
        assert(debugCheckHasMaterial(context));
        assert(debugCheckHasMaterialLocalizations(context));

        // The width of the button and the menu are defined by the widest
        // item and the width of the hint.
        final List<Widget> items = _enabled ? List<Widget>.from(widget.items) : <Widget>[];
        int hintIndex;
        if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
          final Widget emplacedHint =
            _enabled ? widget.hint : DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint);
          hintIndex = items.length;
          items.add(DefaultTextStyle(
            style: _textStyle.copyWith(color: Theme.of(context).hintColor),
            child: IgnorePointer(
              child: emplacedHint,
              ignoringSemantics: false,
            ),
          ));
        }

        final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
          ? _kAlignedButtonPadding
          : _kUnalignedButtonPadding;

        // If value is null (then _selectedIndex is null) or if disabled then we
        // display the hint or nothing at all.
        final int index = _enabled ? (_selectedIndex ?? hintIndex) : hintIndex;
        Widget innerItemsWidget;
        if (items.isEmpty) {
          innerItemsWidget = Container();
        } else {
          innerItemsWidget = IndexedStack(
            index: index,
            alignment: AlignmentDirectional.centerStart,
            children: items,
          );
        }

        const Icon defaultIcon = Icon(Icons.arrow_drop_down);

        Widget result = DefaultTextStyle(
          style: _textStyle,
          child: Container(
            padding: padding.resolve(Directionality.of(context)),
            height: widget.isDense ? _denseButtonHeight : null,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                widget.isExpanded ? Expanded(child: innerItemsWidget) : innerItemsWidget,
                IconTheme(
                  data: IconThemeData(
                    color: _iconColor,
                    size: widget.iconSize,
                  ),
                  child: widget.icon ?? defaultIcon,
                ),
              ],
            ),
          ),
        );

        if (!DropdownButtonHideUnderline.at(context)) {
          final double bottom = widget.isDense ? 0.0 : 8.0;
          result = Stack(
            children: <Widget>[
              result,
              Positioned(
                left: 0.0,
                right: 0.0,
                bottom: bottom,
                child: widget.underline ?? Container(
                  height: 1.0,
                  decoration: const BoxDecoration(
                    border: Border(bottom: BorderSide(color: Color(0xFFBDBDBD), width: 0.0))
                  ),
                ),
              ),
            ],
          );
        }

        return Semantics(
          button: true,
          child: GestureDetector(
            onTap: _enabled ? _handleTap : null,
            behavior: HitTestBehavior.opaque,
            child: result,
          ),
        );
      }
    }

源码较多,下面挑关键的说,首先 看到 内部维护两个属性:

  int _selectedIndex;
  _DropdownRoute<T> _dropdownRoute;

果不其然 发现内部有一个 选中时的索引值 _selectedIndex,而选中时 对这个 _selectedIndex 做了些什么工作呢? 如下:

dropdown源码分析.png

发现点击选中后 将 _selectedIndex 传入了 _DropdownRoute 中。那持续跟进。

    class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
      _DropdownRoute({
        this.items,
        this.padding,
        this.buttonRect,
        this.selectedIndex,
        this.elevation = 8,
        this.theme,
        @required this.style,
        this.barrierLabel,
      }) : assert(style != null);

           .............. 省略部分代码
      final int selectedIndex;


           ..............省略部分代码
      @override
      Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
        return LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            return _DropdownRoutePage<T>(

              ..............省略部分代码

              selectedIndex: selectedIndex,
              elevation: elevation,
              theme: theme,
              style: style,
            );
          }
        );
      }

      void _dismiss() {
        navigator?.removeRoute(this);
      }
    }

_DropdownRoute 在构建页面时 又将 selectedIndex 传入了 _DropdownRoutePage 中。层次这么深,没办法继续跟进 _DropdownRoutePage 中。在 build 方法中终于看到了 selectedIndex 的计算方式,如下:

dropdown源码分析2.png

selectedItemOffset 就是 item 的偏移量,也就是说只要改掉这个值 就能改变弹出的位置。那具体该怎么改呢? 我想要放在 DropdownButton 底部显示,并且想可以控制它和DropdownButton 之间的间距。 首先肯定的拿到 DropdownButton 的高度,其中正好有一个对象 buttonRect 就能拿到 DropdownButton 在屏幕上位置的高度 ,而间距的大小最好是从外部能够传入。最后得出以下公式计算:

 selectedItemOffset = (buttonRect.height + marginTop) * -1;

这里为啥要 乘 -1 呢 ? 经过一轮测试发现 selectedItemOffset 的值大于等于 0 弹出窗都会显示在 DropdownButton 之上,乘 -2 又在底部太远,-1 刚好合适。

内部源码我们不能修改,因此也只能依葫芦画瓢的再造一个 DropdownButton 组件,新建 ExpandDropdownButton 类,其它基本直接复制进来,主要修改上面的计算公式:

    double selectedItemOffset;
    
    if(alwaysBottom){
      selectedItemOffset = (buttonRect.height + marginTop) * -1;
    }else{
      selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
    }

为了保证原来的算方式也可以使用,这里添加了一个 alwaysBottom 属性 在外部控制弹出方式。因为添加了属性内部源码修改的地方还是相对较多这里就不贴出来了,感兴趣的可以见文末源码。

ExpandDropdownButton 中的拓展属性:

    属性                          描述

    alwaysBottom                  是否总是在底部显示,false 为Flutter 自带方式

    marginTop                     弹出窗距离button 距离

    drawPadding                   和右侧图标的内距值(使用该属性尽量不要把父容器限制宽度)

下面来使用下拓展后的 ExpandDropdownButton 组件.

  Container(
      child: ExpandDropdownButton<String>(
        value: content1,

        // 总是在底部显示
        alwaysBottom: true,
        //距离 button 10个单位间距
        marginTop: 10,
        //文字和图标内距40个单位
        drawPadding: 40,
        icon: Icon(
          Icons.arrow_drop_down,
          size: 20,
        ),
        iconDisabledColor: Colors.blue,
        iconEnabledColor: Colors.yellow,
        underline: Container(
          color: Colors.red,
          height: 1,
        ),
        onChanged: (value) {
          setState(() {
            content1 = value;
                           ToastUtil.show(content);
          });
    },
    items: <String>["One", 'Two', 'Free', 'Four']
        .map<DropdownMenuItem<String>>((String value) {
      return DropdownMenuItem<String>(
        value: value,
        child: Text(value),
      );
    }).toList(),
  )),

效果如下:

ExpandDropdownButton.gif

拓展后的 ExpandDropdownButton 能满足绝大部分的场景需求了,如果还有别的要求就需要你自己去拓展了。总的来说效果还是比较给力O(∩_∩)O

至此 Flutter 中的有关弹窗的组件就全部介绍完了,根据业务需求选择适当的弹窗 (Dialog 或者本章介绍的Pop)才是合理利用。

实例源码地址:https://github.com/zhengzaihong/flutter_learn/blob/master/lib/page/pop/PopViewPage.dart

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容