Flutter DropdownButton - 添加用于分隔项目的标题

Flutter DropdownButton - Add title for separating items

我正在尝试实现一个 DropdownButton 可以用标题将特定项目彼此分开(请参见下图中所需的输出,标题为绿色)

就像在示例中一样,我想将这些绿色标题添加到我现有的 DropdownButton,但没有找到任何东西告诉我如何向 DropdownButton[= 添加项目以外的内容20=]

我还尝试使用 ExpansionTileDropdownButton 切换到 ListView,但我想保持 DropdownButton...[=20 的行为=]

是否可以将这些标题添加到 DropdownButton items?还是我应该尝试通过其他方式实现它?我现在卡住了,不知道如何继续

在 dartpad 中尝试此代码:

 import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      
      home: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomeScreen();
  }
}

class HomeScreen extends StatefulWidget {
  @override
  HomeScreenState createState() => HomeScreenState();
}

class HomeScreenState extends State<HomeScreen> {
  String dropdownValue;
  List<Product> products = [
    Product(name: 'sep1', type: 'sep'),
    Product(name: 'milk', type: 'data'),
    Product(name: 'oil', type: 'data'),
    Product(name: 'sep2', type: 'sep'),
    Product(name: 'suger', type: 'data'),
    Product(name: 'salt', type: 'data'),
    Product(name: 'sep3', type: 'sep'),
    Product(name: 'potatoe', type: 'data'),
    Product(name: 'tomatoe', type: 'data'),
    Product(name: 'apple', type: 'data'),
  ];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('text')),
      body: Column(
        children: [
          Text('test'),
          Expanded(
            child: DropdownButton<String>(
              value: dropdownValue,
              items: products.map((value) {
                return DropdownMenuItem(
                  value: value.name,
                  child: value.type == 'data'
                      ? Text(value.name)
                      : Divider(
                          color: Colors.red,
                          thickness: 3,
                        ),
                );
              }).toList(),
              onChanged: (newValue) {
                
                setState(() {
                  
                    dropdownValue = newValue;
                  
                });
                print('$newValue $dropdownValue');
                
              },
            ),
          ),
        ],
      ),
    );
  }
}

class Product {
  String name;
  String type;

  Product({this.name, this.type});
}

除了 Ali Tenni 的回答之外,您还可以像这样对 DropdownMenuItem 进行子类化:

class DropdownMenuItemSeparator<T> extends DropdownMenuItem<T> {
  DropdownMenuItemSeparator() : super(
    enabled: false,     // As of Flutter 2.5.
    child: Container(), // Trick the assertion.
  ); 

  @override
  Widget build(BuildContext context) {
    return Divider(thickness: 3);
  }
}

这将使分隔符更窄,否则默认 build() 会增加一些最小高度。

UPD 2021-09-14 以下不再相关,因为该功能已在 Flutter 2.5 中实现。上面的代码相应更新。

这解决了两个问题之一。不幸的是,第二个也是更大的问题是分隔符仍然可以点击,并且点击会关闭下拉菜单。为了解决这个问题,我向 Flutter 提交了一个功能请求: https://github.com/flutter/flutter/issues/75487

这是我在一个封闭问题中的代码:

已接受的答案更改的 y 位置。我不明白为什么它是公认的答案。

import 'package:flutter/material.dart';

class CustomDropDownMenu extends StatefulWidget {
  final Widget child;

  const CustomDropDownMenu({Key? key, required this.child}) : super(key: key);

  @override
  _CustomDropDownMenu createState() => _CustomDropDownMenu();
}

class _CustomDropDownMenu extends State<CustomDropDownMenu>
    with SingleTickerProviderStateMixin {
  OverlayEntry? overlayEntry;

  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 500));
  }

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

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onExit: (onExit) {
        setOnExitMenu(context, onExit.delta.dy);
      },
      child: GestureDetector(
          onTap: () {
            insertOverlay(context);
            _animationController.forward();
          },
          child: widget.child),
    );
  }

  void setOnExitMenu(BuildContext context, double delta) {
    if (delta >= 0) {
      return;
    }
    closeDropDown();
  }

  void insertOverlay(BuildContext context) {
    closeDropDown();
    final rect = findParamsData(context);
    overlayEntry = _createOverlay(rect: rect);
    Overlay.of(context)!.insert(overlayEntry!);
  }

  OverlayEntry _createOverlay({required Rect rect}) {
    return OverlayEntry(builder: (context) {
      return Material(
        color: Colors.transparent,
        child: Stack(
          alignment: Alignment.center,
          clipBehavior: Clip.none,
          children: [
            Positioned(
              child: IgnorePointer(
                child: Container(
                  color: Colors.black45,
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                ),
              ),
            ),
            Positioned.fromRect(
                rect: Rect.fromLTWH(
                    rect.left, rect.top, rect.width * 3, rect.height * 10),
                child: SlideTransition(
                    position: Tween(
                            begin: const Offset(0, 0.15),
                            end: const Offset(0, 0))
                        .animate(_animationController),
                    child: MouseRegion(
                      onExit: (onExit) {
                        setOnExitMenu(context, -1);
                      },
                      child: const ContentDropDown(),
                    )
                )),
          ],
        ),
      );
    });
  }

  Rect findParamsData(BuildContext context) {
    final RenderBox renderBox = context.findRenderObject()! as RenderBox;

    final size = renderBox.size;
    final offset = renderBox.localToGlobal(Offset.zero);

    return Rect.fromLTWH(
        offset.dx, offset.dy + size.height, size.width, size.height);
  }

  void closeDropDown() {
    if (overlayEntry != null) {
      overlayEntry!.remove();
      overlayEntry = null;
    }
  }
}

class ContentDropDown extends StatelessWidget {
  const ContentDropDown({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
          color: Colors.white,
          border: Border.all(
              color: Theme.of(context).colorScheme.secondary,
              width: 0.5)),
      child: ListView(
        shrinkWrap: true,
        children: [
          TextFormField(),
          ListView.builder(
              shrinkWrap: true,
              itemCount: 10,
              itemBuilder: (context, index) {
                return ListTile(
                  leading: Image.network(
                      "https://upload.wikimedia.org/wikipedia/nap/thumb/f/f3/Logo_UEFA_Champions_League.png/250px-Logo_UEFA_Champions_League.png"),
                  title: const Text("Champions League"),
                );
              }),
        ],
      ),
    );
  }
}