keyboard_manager.dart 13.2 KB
Newer Older
Kevin's avatar
Kevin committed
1 2 3
part of cool_ui;

typedef GetKeyboardHeight = double Function(BuildContext context);
4
typedef KeyboardBuilder = Widget Function(
5
    BuildContext context, KeyboardController controller, String param);
Kevin's avatar
Kevin committed
6 7 8 9

class CoolKeyboard {
  static JSONMethodCodec _codec = const JSONMethodCodec();
  static KeyboardConfig _currentKeyboard;
10
  static Map<CKTextInputType, KeyboardConfig> _keyboards = {};
Kevin's avatar
Kevin committed
11 12 13 14 15 16
  static BuildContext _context;
  static OverlayEntry _keyboardEntry;
  static KeyboardController _keyboardController;
  static GlobalKey<KeyboardPageState> _pageKey;
  static bool isInterceptor = false;

17 18 19 20
  static ValueNotifier<double> _keyboardHeightNotifier = ValueNotifier(null)
    ..addListener(updateKeyboardHeight);

  static String _keyboardParam;
Kevin's avatar
Kevin committed
21

22
  static init(BuildContext context) {
Kevin's avatar
Kevin committed
23 24 25 26
    _context = context;
    interceptorInput();
  }

27 28
  static interceptorInput() {
    if (isInterceptor) return;
Kevin's avatar
Kevin committed
29
    isInterceptor = true;
30 31
    defaultBinaryMessenger.setMockMessageHandler("flutter/textinput",
        (ByteData data) async {
Kevin's avatar
Kevin committed
32
      var methodCall = _codec.decodeMethodCall(data);
33
      switch (methodCall.method) {
Kevin's avatar
Kevin committed
34
        case 'TextInput.show':
35
          if (_currentKeyboard != null) {
Kevin's avatar
Kevin committed
36 37
            openKeyboard();
            return _codec.encodeSuccessEnvelope(null);
38
          } else {
Kevin's avatar
Kevin committed
39 40 41 42
            return await _sendPlatformMessage("flutter/textinput", data);
          }
          break;
        case 'TextInput.hide':
43
          if (_currentKeyboard != null) {
Kevin's avatar
Kevin committed
44 45
            hideKeyboard();
            return _codec.encodeSuccessEnvelope(null);
46
          } else {
Kevin's avatar
Kevin committed
47 48 49 50
            return await _sendPlatformMessage("flutter/textinput", data);
          }
          break;
        case 'TextInput.setEditingState':
51 52
          var editingState = TextEditingValue.fromJSON(methodCall.arguments);
          if (editingState != null && _keyboardController != null) {
Kevin's avatar
Kevin committed
53 54 55 56 57
            _keyboardController.value = editingState;
            return _codec.encodeSuccessEnvelope(null);
          }
          break;
        case 'TextInput.clearClient':
58
          hideKeyboard(animation: true);
Kevin's avatar
Kevin committed
59 60 61 62 63
          clearKeyboard();
          break;
        case 'TextInput.setClient':
          var setInputType = methodCall.arguments[1]['inputType'];
          InputClient client;
64 65
          _keyboards.forEach((inputType, keyboardConfig) {
            if (inputType.name == setInputType['name']) {
Kevin's avatar
Kevin committed
66
              client = InputClient.fromJSON(methodCall.arguments);
67 68 69

              _keyboardParam = (client.configuration.inputType as CKTextInputType).params;

Kevin's avatar
Kevin committed
70 71
              clearKeyboard();
              _currentKeyboard = keyboardConfig;
72 73 74 75 76 77 78 79 80 81 82
              _keyboardController = KeyboardController(client: client)
                ..addListener(() {
                  var callbackMethodCall = MethodCall(
                      "TextInputClient.updateEditingState", [
                    _keyboardController.client.connectionId,
                    _keyboardController.value.toJSON()
                  ]);
                  defaultBinaryMessenger.handlePlatformMessage(
                      "flutter/textinput",
                      _codec.encodeMethodCall(callbackMethodCall),
                      (data) {});
83
                });
Kevin's avatar
Kevin committed
84
              if (_pageKey != null) {
85
                _pageKey.currentState?.update();
Kevin's avatar
Kevin committed
86
              }
Kevin's avatar
Kevin committed
87 88
            }
          });
Kevin's avatar
Kevin committed
89

90 91 92
          if (client != null) {
            await _sendPlatformMessage("flutter/textinput",
                _codec.encodeMethodCall(MethodCall('TextInput.hide')));
Kevin's avatar
Kevin committed
93
            return _codec.encodeSuccessEnvelope(null);
94 95
          } else {
            hideKeyboard(animation: false);
Kevin's avatar
Kevin committed
96 97 98 99
            clearKeyboard();
          }
          break;
      }
100
      ByteData response = await _sendPlatformMessage("flutter/textinput", data);
Kevin's avatar
Kevin committed
101 102 103 104
      return response;
    });
  }

105 106
  static Future<ByteData> _sendPlatformMessage(
      String channel, ByteData message) {
Kevin's avatar
Kevin committed
107 108 109 110 111 112 113 114 115
    final Completer<ByteData> completer = Completer<ByteData>();
    ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
      try {
        completer.complete(reply);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
116 117
          context:
              ErrorDescription('during a platform message response callback'),
Kevin's avatar
Kevin committed
118 119 120 121 122 123
        ));
      }
    });
    return completer.future;
  }

124
  static addKeyboard(CKTextInputType inputType, KeyboardConfig config) {
Kevin's avatar
Kevin committed
125 126 127
    _keyboards[inputType] = config;
  }

128
  static openKeyboard() {
Kevin's avatar
Kevin committed
129 130
    var keyboardHeight = _currentKeyboard.getHeight(_context);
    _keyboardHeightNotifier.value = keyboardHeight;
131 132
    if (_keyboardEntry != null) return;
    _pageKey = GlobalKey<KeyboardPageState>();
Kevin's avatar
Kevin committed
133 134 135 136
    // KeyboardMediaQueryState queryState = _context
    //         .ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
    //     as KeyboardMediaQueryState;
    // queryState.update();
Kevin's avatar
Kevin committed
137

Kevin's avatar
Kevin committed
138 139
    var tempKey = _pageKey;
    _keyboardEntry = OverlayEntry(builder: (ctx) {
Kevin's avatar
Kevin committed
140
      if (_currentKeyboard != null && _keyboardHeightNotifier.value != null) {
141
        return KeyboardPage(
Kevin's avatar
Kevin committed
142
            key: tempKey,
Kevin's avatar
Kevin committed
143
            builder: (ctx) {
144
              return _currentKeyboard?.builder(ctx, _keyboardController, _keyboardParam);
Kevin's avatar
Kevin committed
145 146
            },
            height: _keyboardHeightNotifier.value);
147
      } else {
Kevin's avatar
Kevin committed
148 149 150 151 152
        return Container();
      }
    });

    Overlay.of(_context).insert(_keyboardEntry);
153

154
    BackButtonInterceptor.add((_) {
155 156
      CoolKeyboard.sendPerformAction(TextInputAction.done);
      return true;
157
    }, zIndex: 1, name: 'CustomKeyboard');
Kevin's avatar
Kevin committed
158 159
  }

160
  static hideKeyboard({bool animation = true}) {
161
    BackButtonInterceptor.removeByName('CustomKeyboard');
162
    if (_keyboardEntry != null && _pageKey != null) {
Kevin's avatar
Kevin committed
163
      _keyboardHeightNotifier.value = null;
164 165 166 167 168 169 170 171 172 173 174 175 176
      // _pageKey.currentState.animationController
      //     .addStatusListener((AnimationStatus status) {
      //   if (status == AnimationStatus.dismissed ||
      //       status == AnimationStatus.completed) {
      //     if (_keyboardEntry != null) {
      //       _keyboardEntry.remove();
      //       _keyboardEntry = null;
      //     }
      //   }
      // });
      if (animation) {
        _pageKey.currentState.exitKeyboard();
        Future.delayed(Duration(milliseconds: 116)).then((_) {
Kevin's avatar
Kevin committed
177 178 179 180
          if (_keyboardEntry != null) {
            _keyboardEntry.remove();
            _keyboardEntry = null;
          }
181
        });
182
      } else {
Kevin's avatar
Kevin committed
183 184 185 186 187
        _keyboardEntry.remove();
        _keyboardEntry = null;
      }
    }
    _pageKey = null;
Kevin's avatar
Kevin committed
188 189 190 191 192 193
    try {
      // KeyboardMediaQueryState queryState = _context
      //     .ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
      // as KeyboardMediaQueryState;
      // queryState.update();
    } catch (_) {}
Kevin's avatar
Kevin committed
194 195
  }

196
  static clearKeyboard() {
Kevin's avatar
Kevin committed
197
    _currentKeyboard = null;
198
    if (_keyboardController != null) {
Kevin's avatar
Kevin committed
199 200 201 202 203
      _keyboardController.dispose();
      _keyboardController = null;
    }
  }

204
  static sendPerformAction(TextInputAction action) {
Kevin's avatar
Kevin committed
205
    var callbackMethodCall = MethodCall("TextInputClient.performAction",
206 207 208
        [_keyboardController.client.connectionId, action.toString()]);
    defaultBinaryMessenger.handlePlatformMessage("flutter/textinput",
        _codec.encodeMethodCall(callbackMethodCall), (data) {});
Kevin's avatar
Kevin committed
209
  }
210 211 212 213 214 215

  static updateKeyboardHeight() {
    if (_pageKey != null && _pageKey.currentState != null) {
      _pageKey.currentState.updateHeight(_keyboardHeightNotifier.value);
    }
  }
Kevin's avatar
Kevin committed
216 217
}

218
class KeyboardConfig {
Kevin's avatar
Kevin committed
219 220
  final KeyboardBuilder builder;
  final GetKeyboardHeight getHeight;
221
  const KeyboardConfig({this.builder, this.getHeight});
Kevin's avatar
Kevin committed
222 223
}

224
class InputClient {
Kevin's avatar
Kevin committed
225
  final int connectionId;
226 227
  final TextInputConfiguration configuration;
  const InputClient({this.connectionId, this.configuration});
Kevin's avatar
Kevin committed
228 229

  factory InputClient.fromJSON(List<dynamic> encoded) {
230 231 232 233 234 235 236 237 238 239 240 241
    return InputClient(
        connectionId: encoded[0],
        configuration: TextInputConfiguration(
            inputType: CKTextInputType.fromJSON(encoded[1]['inputType']),
            obscureText: encoded[1]['obscureText'],
            autocorrect: encoded[1]['autocorrect'],
            actionLabel: encoded[1]['actionLabel'],
            inputAction: _toTextInputAction(encoded[1]['inputAction']),
            textCapitalization:
                _toTextCapitalization(encoded[1]['textCapitalization']),
            keyboardAppearance:
                _toBrightness(encoded[1]['keyboardAppearance'])));
Kevin's avatar
Kevin committed
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
  }

  static TextInputAction _toTextInputAction(String action) {
    switch (action) {
      case 'TextInputAction.none':
        return TextInputAction.none;
      case 'TextInputAction.unspecified':
        return TextInputAction.unspecified;
      case 'TextInputAction.go':
        return TextInputAction.go;
      case 'TextInputAction.search':
        return TextInputAction.search;
      case 'TextInputAction.send':
        return TextInputAction.send;
      case 'TextInputAction.next':
        return TextInputAction.next;
      case 'TextInputAction.previuos':
        return TextInputAction.previous;
      case 'TextInputAction.continue_action':
        return TextInputAction.continueAction;
      case 'TextInputAction.join':
        return TextInputAction.join;
      case 'TextInputAction.route':
        return TextInputAction.route;
      case 'TextInputAction.emergencyCall':
        return TextInputAction.emergencyCall;
      case 'TextInputAction.done':
        return TextInputAction.done;
      case 'TextInputAction.newline':
        return TextInputAction.newline;
    }
    throw FlutterError('Unknown text input action: $action');
  }

276 277
  static TextCapitalization _toTextCapitalization(String capitalization) {
    switch (capitalization) {
Kevin's avatar
Kevin committed
278 279 280 281 282 283 284 285 286 287 288 289 290
      case 'TextCapitalization.none':
        return TextCapitalization.none;
      case 'TextCapitalization.characters':
        return TextCapitalization.characters;
      case 'TextCapitalization.sentences':
        return TextCapitalization.sentences;
      case 'TextCapitalization.words':
        return TextCapitalization.words;
    }

    throw FlutterError('Unknown text capitalization: $capitalization');
  }

291 292
  static Brightness _toBrightness(String brightness) {
    switch (brightness) {
Kevin's avatar
Kevin committed
293 294 295 296 297 298 299 300 301 302
      case 'Brightness.dark':
        return Brightness.dark;
      case 'Brightness.light':
        return Brightness.light;
    }

    throw FlutterError('Unknown Brightness: $brightness');
  }
}

303
class CKTextInputType extends TextInputType {
Kevin's avatar
Kevin committed
304
  final String name;
305
  final String params;
Kevin's avatar
Kevin committed
306

307
  const CKTextInputType({this.name, bool signed, bool decimal, this.params})
308
      : super.numberWithOptions(signed: signed, decimal: decimal);
Kevin's avatar
Kevin committed
309 310 311 312 313 314 315

  @override
  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'name': name,
      'signed': signed,
      'decimal': decimal,
316
      'params': params
Kevin's avatar
Kevin committed
317 318 319
    };
  }

320 321 322 323 324 325 326 327 328 329
  @override
  String toString() {
    return '$runtimeType('
        'name: $name, '
        'signed: $signed, '
        'decimal: $decimal)';
  }

  bool operator ==(Object target) {
    if (target is CKTextInputType) {
330
      if (this.name == target.toString()) {
331 332 333 334 335 336
        return true;
      }
    }
    return false;
  }

337 338 339
  @override
  int get hashCode => this.toString().hashCode;

340
  factory CKTextInputType.fromJSON(Map<String, dynamic> encoded) {
Kevin's avatar
Kevin committed
341 342 343
    return CKTextInputType(
        name: encoded['name'],
        signed: encoded['signed'],
344 345
        decimal: encoded['decimal'],
        params: encoded['params']);
Kevin's avatar
Kevin committed
346 347 348
  }
}

349
class KeyboardPage extends StatefulWidget {
Kevin's avatar
Kevin committed
350
  final WidgetBuilder builder;
Kevin's avatar
Kevin committed
351
  final double height;
Kevin's avatar
Kevin committed
352
  const KeyboardPage({this.builder, this.height, Key key}) : super(key: key);
Kevin's avatar
Kevin committed
353 354

  @override
355
  State<StatefulWidget> createState() => KeyboardPageState();
Kevin's avatar
Kevin committed
356 357
}

358
class KeyboardPageState extends State<KeyboardPage> {
Kevin's avatar
Kevin committed
359
  Widget _lastBuildWidget;
360 361
  bool isClose = false;
  double _height = 0;
Kevin's avatar
Kevin committed
362 363 364 365 366

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
Kevin's avatar
Kevin committed
367 368 369 370
    
    WidgetsBinding.instance.addPostFrameCallback((_){
      _height = widget.height;
      setState(()=>{});
371
    });
Kevin's avatar
Kevin committed
372 373 374 375
  }

  @override
  Widget build(BuildContext context) {
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
    return AnimatedPositioned(
      child: IntrinsicHeight(child: Builder(
        builder: (ctx) {
          var result = widget.builder(ctx);
          if (result != null) {
            _lastBuildWidget = result;
          }
          return ConstrainedBox(
            constraints: BoxConstraints(
                minHeight: 0,
                minWidth: 0,
                maxHeight: _height,
                maxWidth: _ScreenUtil.getScreenW(context)),
            child: _lastBuildWidget,
          );
        },
      )),
      left: 0,
      width: _ScreenUtil.getScreenW(context),
      bottom: _height * (isClose ? -1 : 0),
      height: _height,
      duration: Duration(milliseconds: 100),
    );
Kevin's avatar
Kevin committed
399 400 401 402
  }

  @override
  void dispose() {
403 404 405 406 407
    // if (animationController.status == AnimationStatus.forward ||
    //     animationController.status == AnimationStatus.reverse) {
    //   animationController.notifyStatusListeners(AnimationStatus.dismissed);
    // }
    // animationController.dispose();
408
    super.dispose();
Kevin's avatar
Kevin committed
409 410
  }

411
  exitKeyboard() {
412
    isClose = true;
Kevin's avatar
Kevin committed
413
  }
Kevin's avatar
Kevin committed
414 415

  update() {
Kevin's avatar
Kevin committed
416 417 418
    WidgetsBinding.instance.addPostFrameCallback((_){
      setState(()=>{});
    });
419 420 421
  }

  updateHeight(double height) {
Kevin's avatar
Kevin committed
422
    WidgetsBinding.instance.addPostFrameCallback((_){
423
      this._height = height ?? 0;
Kevin's avatar
Kevin committed
424 425
      setState(()=>{});
    });
Kevin's avatar
Kevin committed
426
  }
Kevin's avatar
Kevin committed
427
}