Flutter 给iOS键盘添加工具栏

标签: 编程学习 Flutter学习

老方法

先看一下我写的工具栏代码

class InputDoneView extends StatefulWidget {
  final bool isShowPaste;
  final VoidCallBack pasteCallBack;
  final VoidCallBack doneCallBack;
  InputDoneView({this.isShowPaste = false, this.pasteCallBack, this.doneCallBack});
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _InputDoneViewState();
  }
}

class _InputDoneViewState extends State<InputDoneView> {
  bool _isShowing = true;

  _changeShowing() {
    _isShowing = !_isShowing;
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return AnimatedCrossFade(
      duration: Duration(milliseconds: 180),
      crossFadeState: _isShowing ? CrossFadeState.showFirst : CrossFadeState.showSecond,
      firstChild: Container(
        height: 44,
        width: MediaQuery.of(context).size.width,
        color: ColorConstant.COLOR_LIGHT_GRAY_COLOR,
        child: Row(
          children: <Widget>[
            widget.isShowPaste ? Expanded(
              child: Align(
                alignment: Alignment.topLeft,
                child: Padding(
                  padding: const EdgeInsets.only(top: 1.0, bottom: 1.0),
                  child: CupertinoButton(
                    padding:
                    const EdgeInsets.only(left: 20.0, top: 2.0, bottom: 2.0),
                    onPressed: () {
                      _changeShowing();
                      ///粘贴回调
                      if(widget.pasteCallBack != null) {
                        widget.pasteCallBack();
                      }
                    },
                    child: Text(
                      S.of(context).paste_string,
                      style: TextStyle(
                        color: ColorConstant.LIGHT_WHITE,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ),
              ),
            ) : SizedBox(width: 0,),
            Expanded(
              child: Align(
                alignment: Alignment.topRight,
                child: Padding(
                  padding: const EdgeInsets.only(top: 1.0, bottom: 1.0),
                  child: CupertinoButton(
                    padding:
                    const EdgeInsets.only(right: 20.0, top: 2.0, bottom: 2.0),
                    onPressed: () {
                      _changeShowing();
                      if(widget.doneCallBack != null) {
                        widget.doneCallBack();
                      }
                      FocusScope.of(context).requestFocus(FocusNode());
                    },
                    child: Text(
                      S.of(context).done_string,
                      style: TextStyle(
                        color: ColorConstant.LIGHT_WHITE,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
      secondChild: Container(),
    );
  }
}

我使用了一个轮子 keyboard_visibility 进行键盘的状态监听,当然也可以自己写个小插件,在这里来控制工具栏的展示和消失

KeyboardVisibilityNotification().addNewListener(
  onChange: (bool visible) {
    print('--==-=-==-=---==-=-=-=-=-$visible');
  },
  onShow: () {
    print('--==-=-==-=---==-=-=-=-=-show');
    if (_isShowPaste) {
      _showOverlay(context);
    }
  },
  onHide: () {
    if (_overlayEntry != null) {
      //让键盘下落
      FocusScope.of(context).requestFocus(FocusNode());
    }
    _removeOverlay();
    print('--==-=-==-=---==-=-=-=-=-hide');
  },
);

///显示Done按钮
_showOverlay(BuildContext context) {
    if (_overlayEntry != null) return;
    OverlayState overlayState = Overlay.of(context);
    _overlayEntry = OverlayEntry(builder: (context) {
      return Positioned(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        right: 0.0,
        left: 0.0,
        child: Material(
          child: InputDoneView(),
        ),
      );
    });
    overlayState.insert(_overlayEntry);
}

///移除Done按钮
_removeOverlay() {
    if (_overlayEntry != null) {
      _overlayEntry.remove();
      _overlayEntry = null;
    }
}

这样写可以将就用了但是有些地方需要手动去让工具栏消失,这里用事件总线来控制

eventBus.on(EventBusTitle.EVENT_BUS_HIDE, (arg) {
  _removeOverlay();
});

这里使用的事件总线 event bus 代码(一定确定自己的项目是否需要事件总线,如果不需要尽量不要用,虽然使用简单,但是后期还是比较麻烦的)

class EventBusName {
  ///登录
  static const String EVENT_BUS_LOGIN = 'event_bus_login';
}

///订阅者回调签名
typedef void EventCallback(dynamic arg);

class EventBus {
  //私有构造函数
  EventBus._internal();

  //保存单例
  static EventBus _singleton = EventBus._internal();

  //工厂构造函数
  factory EventBus() => _singleton;

  //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
  var _emap = Map<Object, List<EventCallback>>();

  //粘性事件 key:事件名,value: arg参数
  var _semap = Map<Object, Object>();

  //添加订阅者
  EventCallback on(eventName, EventCallback f) {
    debugPrint('-=-=-=-=-=-=-=-=-=-=-=-=添加监听--------$eventName');
    if (eventName == null || f == null) return null;
    _emap[eventName] ??= new List<EventCallback>();
    _emap[eventName].add(f);
    if (_semap.containsKey(eventName)) {
      f(_semap[eventName]);
    }
    return f;
  }

  eventCount(eventName) {
    if(_emap.containsKey(eventName)) {
      var list = _emap[eventName];
      if(list != null) {
        return list.length;
      } else {
        return 0;
      }
    }
    return 0;
  }

  //移除订阅者
  ///这里有坑
  void off(eventName, [EventCallback f]) {
    debugPrint('-=-=-=-=-=-=-=-=-=-=-=-=移除监听--------$eventName');
    var list = _emap[eventName];
    if (eventName == null || list == null) return;
    if (f == null) {
      _emap[eventName] = null;
    } else {
      list.remove(f);
    }
  }

  ///移除所有
  void removeAll() {
    debugPrint('-=-=-=-=-=-=-=-=-=-=-=-=移除所有监听');
    if (_emap == null || _emap.length == 0) return;
    _emap = Map<Object, List<EventCallback>>();
  }

  //触发事件,事件触发后该事件所有订阅者会被调用
  void emit(eventName, [arg]) {
    debugPrint('-=-=-=-=-=-=-=-=-=-=-=-=发送监听--------$eventName');
    var list = _emap[eventName];
    if (list == null) return;
    int len = list.length - 1;
    //反向遍历,防止在订阅者在回调中移除自身带来的下标错位
    for (var i = len; i > -1; --i) {
      list[i](arg);
    }
  }

  //发送粘性消息,使在其之后的订阅者也能收到
  void emitSticky(eventName, [arg]) {
    emit(eventName, arg);
    if (eventName == null) return null;
    _semap[eventName] = arg;
  }

  //移除粘性消息,避免继续传递
  void removeSticky(eventName){
    if(_semap.containsKey(eventName))
      _semap.remove(eventName);
  }
}

///定义一个top-level变量,页面引入该文件后可以直接使用bus
var eventBus = EventBus();

新方法

其实去年在使用老方法之前我就已经发现有别人的轮子 keyboard_actions 可以用了,但是当时项目已经做好,急于上线,而当时基于轮子的使用方法比较复杂,对于项目改动较大,就没有直接套用, 后来发现新版本的使用起来已经简单了很多

///键盘落下
keyboardDown(BuildContext context) {
  FocusScope.of(context).requestFocus(FocusNode());
}

class KeyboardWidget extends StatelessWidget {
  final KeyBoardController controller;
  final Widget child;
  final bool isDialog;

  KeyboardWidget({this.controller, this.child, this.isDialog = false})
      : assert(controller != null);

  @override
  Widget build(BuildContext context) {
    if(Platform.isAndroid){
      return child;
    }
    return KeyboardActions(
      tapOutsideToDismiss: false,
      isDialog: isDialog,

      config: KeyboardActionsConfig(
//        nextFocus: controller.isMultiply,
        nextFocus: false,
        keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
        actions: controller.nodes
            .map((e) => KeyboardAction(
                focusNode: e,
//                displayArrows: controller.isMultiply,
                displayArrows: false,
                onTapAction: () {
                  controller.down();
                }))
            .toList(),
      ),
      child: child,
    );
  }
}

class KeyBoardController {
  final BuildContext context;
  final List<FocusNode> nodes = List();

  bool get isMultiply => nodes.length > 1;

  KeyBoardController({this.context}) : assert(context != null);

  addNode(FocusNode node) {
    if (!nodes.contains(node)) {
      nodes.add(node);
    }
  }

  clearNodes() {
    nodes.clear();
  }

  down() {
    FocusScope.of(context).requestFocus(FocusNode());
  }
}

extension FocusNodeExtension on FocusNode {
  FocusNode attachController(KeyBoardController controller) {
    controller.addNode(this);
    return this;
  }
}

使用的时候只需要将 KeyboardWidget 嵌套在将要使用的组件外围即可。