diff --git a/README-ZH.md b/README-ZH.md index 6e56018..8ddd8b7 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -270,8 +270,4 @@ Future main() async { [Full Demo](https://github.com/fluttercandies/extended_keyboard/blob/main/example/lib/src/pages/text_input_demo.dart) -![img](https://github.com/fluttercandies/flutter_candies/blob/master/gif/extended_keyboard/chat_demo1.gif) - -[Full Demo](https://github.com/fluttercandies/extended_keyboard/blob/main/example/lib/src/pages/chat_demo1.dart) - \ No newline at end of file diff --git a/README.md b/README.md index ba84c20..f2766be 100644 --- a/README.md +++ b/README.md @@ -271,9 +271,6 @@ if `Scaffold` is used, make sure set `Scaffold.resizeToAvoidBottomInset` to fals [Full Demo](https://github.com/fluttercandies/extended_keyboard/blob/main/example/lib/src/pages/text_input_demo.dart) -![img](https://github.com/fluttercandies/flutter_candies/blob/master/gif/extended_keyboard/chat_demo1.gif) - -[Full Demo](https://github.com/fluttercandies/extended_keyboard/blob/main/example/lib/src/pages/chat_demo1.dart) \ No newline at end of file diff --git a/example/lib/extended_keyboard_example_route.dart b/example/lib/extended_keyboard_example_route.dart index 9efcd37..189a9ba 100644 --- a/example/lib/extended_keyboard_example_route.dart +++ b/example/lib/extended_keyboard_example_route.dart @@ -11,7 +11,6 @@ import 'package:flutter/widgets.dart'; import 'src/main_page.dart'; import 'src/pages/chat_demo.dart'; -import 'src/pages/chat_demo1.dart'; import 'src/pages/text_input_demo.dart'; /// Get route settings base on route name, auto generated by https://github.com/fluttercandies/ff_annotation_route @@ -40,23 +39,6 @@ FFRouteSettings getRouteSettings({ 'group': 'Simple', }, ); - case 'fluttercandies://ChatDemo1': - return FFRouteSettings( - name: name, - arguments: arguments, - builder: () => ChatDemo1( - key: asT( - safeArguments['key'], - ), - ), - routeName: 'ChatDemo1', - description: - 'Show how to build custom keyboard with TextInputScope quickly', - exts: { - 'order': 2, - 'group': 'Simple', - }, - ); case 'fluttercandies://TextInputScope': return FFRouteSettings( name: name, diff --git a/example/lib/extended_keyboard_example_routes.dart b/example/lib/extended_keyboard_example_routes.dart index 52f421a..c6434e9 100644 --- a/example/lib/extended_keyboard_example_routes.dart +++ b/example/lib/extended_keyboard_example_routes.dart @@ -9,7 +9,6 @@ /// The routeNames auto generated by https://github.com/fluttercandies/ff_annotation_route const List routeNames = [ 'fluttercandies://ChatDemo', - 'fluttercandies://ChatDemo1', 'fluttercandies://TextInputScope', 'fluttercandies://demogrouppage', 'fluttercandies://mainpage', @@ -34,21 +33,6 @@ class Routes { /// [exts] : {'order': 0, 'group': 'Simple'} static const String fluttercandiesChatDemo = 'fluttercandies://ChatDemo'; - /// 'Show how to build custom keyboard with TextInputScope quickly' - /// - /// [name] : 'fluttercandies://ChatDemo1' - /// - /// [routeName] : 'ChatDemo1' - /// - /// [description] : 'Show how to build custom keyboard with TextInputScope quickly' - /// - /// [constructors] : - /// - /// ChatDemo1 : [Key? key] - /// - /// [exts] : {'order': 2, 'group': 'Simple'} - static const String fluttercandiesChatDemo1 = 'fluttercandies://ChatDemo1'; - /// 'Show how to build different custom keyboard with TextInputScope quickly' /// /// [name] : 'fluttercandies://TextInputScope' diff --git a/example/lib/src/pages/chat_demo1.dart b/example/lib/src/pages/chat_demo1.dart deleted file mode 100644 index 911663a..0000000 --- a/example/lib/src/pages/chat_demo1.dart +++ /dev/null @@ -1,589 +0,0 @@ -import 'dart:math'; - -import 'package:extended_image/extended_image.dart'; -import 'package:extended_keyboard/extended_keyboard.dart'; -import 'package:extended_keyboard_example/assets.dart'; -import 'package:extended_keyboard_example/src/widget/button.dart'; -import 'package:extended_text/extended_text.dart'; -import 'package:extended_text_field/extended_text_field.dart'; -import 'package:ff_annotation_route_library/ff_annotation_route_library.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:loading_more_list/loading_more_list.dart'; -import '../data/tu_chong_repository.dart'; -import '../data/tu_chong_source.dart'; -import '../special_text/emoji_text.dart' as emoji; -import '../special_text/my_special_text_span_builder.dart'; -import '../widget/toggle_button.dart'; - -enum KeyboardPanelType { - number, - emoji, - image, -} - -@FFRoute( - name: 'fluttercandies://ChatDemo1', - routeName: 'ChatDemo1', - description: 'Show how to build custom keyboard with TextInputScope quickly', - exts: { - 'order': 2, - 'group': 'Simple', - }, -) -class ChatDemo1 extends StatefulWidget { - const ChatDemo1({Key? key}) : super(key: key); - - @override - State createState() => _ChatDemo1State(); -} - -class _ChatDemo1State extends State { - KeyboardPanelType _keyboardPanelType = KeyboardPanelType.number; - final GlobalKey _key = - GlobalKey(); - final TextEditingController _textEditingController = TextEditingController(); - final MySpecialTextSpanBuilder _mySpecialTextSpanBuilder = - MySpecialTextSpanBuilder(); - late TuChongRepository imageList = TuChongRepository(maxLength: 100); - final ScrollController _controller = ScrollController(); - final FocusNode _focusNode = FocusNode(); - - final List _messages = []; - - late List _configurations; - - Duration duration = const Duration(milliseconds: 300); - - @override - void initState() { - super.initState(); - - _configurations = [ - for (int i = 0; i < KeyboardPanelType.values.length; i++) - KeyboardConfiguration( - getKeyboardHeight: (double? systemKeyboardHeight) => - systemKeyboardHeight ?? 346, - builder: () { - return _buildCustomKeyboard( - KeyboardPanelType.values[i], - ); - }, - keyboardName: KeyboardPanelType.values[i].toString(), - showDuration: duration, - hideDuration: duration, - ), - ]; - _focusNode.addListener(_onFocusChanged); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar(title: const Text('ChatDemo(TextInputBuilder)')), - body: SafeArea( - bottom: true, - child: TextInputScope( - resizeToAvoidBottomInset: true, - body: Column(children: [ - Expanded( - child: KeyboardDismisser( - child: ListView.builder( - controller: _controller, - itemBuilder: (BuildContext context, int index) { - final Message message = _messages[index]; - List children = [ - ExtendedImage.asset( - Assets.assets_avatar_jpg, - width: 20, - height: 20, - ), - const SizedBox(width: 5), - Flexible( - child: ExtendedText( - message.content, - specialTextSpanBuilder: _mySpecialTextSpanBuilder, - maxLines: 10, - ), - ), - ]; - if (message.isMe) { - children = children.reversed.toList(); - } - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: message.isMe - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: children, - ), - ); - }, - itemCount: _messages.length, - ), - ), - ), - Container( - padding: const EdgeInsets.all(5), - decoration: const BoxDecoration( - border: Border( - top: BorderSide( - color: Colors.grey, - ), - bottom: BorderSide( - color: Colors.grey, - ), - ), - ), - child: Row( - children: [ - Expanded( - child: ExtendedTextField( - key: _key, - specialTextSpanBuilder: _mySpecialTextSpanBuilder, - controller: _textEditingController, - focusNode: _focusNode, - textInputAction: TextInputAction.done, - strutStyle: const StrutStyle(), - keyboardType: _getKeyboardType(), - decoration: InputDecoration( - hintText: 'Input something', - border: InputBorder.none, - suffixIcon: GestureDetector( - onTap: () { - setState(() { - sendMessage(_textEditingController.text); - _textEditingController.clear(); - }); - }, - child: const Icon(Icons.send), - ), - contentPadding: const EdgeInsets.all( - 12.0, - ), - ), - maxLines: 2, - onTap: () { - setState(() { - _keyboardPanelType = KeyboardPanelType.number; - }); - }, - ), - ), - Row( - children: [ - ToggleButton( - builder: (bool active) => Icon( - Icons.sentiment_very_satisfied, - color: active ? Colors.orange : null, - ), - activeChanged: (bool active) { - setState(() { - _keyboardPanelType = KeyboardPanelType.emoji; - if (active) { - _focusNode.requestFocus(); - } else { - _keyboardPanelType = KeyboardPanelType.number; - _focusNode.unfocus(); - } - }); - }, - active: _keyboardPanelType == KeyboardPanelType.emoji, - ), - ToggleButton( - builder: (bool active) => Icon( - Icons.image, - color: active ? Colors.orange : null, - ), - activeChanged: (bool active) { - setState(() { - _keyboardPanelType = KeyboardPanelType.image; - if (active) { - _focusNode.requestFocus(); - } else { - _keyboardPanelType = KeyboardPanelType.number; - _focusNode.unfocus(); - } - }); - }, - active: _keyboardPanelType == KeyboardPanelType.image, - ), - ], - ), - ], - ), - ), - ]), - configurations: _configurations, - ), - ), - ); - } - - Widget _buildCustomKeyboard( - KeyboardPanelType keyboardPanelType, - ) { - switch (_keyboardPanelType) { - case KeyboardPanelType.number: - return Material( - child: Container( - padding: const EdgeInsets.only( - left: 10, - right: 10, - top: 20, - bottom: 20, - ), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.blueAccent, Colors.lightBlueAccent], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 10, - offset: Offset(0, 4), - ), - ], - ), - child: IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - flex: 15, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Row( - children: [ - Expanded( - flex: 5, - child: NumberButton( - number: 1, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 2, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 3, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - ], - ), - ), - Expanded( - child: Row( - children: [ - Expanded( - flex: 5, - child: NumberButton( - number: 4, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 5, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 6, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - ], - ), - ), - Expanded( - child: Row( - children: [ - Expanded( - flex: 5, - child: NumberButton( - number: 7, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 8, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 9, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - ], - ), - ), - Expanded( - child: Row( - children: [ - Expanded( - flex: 5, - child: CustomButton( - child: const Text('.'), - onTap: () { - _textEditingController.insertText('.'); - }, - ), - ), - Expanded( - flex: 5, - child: NumberButton( - number: 0, - insertText: (String text) => - _textEditingController.insertText(text), - ), - ), - Expanded( - flex: 5, - child: CustomButton( - child: const Icon(Icons.arrow_downward), - onTap: () { - FocusManager.instance.primaryFocus - ?.unfocus(); - }, - ), - ), - ], - ), - ), - ], - ), - ), - Expanded( - flex: 7, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: CustomButton( - child: const Icon(Icons.backspace), - onTap: () { - delete(); - }, - ), - ), - Expanded( - child: CustomButton( - child: Text(TextInputAction.done.name), - onTap: () { - _textEditingController - .performAction(TextInputAction.done); - }, - )) - ], - ), - ), - ], - ), - ), - ), - ); - case KeyboardPanelType.emoji: - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 8, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0), - itemBuilder: (BuildContext context, int index) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - insertText('[${index + 1}]'); - }, - child: Image.asset( - emoji.EmojiUitl.instance.emojiMap['[${index + 1}]']!), - ); - }, - itemCount: emoji.EmojiUitl.instance.emojiMap.length, - padding: const EdgeInsets.all(5.0), - ); - case KeyboardPanelType.image: - return LoadingMoreList( - ListConfig( - sourceList: imageList, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, - ), - itemBuilder: (BuildContext context, TuChongItem item, int index) { - final String url = item.imageUrl; - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - // - setState(() { - sendMessage( - ""); - }); - }, - child: ExtendedImage.network( - url, - ), - ); - }, - padding: const EdgeInsets.all(5.0), - ), - ); - default: - } - return Container(); - } - - void insertText(String text) { - _textEditingController.insertText(text); - - SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - _key.currentState?.bringIntoView(_textEditingController.selection.base); - }); - } - - void delete() { - final TextSpan oldTextSpan = - _mySpecialTextSpanBuilder.build(_textEditingController.text); - - final TextEditingValue value = handleSpecialTextSpanDelete( - _textEditingController.deleteText(), - _textEditingController.value, - oldTextSpan, - null, - ); - _textEditingController.value = value; - } - - void sendMessage(String text) { - if (text.isEmpty) { - return; - } - setState(() { - _messages.add(Message(content: text)); - }); - SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - _controller.animateTo( - _controller.position.maxScrollExtent, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ); - }); - } - - TextInputType _getKeyboardType() { - for (final KeyboardConfiguration element in _configurations) { - if (element.keyboardType.name == _keyboardPanelType.toString()) { - return element.keyboardType; - } - } - return _configurations.first.keyboardType; - } - - void _onFocusChanged() { - if (!_focusNode.hasFocus && - _keyboardPanelType != KeyboardPanelType.number) { - Future.delayed(duration, () { - if (mounted) { - setState(() { - _keyboardPanelType = KeyboardPanelType.number; - }); - } - }); - } - } -} - -class Message { - Message({ - required this.content, - }) : isMe = Random().nextBool(); - final bool isMe; - final String content; -} - -TextEditingValue handleSpecialTextSpanDelete( - TextEditingValue value, - TextEditingValue? oldValue, - InlineSpan oldTextSpan, - TextInputConnection? textInputConnection) { - final String? oldText = oldValue?.text; - String newText = value.text; - - /// take care of image span - if (oldText != null && oldText.length > newText.length) { - final int difStart = value.selection.extentOffset; - //int difEnd = oldText.length - 1; - // for (; difStart < newText.length; difStart++) { - // if (oldText[difStart] != newText[difStart]) { - // break; - // } - // } - - int caretOffset = value.selection.extentOffset; - if (difStart > 0) { - oldTextSpan.visitChildren((InlineSpan span) { - if (span is SpecialInlineSpanBase && - (span as SpecialInlineSpanBase).deleteAll) { - final SpecialInlineSpanBase specialTs = span as SpecialInlineSpanBase; - if (difStart > specialTs.start && difStart < specialTs.end) { - //difStart = ts.start; - newText = newText.replaceRange(specialTs.start, difStart, ''); - caretOffset -= difStart - specialTs.start; - return false; - } - } - return true; - }); - - if (newText != value.text) { - value = TextEditingValue( - text: newText, - selection: value.selection.copyWith( - baseOffset: caretOffset, - extentOffset: caretOffset, - affinity: value.selection.affinity, - isDirectional: value.selection.isDirectional)); - textInputConnection?.setEditingState(value); - } - } - } - - return value; -} diff --git a/lib/src/text_input/keyboard_binding.dart b/lib/src/text_input/keyboard_binding.dart index eb3ad25..f678521 100644 --- a/lib/src/text_input/keyboard_binding.dart +++ b/lib/src/text_input/keyboard_binding.dart @@ -28,7 +28,7 @@ mixin KeyboardBindingMixin on WidgetsFlutterBinding { , List>{}; /// Notifier that updates when a custom keyboard needs to be shown. - ValueNotifier showKeyboardNotifier = + ValueNotifier keyboardConfigurationNotifier = ValueNotifier(null); /// Registers a route with its corresponding keyboard configurations. @@ -83,25 +83,25 @@ mixin KeyboardBindingMixin on WidgetsFlutterBinding { switch (methodCall.method) { case 'TextInput.show': if (keyboardConfiguration != null) { - showKeyboardNotifier.value = keyboardConfiguration; + keyboardConfigurationNotifier.value = keyboardConfiguration; return codec.encodeSuccessEnvelope(null); } break; case 'TextInput.hide': - showKeyboardNotifier.value = keyboardConfiguration; + keyboardConfigurationNotifier.value = keyboardConfiguration; break; case 'TextInput.clearClient': _connectionId = null; _name = null; if (keyboardConfiguration != null) { - showKeyboardNotifier.value = keyboardConfiguration; + keyboardConfigurationNotifier.value = keyboardConfiguration; _keyboardConfiguration = null; return codec.encodeSuccessEnvelope(null); } break; case 'TextInput.setClient': - _connectionId = methodCall.arguments[0] as int; - _name = methodCall.arguments[1]['inputType']['name'] as String; + _connectionId = methodCall.arguments[0] as int?; + _name = methodCall.arguments[1]['inputType']['name'] as String?; for (final ModalRoute route in _configurations.keys) { if (route.isCurrent) { @@ -117,6 +117,23 @@ mixin KeyboardBindingMixin on WidgetsFlutterBinding { } } break; + case 'TextInput.updateConfig': + _name = methodCall.arguments['inputType']['name'] as String?; + for (final ModalRoute route in _configurations.keys) { + if (route.isCurrent) { + final List configs = + _configurations[route]!; + for (final KeyboardConfiguration config in configs) { + if (_name == config.keyboardType.name) { + _keyboardConfiguration = config; + _hideSystemKeyBoardIfNeed(); + return codec.encodeSuccessEnvelope(null); + } + } + } + } + break; + default: } } diff --git a/lib/src/text_input/text_input_scope.dart b/lib/src/text_input/text_input_scope.dart index b69afd4..65b94ce 100644 --- a/lib/src/text_input/text_input_scope.dart +++ b/lib/src/text_input/text_input_scope.dart @@ -107,7 +107,8 @@ class _TextInputScopeState extends State { @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: KeyboardBindingMixin.binding.showKeyboardNotifier, + valueListenable: + KeyboardBindingMixin.binding.keyboardConfigurationNotifier, builder: ( BuildContext context, KeyboardConfiguration? configuration,