keyboard_manager.dart 14.5 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(
Kevin's avatar
Kevin committed
5
    BuildContext context, KeyboardController controller, String? param);
Kevin's avatar
Kevin committed
6 7 8

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

Kevin's avatar
Kevin committed
17
  static ValueNotifier<double> _keyboardHeightNotifier = ValueNotifier(0)
18 19
    ..addListener(updateKeyboardHeight);

Kevin's avatar
Kevin committed
20
  static String? _keyboardParam;
Kevin's avatar
Kevin committed
21

Kevin's avatar
Kevin committed
22
  static Timer? clearTask;
Kevin's avatar
Kevin committed
23

24 25
  static init(KeyboardRootState root, BuildContext context) {
    _root = root;
Kevin's avatar
Kevin committed
26 27 28 29
    _context = context;
    interceptorInput();
  }

30 31
  static interceptorInput() {
    if (isInterceptor) return;
32 33 34
    if (!(ServicesBinding.instance is MockBinding)) {
      throw Exception('CoolKeyboard can only be used in MockBinding');
    }
Kevin's avatar
Kevin committed
35 36 37 38 39
    var mockBinding = ServicesBinding.instance as MockBinding;
    var mockBinaryMessenger =
        mockBinding.defaultBinaryMessenger as MockBinaryMessenger;
    mockBinaryMessenger.setMockMessageHandler(
        "flutter/textinput", _textInputHanlde);
40
    isInterceptor = true;
41 42 43 44 45 46 47 48 49 50
  }

  static Future<ByteData?> _textInputHanlde(ByteData? data) async {
    var methodCall = _codec.decodeMethodCall(data);
    switch (methodCall.method) {
      case 'TextInput.show':
        if (_currentKeyboard != null) {
          if (clearTask != null) {
            clearTask!.cancel();
            clearTask = null;
Kevin's avatar
Kevin committed
51
          }
52 53 54 55 56
          openKeyboard();
          return _codec.encodeSuccessEnvelope(null);
        } else {
          if (data != null) {
            return await _sendPlatformMessage("flutter/textinput", data);
Kevin's avatar
Kevin committed
57
          }
58 59 60 61
        }
        break;
      case 'TextInput.hide':
        if (_currentKeyboard != null) {
62 63 64
          if (clearTask == null) {
            clearTask = new Timer(Duration(milliseconds: 16),
                () => hideKeyboard(animation: true));
Kevin's avatar
Kevin committed
65
          }
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
          return _codec.encodeSuccessEnvelope(null);
        } else {
          if (data != null) {
            return await _sendPlatformMessage("flutter/textinput", data);
          }
        }
        break;
      case 'TextInput.setEditingState':
        var editingState = TextEditingValue.fromJSON(methodCall.arguments);
        if (_keyboardController != null) {
          _keyboardController!.value = editingState;
          return _codec.encodeSuccessEnvelope(null);
        }
        break;
      case 'TextInput.clearClient':
        var isShow = _currentKeyboard != null;
        if (clearTask == null) {
          clearTask = new Timer(
              Duration(milliseconds: 16), () => hideKeyboard(animation: true));
        }
        clearKeyboard();
        if (isShow) {
          return _codec.encodeSuccessEnvelope(null);
        }
        break;
      case 'TextInput.setClient':
        var setInputType = methodCall.arguments[1]['inputType'];
        InputClient? client;
        _keyboards.forEach((inputType, keyboardConfig) {
          if (inputType.name == setInputType['name']) {
            client = InputClient.fromJSON(methodCall.arguments);

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

Kevin's avatar
Kevin committed
101
            clearKeyboard();
102 103
            _currentKeyboard = keyboardConfig;
            _keyboardController = KeyboardController(client: client!)
104
              ..addListener(_updateEditingState);
105 106 107
            if (_pageKey != null) {
              _pageKey!.currentState?.update();
            }
Kevin's avatar
Kevin committed
108
          }
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
        });

        if (client != null) {
          await _sendPlatformMessage("flutter/textinput",
              _codec.encodeMethodCall(MethodCall('TextInput.hide')));
          return _codec.encodeSuccessEnvelope(null);
        } else {
          if (clearTask == null) {
            hideKeyboard(animation: false);
          }
          clearKeyboard();
        }
      // break;
    }
    if (data != null) {
      ByteData? response =
          await _sendPlatformMessage("flutter/textinput", data);
      return response;
    }
    return null;
Kevin's avatar
Kevin committed
129 130
  }

131
  static void _updateEditingState() {
Kevin's avatar
Kevin committed
132
    var callbackMethodCall = MethodCall("TextInputClient.updateEditingState", [
133 134 135
      _keyboardController!.client.connectionId,
      _keyboardController!.value.toJSON()
    ]);
Kevin's avatar
Kevin committed
136 137 138 139
    WidgetsBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
        "flutter/textinput",
        _codec.encodeMethodCall(callbackMethodCall),
        (data) {});
140 141
  }

142
  static Future<ByteData?> _sendPlatformMessage(
143
      String channel, ByteData message) {
144
    final Completer<ByteData?> completer = Completer<ByteData?>();
Kevin's avatar
Kevin committed
145
    ui.window.sendPlatformMessage(channel, message, (ByteData? reply) {
Kevin's avatar
Kevin committed
146 147 148 149 150 151 152
      try {
        completer.complete(reply);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
153 154
          context:
              ErrorDescription('during a platform message response callback'),
Kevin's avatar
Kevin committed
155 156 157 158 159 160
        ));
      }
    });
    return completer.future;
  }

161
  static addKeyboard(CKTextInputType inputType, KeyboardConfig config) {
Kevin's avatar
Kevin committed
162 163 164
    _keyboards[inputType] = config;
  }

165
  static openKeyboard() {
Kevin's avatar
Kevin committed
166
    var keyboardHeight = _currentKeyboard!.getHeight(_context!);
Kevin's avatar
Kevin committed
167
    _keyboardHeightNotifier.value = keyboardHeight;
Kevin's avatar
Kevin committed
168
    if (_root!.hasKeyboard && _pageKey != null) return;
169
    _pageKey = GlobalKey<KeyboardPageState>();
Kevin's avatar
Kevin committed
170 171 172 173
    // KeyboardMediaQueryState queryState = _context
    //         .ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
    //     as KeyboardMediaQueryState;
    // queryState.update();
Kevin's avatar
Kevin committed
174

Kevin's avatar
Kevin committed
175
    var tempKey = _pageKey;
176
    var isUpdate = false;
Kevin's avatar
Kevin committed
177
    _root!.setKeyboard((ctx) {
178
      if (_currentKeyboard != null && _keyboardHeightNotifier.value != 0) {
179 180
        if (!isUpdate) {
          isUpdate = true;
Kevin's avatar
Kevin committed
181
          // WidgetsBinding.instance.addPostFrameCallback((_) {
182 183 184
          //   _keyboardController!.addText('1');
          // });
        }
185
        return KeyboardPage(
Kevin's avatar
Kevin committed
186
            key: tempKey,
Kevin's avatar
Kevin committed
187
            builder: (ctx) {
Kevin's avatar
Kevin committed
188 189
              return _currentKeyboard?.builder(
                  ctx, _keyboardController!, _keyboardParam);
Kevin's avatar
Kevin committed
190 191
            },
            height: _keyboardHeightNotifier.value);
192
      } else {
Kevin's avatar
Kevin committed
193 194 195 196
        return Container();
      }
    });

197
    BackButtonInterceptor.add((_, __) {
198 199
      CoolKeyboard.sendPerformAction(TextInputAction.done);
      return true;
200
    }, zIndex: 1, name: 'CustomKeyboard');
Kevin's avatar
Kevin committed
201 202
  }

203
  static hideKeyboard({bool animation = true}) {
Kevin's avatar
Kevin committed
204
    if (clearTask != null) {
Kevin's avatar
Kevin committed
205 206
      if (clearTask!.isActive) {
        clearTask!.cancel();
Kevin's avatar
Kevin committed
207 208 209
      }
      clearTask = null;
    }
210
    BackButtonInterceptor.removeByName('CustomKeyboard');
Kevin's avatar
Kevin committed
211
    if (_root!.hasKeyboard && _pageKey != null) {
212 213 214 215
      // _pageKey.currentState.animationController
      //     .addStatusListener((AnimationStatus status) {
      //   if (status == AnimationStatus.dismissed ||
      //       status == AnimationStatus.completed) {
216
      //     if (_root.hasKeyboard) {
217 218 219 220 221 222
      //       _keyboardEntry.remove();
      //       _keyboardEntry = null;
      //     }
      //   }
      // });
      if (animation) {
223
        _pageKey!.currentState?.exitKeyboard();
224
        Future.delayed(Duration(milliseconds: 116)).then((_) {
Kevin's avatar
Kevin committed
225
          _root!.clearKeyboard();
226
        });
227
      } else {
Kevin's avatar
Kevin committed
228
        _root!.clearKeyboard();
Kevin's avatar
Kevin committed
229 230 231
      }
    }
    _pageKey = null;
Kevin's avatar
Kevin committed
232
    _keyboardHeightNotifier.value = 0;
Kevin's avatar
Kevin committed
233 234 235 236 237 238
    try {
      // KeyboardMediaQueryState queryState = _context
      //     .ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
      // as KeyboardMediaQueryState;
      // queryState.update();
    } catch (_) {}
Kevin's avatar
Kevin committed
239 240
  }

241
  static clearKeyboard() {
Kevin's avatar
Kevin committed
242
    _currentKeyboard = null;
243
    if (_keyboardController != null) {
Kevin's avatar
Kevin committed
244
      _keyboardController!.dispose();
Kevin's avatar
Kevin committed
245 246 247 248
      _keyboardController = null;
    }
  }

249
  static sendPerformAction(TextInputAction action) {
Kevin's avatar
Kevin committed
250
    var callbackMethodCall = MethodCall("TextInputClient.performAction",
Kevin's avatar
Kevin committed
251
        [_keyboardController!.client.connectionId, action.toString()]);
Kevin's avatar
Kevin committed
252
    WidgetsBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
253 254 255
        "flutter/textinput",
        _codec.encodeMethodCall(callbackMethodCall),
        (data) {});
Kevin's avatar
Kevin committed
256
  }
257 258

  static updateKeyboardHeight() {
259
    if (_pageKey != null &&
Kevin's avatar
Kevin committed
260
        _pageKey!.currentState != null &&
261
        clearTask == null) {
Kevin's avatar
Kevin committed
262
      _pageKey!.currentState!.updateHeight(_keyboardHeightNotifier.value);
263 264
    }
  }
Kevin's avatar
Kevin committed
265 266
}

267
class KeyboardConfig {
Kevin's avatar
Kevin committed
268 269
  final KeyboardBuilder builder;
  final GetKeyboardHeight getHeight;
270
  const KeyboardConfig({required this.builder, required this.getHeight});
Kevin's avatar
Kevin committed
271 272
}

273
class InputClient {
Kevin's avatar
Kevin committed
274
  final int connectionId;
275
  final TextInputConfiguration configuration;
276
  const InputClient({required this.connectionId, required this.configuration});
Kevin's avatar
Kevin committed
277 278

  factory InputClient.fromJSON(List<dynamic> encoded) {
279 280 281 282 283 284 285 286 287 288 289 290
    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
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  }

  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');
  }

325 326
  static TextCapitalization _toTextCapitalization(String capitalization) {
    switch (capitalization) {
Kevin's avatar
Kevin committed
327 328 329 330 331 332 333 334 335 336 337 338 339
      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');
  }

340 341
  static Brightness _toBrightness(String brightness) {
    switch (brightness) {
Kevin's avatar
Kevin committed
342 343 344 345 346 347 348 349 350 351
      case 'Brightness.dark':
        return Brightness.dark;
      case 'Brightness.light':
        return Brightness.light;
    }

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

352
class CKTextInputType extends TextInputType {
Kevin's avatar
Kevin committed
353
  final String name;
Kevin's avatar
Kevin committed
354
  final String? params;
Kevin's avatar
Kevin committed
355

356 357
  const CKTextInputType(
      {required this.name, bool? signed, bool? decimal, this.params})
358
      : super.numberWithOptions(signed: signed, decimal: decimal);
Kevin's avatar
Kevin committed
359 360 361 362 363 364 365

  @override
  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'name': name,
      'signed': signed,
      'decimal': decimal,
366
      'params': params
Kevin's avatar
Kevin committed
367 368 369
    };
  }

370 371 372 373 374 375 376 377 378 379
  @override
  String toString() {
    return '$runtimeType('
        'name: $name, '
        'signed: $signed, '
        'decimal: $decimal)';
  }

  bool operator ==(Object target) {
    if (target is CKTextInputType) {
380
      if (this.name == target.toString()) {
381 382 383 384 385 386
        return true;
      }
    }
    return false;
  }

387 388 389
  @override
  int get hashCode => this.toString().hashCode;

390
  factory CKTextInputType.fromJSON(Map<String, dynamic> encoded) {
Kevin's avatar
Kevin committed
391 392 393
    return CKTextInputType(
        name: encoded['name'],
        signed: encoded['signed'],
394 395
        decimal: encoded['decimal'],
        params: encoded['params']);
Kevin's avatar
Kevin committed
396 397 398
  }
}

399
class KeyboardPage extends StatefulWidget {
400
  final Widget? Function(BuildContext context) builder;
Kevin's avatar
Kevin committed
401
  final double height;
402 403
  const KeyboardPage({required this.builder, this.height = 0, Key? key})
      : super(key: key);
Kevin's avatar
Kevin committed
404 405

  @override
406
  State<StatefulWidget> createState() => KeyboardPageState();
Kevin's avatar
Kevin committed
407 408
}

409
class KeyboardPageState extends State<KeyboardPage> {
Kevin's avatar
Kevin committed
410
  Widget? _lastBuildWidget;
411 412
  bool isClose = false;
  double _height = 0;
Kevin's avatar
Kevin committed
413 414 415 416 417

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
Kevin's avatar
Kevin committed
418

Kevin's avatar
Kevin committed
419
    WidgetsBinding.instance.addPostFrameCallback((_) {
Kevin's avatar
Kevin committed
420
      _height = widget.height;
Kevin's avatar
Kevin committed
421
      setState(() => {});
422
    });
Kevin's avatar
Kevin committed
423 424 425 426
  }

  @override
  Widget build(BuildContext context) {
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
    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
450 451 452 453
  }

  @override
  void dispose() {
454 455 456 457 458
    // if (animationController.status == AnimationStatus.forward ||
    //     animationController.status == AnimationStatus.reverse) {
    //   animationController.notifyStatusListeners(AnimationStatus.dismissed);
    // }
    // animationController.dispose();
459
    super.dispose();
Kevin's avatar
Kevin committed
460 461
  }

462
  exitKeyboard() {
463
    isClose = true;
Kevin's avatar
Kevin committed
464
  }
Kevin's avatar
Kevin committed
465 466

  update() {
Kevin's avatar
Kevin committed
467
    WidgetsBinding.instance.addPostFrameCallback((_) {
Kevin's avatar
Kevin committed
468
      setState(() => {});
Kevin's avatar
Kevin committed
469
    });
470 471 472
  }

  updateHeight(double height) {
Kevin's avatar
Kevin committed
473
    WidgetsBinding.instance.addPostFrameCallback((_) {
Kevin's avatar
Kevin committed
474
      this._height = height;
Kevin's avatar
Kevin committed
475
      setState(() => {});
Kevin's avatar
Kevin committed
476
    });
Kevin's avatar
Kevin committed
477
  }
Kevin's avatar
Kevin committed
478
}