Flutter输入框下方弹出历史下拉选择PopWindow(输入框不失去焦点)

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 设置 focusNodekey 属性

弹出框方法

  /// 显示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),
    );
}
  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值