在Flutter中创建自定义下拉菜单 - 或者如何将自定义下拉菜单选项放置在其他所有内容上方的图层中。
在Flutter中创建自定义下拉菜单 - 或者如何将自定义下拉菜单选项放置在其他所有内容上方的图层中。
我正在寻找一种自定义下拉菜单的方法,以便可以自己进行样式设计。
我遇到了这个回答,看起来非常有用
https://stackoverflow.com/a/63165793/3808307
问题在于,如果容器比下拉菜单小,Flutter会报像素溢出的警告。我该如何使这个下拉菜单位于页面上的其他元素之上,以免出现此警告?或者是否有另一种方法可以重新创建自定义下拉菜单而不会出现此问题?
我找到的所有答案都涉及内置的DropdownButton
下面是上述链接的回答,已经进行了修改
首先,创建一个名为drop_list_model.dart
的Dart文件:
import 'package:flutter/material.dart';
class DropListModel {
DropListModel(this.listOptionItems);
final List
}
class OptionItem {
final String id;
final String title;
OptionItem({@required this.id, @required this.title});
}
接下来,创建文件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
OptionItem optionItemSelected;
final DropListModel dropListModel;
AnimationController expandController;
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:
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:
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
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:
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);
},
),
);
}
}
初始化值:
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");
最后使用它:
Container(height: 47, child: SelectDropList(
this.optionItemSelected,
this.dropListModel,
(optionItem){
optionItemSelected = optionItem;
setState(() {
});
},
))
问题的出现的原因:在使用自定义下拉菜单时,原生的下拉菜单功能不够灵活,无法满足设计需求。因此,需要使用Overlay来创建自定义下拉菜单,以便在其他组件的上方浮动显示。
解决方法:通过使用Overlay类,可以将独立的子组件插入到覆盖层的Stack中,从而实现在其他组件上方浮动显示。具体操作步骤如下:
1. 创建OverlayEntry对象,用于管理子组件在覆盖层中的参与情况。
2. 在需要显示下拉菜单的触发事件中,根据isDropdownOpened的状态来判断是插入还是移除OverlayEntry。
3. 使用_createFloatingDropdown()方法创建自定义下拉菜单的布局,并设置其位置和大小。
4. 将创建的OverlayEntry插入到当前上下文的Overlay中。
5. 根据isDropdownOpened的状态来更新下拉菜单的显示状态。
示例代码中的createFloatingDropdown()方法通过Positioned组件设置下拉菜单的位置,并使用Container组件作为下拉菜单的容器,其中包含任意子组件。
问题的解决方法是使用Overlay类创建自定义下拉菜单,并通过OverlayEntry来管理子组件的显示与隐藏。这种方法可以灵活地实现各种设计需求,并且可以通过调整位置和大小来满足不同的展示效果。
在这个问题的例子中,原生的下拉菜单按钮位于列表项中,因此需要为每个下拉菜单单独设置位置,这不太方便。特别是当位于底部的下拉菜单需要向上展开时(以避免超出页面范围),而位于顶部的下拉菜单需要向下展开时,这种操作显得不太方便。因此,使用Overlay类创建自定义下拉菜单可以解决这个问题。
问题的出现是因为内置的下拉菜单(dropdown)无法满足某些特定的使用场景需求,例如需要在按钮下方显示下拉菜单或者需要完全控制下拉菜单的显示位置。为了解决这个问题,作者尝试自己创建了一个自定义的下拉菜单。作者在Medium上找到了一篇关于使用overlay显示浮动小部件的文章,对此非常有帮助。
解决方法是通过创建一个全屏的堆叠布局,使用overlay显示下拉菜单。通过使用LayerLink
和CompositedTransformFollower
小部件将overlay与按钮关联起来。使用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
final Widget child;
final void Function(T, int) onChange;
final List
final DropdownStyle dropdownStyle;
final DropdownButtonStyle dropdownButtonStyle;
final Icon icon;
final bool hideIcon;
final bool leadingIcon;
CustomDropdown({
Key key,
this.hideIcon = false,
this.child,
this.items,
this.dropdownStyle = const DropdownStyle(),
this.dropdownButtonStyle = const DropdownButtonStyle(),
this.icon,
this.leadingIcon = false,
this.onChange,
}) : super(key: key);
_CustomDropdownState
}
class _CustomDropdownState
with TickerProviderStateMixin {
final LayerLink _layerLink = LayerLink();
OverlayEntry _overlayEntry;
bool _isOpen = false;
int _currentIndex = -1;
AnimationController _animationController;
Animation
Animation
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,
));
}
Widget build(BuildContext context) {
var style = widget.dropdownButtonStyle;
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() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
var topOffset = offset.dy + size.height + 5;
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _toggleDropdown(close: true),
behavior: HitTestBehavior.translucent,
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();
}
}
}
class DropdownItem
final T value;
final Widget child;
const DropdownItem({Key key, this.value, this.child}) : super(key: key);
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;
final Offset offset;
final double width;
const DropdownStyle({
this.constraints,
this.offset,
this.width,
this.elevation,
this.color,
this.padding,
this.borderRadius,
});
}
使用自定义下拉菜单的代码如下:
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomDropdown
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
value: item.key + 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.value),
),
),
)
.toList(),
),
),
);
}
作者表示该自定义下拉菜单在当前环境中运行良好,但可能还需要进行一些改进。
原因:问题出现的原因是用户想要自定义下拉菜单的外观和样式,但是标准的Flutter下拉菜单没有提供足够的灵活性和自定义选项。
解决方法:用户可以通过以下方法解决问题:
1. 使用标准的Flutter下拉菜单,然后通过修改ThemeData类来自定义下拉菜单的外观和样式,包括背景色和文本样式。
2. 如果不想或不能使用ThemeData类,可以使用DropdownButton类的dropdownColor属性直接为下拉菜单指定颜色,而不改变任何ThemeData。这样可以自动改变下拉菜单项的颜色。
3. 如果想要改变下拉菜单的宽度,可以将DropdownButton的child属性设置为一个新的Container,并设置所需的宽度。建议使用动态宽度以避免在使用更复杂的布局时出现溢出问题。
4. DropdownButton类还具有展开的功能,可以占据尽可能多的空间。可以通过设置DropdownButton的isExpanded属性为true来实现。
用户还提到了一些其他问题,如希望选中的元素在垂直轴上与下拉按钮具有相同的坐标,希望使用阴影而不是海拔等。对于这些问题,用户通过将PopupMenuButton的代码复制到单独的文件中,并进行修改来解决。用户表示不确定这是否是最佳解决方案,并且提到了对Flutter默认样式的不满。
以上就是问题的原因和解决方法的整理。