Flutter类似PopWindow选择框
Flutter中TextField弹出PopWindow,里边放着类似ListView的内容,点击其中Item的时候TextField会失去焦点。针对该问题做出修改:
使用了TransitionRoute代替PopupRoute,这样就不会失去焦点。
代码:
import 'package:flutter/material.dart';
class PopupWindow extends StatefulWidget {
static void showPopWindow(context, String msg, GlobalKey popKey,
[PopDirection popDirection = PopDirection.bottom,
Widget? popWidget,
double offset = 0,
bool isList = false]) {
Navigator.push(
context,
PopRoute(
child: PopupWindow(
msg: msg,
popKey: popKey,
popDirection: popDirection,
popWidget: popWidget,
offset: offset,
isList: isList,
)));
}
String msg;
GlobalKey popKey;
PopDirection popDirection;
Widget? popWidget; //自定义widget
double offset; //popupWindow偏移量
bool isList = false;
PopupWindow(
{required this.popWidget,
required this.msg,
required this.popKey,
this.popDirection = PopDirection.bottom,
required this.offset,
this.isList = false});
State<StatefulWidget> createState() {
return _PopupWindowState();
}
}
class _PopupWindowState extends State<PopupWindow> {
late GlobalKey buttonKey;
double left = -100;
double top = -100;
double right = -100;
double bottom = -100;
void initState() {
super.initState();
buttonKey = GlobalKey();
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderBox renderBox = widget.popKey.currentContext?.findRenderObject() as RenderBox;
Offset localToGlobal =
renderBox.localToGlobal(Offset.zero); //targetWidget的坐标位置
Size size = renderBox.size; //targetWidget的size
RenderBox buttonBox = buttonKey.currentContext?.findRenderObject() as RenderBox;
var buttonSize = buttonBox.size; //button的size
switch (widget.popDirection) {
case PopDirection.left:
left = localToGlobal.dx - buttonSize.width - widget.offset;
top = localToGlobal.dy + size.height / 2 - buttonSize.height / 2;
break;
case PopDirection.top:
left = localToGlobal.dx + size.width / 2 - buttonSize.width / 2;
top = localToGlobal.dy - buttonSize.height - widget.offset;
fixPosition(buttonSize);
break;
case PopDirection.right:
left = localToGlobal.dx + size.width + widget.offset;
top = localToGlobal.dy + size.height / 2 - buttonSize.height / 2;
break;
case PopDirection.bottom:
// left = localToGlobal.dx + size.width / 2 - buttonSize.width / 2;
left = localToGlobal.dx + size.width - buttonSize.width;
top = localToGlobal.dy + size.height + widget.offset;
right = 0;
bottom = 0;
fixPosition(buttonSize);
break;
}
setState(() {});
});
}
void fixPosition(Size buttonSize) {
if (left < 0) {
left = 0;
}
if (left + buttonSize.width >= MediaQuery.of(context).size.width) {
left = MediaQuery.of(context).size.width - buttonSize.width;
}
}
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: GestureDetector(
child: Stack(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.transparent,
),
Positioned(
left: left,
top: top,
child: widget.popWidget == null
? _buildWidget(widget.msg)
: _buildCustomWidget(widget.popWidget?? const Text("传入的空的"),
),
)
],
),
onTap: () {
Navigator.pop(context);
},
),
);
}
Widget _buildWidget(String text) => Container(
key: buttonKey,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.all(Radius.circular(6))),
child: Text(text),
);
Widget _buildCustomWidget(Widget child) => Container(
constraints: widget.isList ? BoxConstraints(maxWidth: 80,maxHeight: 300) : BoxConstraints(),
key: buttonKey,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: child,
);
}
class PopRoute extends TransitionRoute {
final Duration _duration = const Duration(milliseconds: 200);
Widget child;
PopRoute({required this.child});
Color? get barrierColor => null;
bool get barrierDismissible => true;
String? get barrierLabel => null;
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return child;
}
Duration get transitionDuration => _duration;
Iterable<OverlayEntry> createOverlayEntries() sync* {
/// 创建页面实际内容,最终调用到 Route 的 builder 方法
yield OverlayEntry(builder: _buildModalScope, maintainState: true);
}
Widget _buildModalScope(BuildContext context) {
// To be sorted before the _modalBarrier.
return child;
}
bool get opaque => false;
}
//popwindow的方向
enum PopDirection { left, top, right, bottom }
/// author baorant
/// 通用菜单项
class MenuItem {
// 显示的文本
String label;
// 选中的值
dynamic value;
// 是否选中
bool checked;
MenuItem({this.label = '', this.value, this.checked = false});
}
使用:
先定义一个弹出pop的方法 showPop
在FocusNode的addListener((){}) 中调用showPop
给TextField 设置 focusNode 和 key 属性
弹出框方法
/// 显示pop窗口,不能影响输入框输入内容
/// 由于嵌套 ListView 需要设置宽度高度
/// 这里判断如果item大于6条设置固定宽高,使用listview,否则自适应宽高,循环创建btn
void showPop(
GlobalKey key, RxList<MenuItem> items, Function(String) onPressed) {
PopupWindow.showPopWindow(
context,
"",
key,
PopDirection.bottom,
items.length > 6
? MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return GFButton(
size: 25,
color: Colors.white,
text: items[index].value,
textStyle: TextStyle(
fontSize: 15.sp,
color: Colors.black,
fontWeight: FontWeight.w400),
fullWidthButton: false,
onPressed: ThrottleUtil.debounce(() {
onPressed(items[index].value.toString());
Navigator.of(context).pop();
}));
}))
: Column(
children: items
.mapIndexed((index, element) => GFButton(
size: 25,
color: Colors.white,
text: items[index].value,
textStyle: TextStyle(
fontSize: 15.sp,
color: Colors.black,
fontWeight: FontWeight.w400),
fullWidthButton: false,
onPressed: ThrottleUtil.debounce(() {
onPressed(items[index].value.toString());
Navigator.of(context).pop();
})))
.toList(),)
,5, items.length > 6);
}
防止重复点击
import 'dart:async';
import 'package:flutter/cupertino.dart';
class ThrottleUtil {
static const Duration _KDelay = Duration(milliseconds: 500);
var enable = true;
///防止重复点击
///func 要执行的方法
Function throttle(
Function func, {
Duration delay = _KDelay,
}) {
return () {
if (enable) {
func();
enable = false;
Future.delayed(delay, () {
enable = true;
});
}
};
}
static void Function() debounce(Null Function() fn, [int t = 300]) {
Timer? _debounce;
return () {
// 还在时间之内,抛弃上一次
if (_debounce?.isActive ?? false) {
debugPrint('短时间内 重复无效点击..');
_debounce?.cancel();
}else{
fn();
}
_debounce = Timer(Duration(milliseconds: t), () {
_debounce?.cancel();
_debounce = null;
});
};
}
}
实际调用位置
focusNode1.addListener(() {
if (focusNode1.hasFocus && items1.isNotEmpty) {
showPop(globalKey, items1, (String str) {
etController.text = str; // 这个etController 时TextField的controller
});
}
});
UI设置
TextField(
style: TextStyle(color: ColorRes.color_666, fontSize: 12.sp),
key: globalKey,
focusNode: focusNode1,
textAlign: TextAlign.right,
controller: etController,
decoration: InputDecoration(
hintStyle: TextStyle(color: ColorRes.color_666, fontSize: 12.sp),
focusedBorder: _inputBorder(),
disabledBorder: _inputBorder(),
errorBorder: _inputBorder(),
focusedErrorBorder: _inputBorder(),
enabledBorder: _inputBorder(),
border: _inputBorder(),
contentPadding: const EdgeInsets.all(0),
hintText: controller.list[index].textOrHint,
),
)
InputBorder _inputBorder() {
return const OutlineInputBorder(
borderSide: BorderSide(width: 0, color: Colors.transparent),
);
}