颤振:我想在下拉颤振下显示下拉列表

Flutter: I want to show dropdown list under dropdown flutter

我想创建看起来像我提到的下拉菜单,但我无法实现我的方面结果

我尝试使用渲染框来制作自定义下拉菜单,但它想要感觉像实际的下拉菜单

谁能帮我得到这种结果

我想要这样的结果:-

我现在的 ui 是这样的:-

这是我的代码:-


class AppDropDown extends StatefulWidget {
  AppDropDown({
    Key? key,
    required this.dropDownList,
    required this.selected,
    this.text = "",
  }) : super(key: key);
  final List<String> dropDownList;
  String selected;
  final String text;

  @override
  State<PerytonDropDown> createState() => _PerytonDropDownState();
}

class _PerytonDropDownState extends State<PerytonDropDown> {
  @override
  Widget build(BuildContext context) {
    return Card(
      color: Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(15.sp),
      ),
      child: Material(
        borderRadius: BorderRadius.circular(15.sp),
        clipBehavior: Clip.antiAlias,
        color: Colors.transparent,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (widget.text.isNotEmpty) (8.0).addHSpace(),
            if (widget.text.isNotEmpty) "${widget.text}".grayText(),
            SizedBox(
              height: 40,
              child: DropdownButton<String>(
                isExpanded: true,
                value: widget.selected,
                underline: SizedBox(),
                onChanged: (String? value) {
                  print(value);
                  setState(() {
                    widget.selected = value!;
                  });
                },
                alignment: Alignment.bottomRight,
                borderRadius: BorderRadius.circular(15),
                items: widget.dropDownList
                    .map((item) => DropdownMenuItem(
                          child: Text(
                            item,
                          ),
                          value: item,
                        ))
                    .toList(),
              ),
            )
          ],
        ).pSymmetricOnly(horizontal: 10),
      ),
    );
  }
}

我使用 OverlayEntry 和 The Flow 小部件。

final menuProvider =
    ChangeNotifierProvider.autoDispose<MenuChangeNotifier>((ref) {
  return MenuChangeNotifier();
});

class MenuChangeNotifier extends ChangeNotifier {
  bool isEnterDropDown = false;
  OverlayEntry? floatingDropdown;
  bool isDropdownOpened = false;
  bool isHighlight = false;
  late double height, width, xPosition, yPosition;
  String onGoingMenu = '';

  void setOverlayChange(BuildContext context, Map itemMenu, String text) {
    if (!isEnterDropDown && isDropdownOpened && floatingDropdown != null) {
      if (itemMenu.isEmpty) {
        isHighlight = false; //Juste pour changer Title

        notifyListeners();
        return;
      }

      floatingDropdown!.remove();
      isHighlight = false;
      isDropdownOpened = false;
      onGoingMenu = '';
    } else if (!isDropdownOpened) {
      onGoingMenu = text;
      if (itemMenu.isEmpty) {
        isHighlight = !isHighlight; //Juste pour changer Title

        notifyListeners();
        return;
      }

      findDropdownData(context);

      floatingDropdown = _createFloatingDropdown(itemMenu);

      Overlay.of(context)!.insert(floatingDropdown!);

      isDropdownOpened = true;
      isHighlight = true;
      isEnterDropDown = false;
    }

    notifyListeners();
  }

  void findDropdownData(BuildContext context) {
    final RenderBox renderBox = context.findRenderObject()! as RenderBox;

    height = renderBox.size.height + 3;
    width = renderBox.size.width;
    final Offset offset = renderBox.localToGlobal(Offset.zero);
    xPosition = offset.dx;
    yPosition = offset.dy;
  }

  OverlayEntry _createFloatingDropdown(Map itemMenu) {
    return OverlayEntry(builder: (context) {
      return Stack(
        children: [
          Positioned(
            top: yPosition + height - 4,
            child: IgnorePointer(
              child: Container(
                color: Colors.black45,
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
              ),
            ),
          ),
          Positioned(
            left: xPosition,
            top: yPosition + height - 4,
            //-4 pour que le curseur sois dans le dropdown
            child: DropDown(
              itemHeight: height,
              listMenu: itemMenu,
              key: GlobalKey(),
            ),
          )
        ],
      );
    });
  }

  void closeDropDown() {
    if (floatingDropdown == null) {
      return;
    }
    if (isDropdownOpened && isEnterDropDown) {
      floatingDropdown!.remove();
      isDropdownOpened = false;
      isHighlight = false;
      isEnterDropDown = false;
      notifyListeners();
    }
  }
}

class TitleDropdown extends HookConsumerWidget {
  final String text;
  final Map itemMenu;

   const TitleDropdown(
      {required Key key, required this.text, required this.itemMenu})
      : super(key: key);

  @override
  Widget build(BuildContext context,WidgetRef ref) {
    final theme = Theme.of(context);
    final animationController = useAnimationController(duration: const Duration(milliseconds: 400));
    Animation<double> animTween = Tween<double>(
        begin: 0.0, end: 100) //32 le padding
        .animate(CurvedAnimation(
        parent: animationController, curve: Curves.easeInOut));
    return InkWell(
      onHover: (onHover) async {
        await Future.delayed(const Duration(milliseconds: 10));
        ref
            .read(menuProvider)
            .setOverlayChange(context, itemMenu, text);
      },
      onTap: () {},
      child: Consumer(builder: (context, ref, _) {
        final toggle = ref.watch(menuProvider.select((value) => value.isHighlight)) &&
            ref.watch(menuProvider.select((value) => value.onGoingMenu)) == text;

        WidgetsBinding.instance!.addPostFrameCallback((_) {
          animTween = Tween<double>(
              begin: 0.0, end: context.size!.width - 32) //32 le padding
              .animate(CurvedAnimation(
              parent: animationController, curve: Curves.easeInOut));
        });

        if (toggle) {
          animationController.forward();
        } else {
          animationController.reverse();
        }

        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Column(
            children: [
              Text(
                text,
                style: theme.textTheme.headline6,
              ),
              AnimatedBuilder(
                animation: animTween,
                builder: (context, _) {
                  return Container(
                    color: theme.colorScheme.onBackground,
                    height: 1,
                    width: animTween.value,
                  );
                },
              )
            ],
          ),
        );
      }),
    );
  }
}

class DropDown extends HookConsumerWidget {
  final double itemHeight;
  final Map? listMenu;

  const DropDown({required Key key, required this.itemHeight, this.listMenu})
      : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final l = listMenu?.length ?? 0;
    final h = heightOfOneItem * l.toDouble();

    final theme = Theme.of(context);
    final anim = useAnimationController(
      duration: const Duration(milliseconds: 250),
    );
    anim.forward();

    final primaryColor = Theme.of(context).colorScheme.primaryVariant;
    return MouseRegion(
      onEnter: (PointerEnterEvent pointerEnterEvent) {
        ref.read(menuProvider).isEnterDropDown = true;
      },
      onExit: (PointerExitEvent pointerExitEvent) {
        ref.read(menuProvider).closeDropDown();
      },
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          ClipPath(
            clipper: ArrowClipper(),
            child: Container(
              height: arrowHeight,
              width: 40,
              decoration: BoxDecoration(
                color: primaryColor,
              ),
            ),
          ),
          SizedBox(
            height: h,
            width: 200,
            child: AnimatedAppear(
              children: <Widget>[
                if (listMenu != null && listMenu!.isNotEmpty)
                  ...listMenu!.keys.map((key) => Row(
                    children: [
                      Expanded(
                        child: Material(
                          color: Colors.transparent,
                          child: SizedBox(
                            height: heightOfOneItem,
                            child: DropDownItem(
                              text: key!.toString(),
                              key: GlobalKey(),
                              isFirstItem:
                                  listMenu!.keys.first.toString() == key,
                              isLastItem:
                                  listMenu!.keys.last.toString() == key,
                              iconData: Icons.person_outline,
                            ),
                          ),
                        ),
                      ),
                    ],
                  )),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class DropDownItem extends StatefulWidget {
  final String text;
  final IconData iconData;
  final bool isFirstItem;
  final bool isLastItem;

  const DropDownItem(
      {Key? key,
      required this.text,
      required this.iconData,
      this.isFirstItem = false,
      this.isLastItem = false})
      : super(key: key);

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

class _DropDownItemState extends State<DropDownItem> {
  late bool isSelected;


  @override
  void initState() {
    super.initState();
    isSelected = false;
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return InkWell(
      onTap: () {},
      onHover: (onHover) {
        setState(() {
          isSelected = !isSelected;
        });
      },
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.only(
            topRight: widget.isFirstItem ? const Radius.circular(8) : Radius.zero,
            bottomLeft: widget.isLastItem ? const Radius.circular(8) : Radius.zero,
            bottomRight: widget.isLastItem ? const Radius.circular(8) : Radius.zero,
          ),
          color: isSelected
              ? theme.colorScheme.primary
              : theme.colorScheme.primaryVariant,
        ),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: FittedBox(
          fit: BoxFit.scaleDown,
          alignment: Alignment.centerLeft,
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Text(
                widget.text,
                style: theme.textTheme.headline5,
              ),
              Icon(
                widget.iconData,
                color: theme.colorScheme.onPrimary,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class ArrowClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final Path path = Path();

    path.moveTo(0, size.height);
    path.lineTo(size.width / 2, 0);
    path.lineTo(size.width, size.height);

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}

class AnimatedAppear extends HookWidget {
  final List<Widget> children;
  final bool isForward;

  const AnimatedAppear(
      {required this.children, this.isForward = true, Key? key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    final animationController = useAnimationController(
      duration: const Duration(milliseconds: 1000),
    );
    if (isForward) {
      animationController.forward();
    } else {
      animationController.reverse();
    }

    return Flow(
      delegate: FlowMenuDelegate(animationController: animationController),
      children: children,
    );
  }
}

class FlowMenuDelegate extends FlowDelegate {
  final AnimationController animationController;

  FlowMenuDelegate({required this.animationController})
      : super(repaint: animationController);

  @override
  void paintChildren(FlowPaintingContext context) {
    final size = context.size;

    final xStart = size.width / 2;
    final yStart = -size.height /2 + arrowHeight/2;


    const margin = 0;
    final animation = Tween(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(parent: animationController, curve: Curves.elasticOut));
    for (int i = context.childCount - 1; i >= 0; i--) {
      final childSize = context.getChildSize(i);

      if (childSize == null) {
        continue;
      }

      final dy = yStart + (margin + heightOfOneItem) * (i) * animation.value;

      context.paintChild(i,
          transform: Matrix4.translationValues(
              xStart - childSize.width / 2, dy.toDouble(), 0),
          opacity: animation.value);
    }
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) => false;
}

我制作了一个小部件,现在可以使用此代码。

这是下拉小部件

class CustDropDown<T> extends StatefulWidget {
  final List<CustDropdownMenuItem> items;
  final Function onChanged;
  final String hintText;
  final double borderRadius;
  final double maxListHeight;
  final double borderWidth;
  final int defaultSelectedIndex;
  final bool enabled;

  const CustDropDown(
      {required this.items,
      required this.onChanged,
      this.hintText = "",
      this.borderRadius = 0,
      this.borderWidth = 1,
      this.maxListHeight = 100,
      this.defaultSelectedIndex = -1,
      Key? key,
      this.enabled = true})
      : super(key: key);

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

class _CustDropDownState extends State<CustDropDown>
    with WidgetsBindingObserver {
  bool _isOpen = false, _isAnyItemSelected = false, _isReverse = false;
  late OverlayEntry _overlayEntry;
  late RenderBox? _renderBox;
  Widget? _itemSelected;
  late Offset dropDownOffset;
  final LayerLink _layerLink = LayerLink();

  @override
  void initState() {
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      if (mounted) {
        setState(() {
          dropDownOffset = getOffset();
        });
      }
      if (widget.defaultSelectedIndex > -1) {
        if (widget.defaultSelectedIndex < widget.items.length) {
          if (mounted) {
            setState(() {
              _isAnyItemSelected = true;
              _itemSelected = widget.items[widget.defaultSelectedIndex];
              widget.onChanged(widget.items[widget.defaultSelectedIndex].value);
            });
          }
        }
      }
    });
    WidgetsBinding.instance!.addObserver(this);
    super.initState();
  }

  void _addOverlay() {
    if (mounted) {
      setState(() {
        _isOpen = true;
      });
    }

    _overlayEntry = _createOverlayEntry();
    Overlay.of(context)!.insert(_overlayEntry);
  }

  void _removeOverlay() {
    if (mounted) {
      setState(() {
        _isOpen = false;
      });
      _overlayEntry.remove();
    }
  }

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

  OverlayEntry _createOverlayEntry() {
    _renderBox = context.findRenderObject() as RenderBox?;

    var size = _renderBox!.size;

    dropDownOffset = getOffset();

    return OverlayEntry(
        maintainState: false,
        builder: (context) => Align(
              alignment: Alignment.center,
              child: CompositedTransformFollower(
                link: _layerLink,
                showWhenUnlinked: false,
                offset: dropDownOffset,
                child: SizedBox(
                  height: widget.maxListHeight,
                  width: size.width,
                  child: Column(
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: _isReverse
                        ? MainAxisAlignment.end
                        : MainAxisAlignment.start,
                    children: <Widget>[
                      Padding(
                        padding: const EdgeInsets.only(top: 10),
                        child: Container(
                          constraints: BoxConstraints(
                              maxHeight: widget.maxListHeight,
                              maxWidth: size.width),
                          decoration: BoxDecoration(
                              color: Colors.white,
                              borderRadius: BorderRadius.circular(12)),
                          child: ClipRRect(
                            borderRadius: BorderRadius.all(
                              Radius.circular(widget.borderRadius),
                            ),
                            child: Material(
                              elevation: 0,
                              shadowColor: Colors.grey,
                              child: ListView(
                                padding: EdgeInsets.zero,
                                shrinkWrap: true,
                                children: widget.items
                                    .map((item) => GestureDetector(
                                          child: Padding(
                                            padding: const EdgeInsets.all(8.0),
                                            child: item.child,
                                          ),
                                          onTap: () {
                                            if (mounted) {
                                              setState(() {
                                                _isAnyItemSelected = true;
                                                _itemSelected = item.child;
                                                _removeOverlay();
                                                if (widget.onChanged != null)
                                                  widget.onChanged(item.value);
                                              });
                                            }
                                          },
                                        ))
                                    .toList(),
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ));
  }

  Offset getOffset() {
    RenderBox? renderBox = context.findRenderObject() as RenderBox?;
    double y = renderBox!.localToGlobal(Offset.zero).dy;
    double spaceAvailable = _getAvailableSpace(y + renderBox.size.height);
    if (spaceAvailable > widget.maxListHeight) {
      _isReverse = false;
      return Offset(0, renderBox.size.height);
    } else {
      _isReverse = true;
      return Offset(
          0,
          renderBox.size.height -
              (widget.maxListHeight + renderBox.size.height));
    }
  }

  double _getAvailableSpace(double offsetY) {
    double safePaddingTop = MediaQuery.of(context).padding.top;
    double safePaddingBottom = MediaQuery.of(context).padding.bottom;

    double screenHeight =
        MediaQuery.of(context).size.height - safePaddingBottom - safePaddingTop;

    return screenHeight - offsetY;
  }

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: _layerLink,
      child: GestureDetector(
        onTap: widget.enabled
            ? () {
                _isOpen ? _removeOverlay() : _addOverlay();
              }
            : null,
        child: Container(
          decoration: _getDecoration(),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Flexible(
                flex: 3,
                child: _isAnyItemSelected
                    ? Padding(
                        padding: const EdgeInsets.only(left: 4.0),
                        child: _itemSelected!,
                      )
                    : Padding(
                        padding:
                            const EdgeInsets.only(left: 4.0), // change it here
                        child: Text(
                          widget.hintText,
                          maxLines: 1,
                          overflow: TextOverflow.clip,
                        ),
                      ),
              ),
              const Flexible(
                flex: 1,
                child: Icon(
                  Icons.arrow_drop_down,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Decoration? _getDecoration() {
    if (_isOpen && !_isReverse) {
      return BoxDecoration(
          borderRadius: BorderRadius.only(
              topLeft: Radius.circular(widget.borderRadius),
              topRight: Radius.circular(
                widget.borderRadius,
              )));
    } else if (_isOpen && _isReverse) {
      return BoxDecoration(
          borderRadius: BorderRadius.only(
              bottomLeft: Radius.circular(widget.borderRadius),
              bottomRight: Radius.circular(
                widget.borderRadius,
              )));
    } else if (!_isOpen) {
      return BoxDecoration(
          borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius)));
    }
  }
}

class CustDropdownMenuItem<T> extends StatelessWidget {
  final T value;
  final Widget child;

  const CustDropdownMenuItem({required this.value, required this.child});

  @override
  Widget build(BuildContext context) {
    return child;
  }
}

在 ui 部分使用下拉列表

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF2F3F7),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Container(
              width: 200,
              height: 40,
              decoration: BoxDecoration(
                  color: Colors.white, borderRadius: BorderRadius.circular(12)),
              child: CustDropDown(
                items: const [
                  CustDropdownMenuItem(
                    value: 0,
                    child: Text("Day"),
                  ),
                  CustDropdownMenuItem(
                    value: 0,
                    child: Text("Week"),
                  )
                ],
                hintText: "DropDown",
                borderRadius: 5,
                onChanged: (val) {
                  print(val);
                },
              ),
            ),
          )
        ],
      ),
    );
  }
}

我已经解决了这个问题,并在 DropdownButton2 中添加了更多自定义项。它基于 Flutter 的核心 DropdownButton,具有更多选项,您可以根据需要进行自定义。

免责声明:我是上述包的作者。