Skip to content

Instantly share code, notes, and snippets.

@suragch
Last active May 31, 2023 19:33
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save suragch/47149f1083fb7a405cdfd18084054c39 to your computer and use it in GitHub Desktop.
Save suragch/47149f1083fb7a405cdfd18084054c39 to your computer and use it in GitHub Desktop.
Custom In-App Keyboard in Flutter
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: KeyboardDemo(),
);
}
}
class KeyboardDemo extends StatefulWidget {
@override
_KeyboardDemoState createState() => _KeyboardDemoState();
}
class _KeyboardDemoState extends State<KeyboardDemo> {
TextEditingController _controller = TextEditingController();
bool _readOnly = true;
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Column(
children: [
SizedBox(height: 50),
TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(3),
),
),
style: TextStyle(fontSize: 24),
autofocus: true,
showCursor: true,
readOnly: _readOnly,
),
IconButton(
icon: Icon(Icons.keyboard),
onPressed: () {
setState(() {
_readOnly = !_readOnly;
});
},
),
Spacer(),
CustomKeyboard(
onTextInput: (myText) {
_insertText(myText);
},
onBackspace: () {
_backspace();
},
),
],
),
);
}
void _insertText(String myText) {
final text = _controller.text;
final textSelection = _controller.selection;
final newText = text.replaceRange(
textSelection.start,
textSelection.end,
myText,
);
final myTextLength = myText.length;
_controller.text = newText;
_controller.selection = textSelection.copyWith(
baseOffset: textSelection.start + myTextLength,
extentOffset: textSelection.start + myTextLength,
);
}
void _backspace() {
final text = _controller.text;
final textSelection = _controller.selection;
final selectionLength = textSelection.end - textSelection.start;
// There is a selection.
if (selectionLength > 0) {
final newText = text.replaceRange(
textSelection.start,
textSelection.end,
'',
);
_controller.text = newText;
_controller.selection = textSelection.copyWith(
baseOffset: textSelection.start,
extentOffset: textSelection.start,
);
return;
}
// The cursor is at the beginning.
if (textSelection.start == 0) {
return;
}
// Delete the previous character
final previousCodeUnit = text.codeUnitAt(textSelection.start - 1);
final offset = _isUtf16Surrogate(previousCodeUnit) ? 2 : 1;
final newStart = textSelection.start - offset;
final newEnd = textSelection.start;
final newText = text.replaceRange(
newStart,
newEnd,
'',
);
_controller.text = newText;
_controller.selection = textSelection.copyWith(
baseOffset: newStart,
extentOffset: newStart,
);
}
bool _isUtf16Surrogate(int value) {
return value & 0xF800 == 0xD800;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class CustomKeyboard extends StatelessWidget {
CustomKeyboard({
Key key,
this.onTextInput,
this.onBackspace,
}) : super(key: key);
final ValueSetter<String> onTextInput;
final VoidCallback onBackspace;
void _textInputHandler(String text) => onTextInput?.call(text);
void _backspaceHandler() => onBackspace?.call();
@override
Widget build(BuildContext context) {
return Container(
height: 160,
color: Colors.blue,
child: Column(
children: [
buildRowOne(),
buildRowTwo(),
buildRowThree(),
],
),
);
}
Expanded buildRowOne() {
return Expanded(
child: Row(
children: [
TextKey(
text: '1',
onTextInput: _textInputHandler,
),
TextKey(
text: '2',
onTextInput: _textInputHandler,
),
TextKey(
text: '3',
onTextInput: _textInputHandler,
),
TextKey(
text: '4',
onTextInput: _textInputHandler,
),
TextKey(
text: '5',
onTextInput: _textInputHandler,
),
],
),
);
}
Expanded buildRowTwo() {
return Expanded(
child: Row(
children: [
TextKey(
text: 'a',
onTextInput: _textInputHandler,
),
TextKey(
text: 'b',
onTextInput: _textInputHandler,
),
TextKey(
text: 'c',
onTextInput: _textInputHandler,
),
TextKey(
text: 'd',
onTextInput: _textInputHandler,
),
TextKey(
text: 'e',
onTextInput: _textInputHandler,
),
],
),
);
}
Expanded buildRowThree() {
return Expanded(
child: Row(
children: [
TextKey(
text: ' ',
flex: 4,
onTextInput: _textInputHandler,
),
BackspaceKey(
onBackspace: _backspaceHandler,
),
],
),
);
}
}
class TextKey extends StatelessWidget {
const TextKey({
Key key,
@required this.text,
this.onTextInput,
this.flex = 1,
}) : super(key: key);
final String text;
final ValueSetter<String> onTextInput;
final int flex;
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Material(
color: Colors.blue.shade300,
child: InkWell(
onTap: () {
onTextInput?.call(text);
},
child: Container(
child: Center(child: Text(text)),
),
),
),
),
);
}
}
class BackspaceKey extends StatelessWidget {
const BackspaceKey({
Key key,
this.onBackspace,
this.flex = 1,
}) : super(key: key);
final VoidCallback onBackspace;
final int flex;
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Material(
color: Colors.blue.shade300,
child: InkWell(
onTap: () {
onBackspace?.call();
},
child: Container(
child: Center(
child: Icon(Icons.backspace),
),
),
),
),
),
);
}
}
@sagarkardani
Copy link

I was able to make my custom keyboard as per my requirement.

One problem is that when typing with custom keyboard Textfield does not scroll to cursor when text goes beyond its width and cursor becomes invisible whereas while using system keyboard Textfield automatically scrolls to cursor when text goes beyond its width.

CustomKeyboard

@suragch
Copy link
Author

suragch commented Aug 13, 2021

@sagarkardani I'm actually having this problem in another package that I'm working on. I thought it was a problem with my text field widget, but that's interesting that it is related to the keyboard. I think there is a way to scroll to a position within the text field since I've seen that within the TextField source code. I'd have to go back and find it again. Apparently something about the custom keyboard isn't triggering that scroll. Please leave another comment if you find the solution.

@sagarkardani
Copy link

@suragch
Thanks for reply.
I am also working on this problem. If I find some solution then will surely update here.

Regards,
Sagar

@mhdzidannn
Copy link

@sagarkardani @suragch were there any progress regarding this issue? My custom keyboard also unable to scroll to cursor while the normal keyboard can.

@sagarkardani
Copy link

@mhdzidannn
Nope, 😔 If I am able to find some solution, will update here.

@Vivaldo-Roque
Copy link

@sagarkardani I'm actually having this problem in another package that I'm working on. I thought it was a problem with my text field widget, but that's interesting that it is related to the keyboard. I think there is a way to scroll to a position within the text field since I've seen that within the TextField source code. I'd have to go back and find it again. Apparently something about the custom keyboard isn't triggering that scroll. Please leave another comment if you find the solution.

You can use a ScrollController with TextField.
And scroll to the end.

@frog-fest
Copy link

@Vivaldo-Roque You can use a ScrollController with TextField. And scroll to the end.

Mind if you share an example or elaborate? I'm having the same issue (with a different custom keyboard) and it's giving me headaches because I cannot solve this core problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment