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

3

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

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

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

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

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

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

31 32
  static interceptorInput() {
    if (isInterceptor) return;
33 34 35 36 37 38
    if (!(ServicesBinding.instance is MockBinding)) {
      throw Exception('CoolKeyboard can only be used in MockBinding');
    }
    var mockBinding = ServicesBinding.instance! as MockBinding;
    var mockBinaryMessenger = mockBinding.defaultBinaryMessenger as MockBinaryMessenger; 
    mockBinaryMessenger
39
        .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 132 133 134 135 136 137 138 139 140 141
  static void _updateEditingState() {
    var callbackMethodCall = MethodCall(
        "TextInputClient.updateEditingState", [
      _keyboardController!.client.connectionId,
      _keyboardController!.value.toJSON()
    ]);
    ServicesBinding.instance!.defaultBinaryMessenger
        .handlePlatformMessage("flutter/textinput",
            _codec.encodeMethodCall(callbackMethodCall), (data) {});
  }

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 181 182 183 184
        if (!isUpdate) {
          isUpdate = true;
          // WidgetsBinding.instance!.addPostFrameCallback((_) {
          //   _keyboardController!.addText('1');
          // });
        }
185
        return KeyboardPage(
Kevin's avatar
Kevin committed
186
            key: tempKey,
Kevin's avatar
Kevin committed
187
            builder: (ctx) {
188
              return _currentKeyboard?.builder(ctx, _keyboardController!, _keyboardParam);
Kevin's avatar
Kevin committed
189 190
            },
            height: _keyboardHeightNotifier.value);
191
      } else {
Kevin's avatar
Kevin committed
192 193 194 195
        return Container();
      }
    });

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

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

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

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

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

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

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

  factory InputClient.fromJSON(List<dynamic> encoded) {
278 279 280 281 282 283 284 285 286 287 288 289
    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
290 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
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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