Skip to content

Instantly share code, notes, and snippets.

@suragch
Last active June 10, 2024 22:41
Show Gist options
  • 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),
),
),
),
),
),
);
}
}
@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