颤振:我想在下拉颤振下显示下拉列表
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,具有更多选项,您可以根据需要进行自定义。
免责声明:我是上述包的作者。
我想创建看起来像我提到的下拉菜单,但我无法实现我的方面结果
我尝试使用渲染框来制作自定义下拉菜单,但它想要感觉像实际的下拉菜单
谁能帮我得到这种结果
我想要这样的结果:-
我现在的 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,具有更多选项,您可以根据需要进行自定义。
免责声明:我是上述包的作者。