詳解Flutter自定義應用程序內鍵盤的實現(xiàn)方法
本文將向您展示如何創(chuàng)建自定義鍵盤小部件,用于在您自己的應用程序中的Flutter TextField中輸入文本。使用案例包括特殊字符或語言的文本輸入,其中系統(tǒng)鍵盤可能不存在或用戶可能沒有安裝正確的鍵盤。
我們今天將制作一個更簡單的版本:


注意 :本文不會告訴您如何構建用戶在任何應用程序中安裝和使用的系統(tǒng)鍵盤。這只是一種基于小部件的方法,可以在您自己的應用程序中使用。
完整的代碼在文章的底部。
創(chuàng)建關鍵小部件
Flutter的優(yōu)點是,通過組合更簡單的小部件,可以輕松構建鍵盤等復雜布局。首先,您將創(chuàng)建幾個簡單的按鍵小部件。
文本鍵
我已經圈出了由您首先制作的TextKey小部件制作的鍵。

顯示文本鍵(包括空格鍵)的自定義寫意紅色圓圈
將以下TextKey小部件添加到您的項目中:
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)),
),
),
),
),
);
}
}
以下是有趣的部分:
flex屬性允許您的按鍵均勻分布在一行之間,甚至占據(jù)行的更大比例(如上圖中的空格鍵)。- 按下按鍵后,它將以anonTextInput回調的形式將其值傳遞給鍵盤。
Backspace鍵
您還需要一個與TextKey小部件具有不同外觀和功能的退格鍵。

退格鍵
將以下小部件添加到您的項目中:
?
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),
),
),
),
),
),
);
}
備注:
TextKey代碼有點重復,因此一些重構是為了使其更加簡介。onBackspace是VoidCallback,因為不需要將任何文本傳遞回鍵盤。
將按鍵組成鍵盤
一旦有了按鍵,鍵盤就很容易布局,因為它們只是列中的行。

包含三行的列
這是代碼。我省略了一些重復的部分,以便簡潔。不過,你可以在文章的末尾找到它。
?
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(),
buildRowFour(),
buildRowFive()
],
),
);
}
?
Expanded buildRowOne() {
return Expanded(
child: Row(
children: [
TextKey(
text: '堅',
onTextInput: _textInputHandler,
),
TextKey(
text: '果',
onTextInput: _textInputHandler,
),
TextKey(
text: '祝',
onTextInput: _textInputHandler,
),
],
),
);
}
?
Expanded buildRowTwo() {
return Expanded(
child: Row(
children: [
TextKey(
text: 'I',
onTextInput: _textInputHandler,
),
TextKey(
text: 'n',
onTextInput: _textInputHandler,
),
TextKey(
text: 'f',
onTextInput: _textInputHandler,
),
TextKey(
text: 'o',
onTextInput: _textInputHandler,
),
TextKey(
text: 'Q',
onTextInput: _textInputHandler,
),
],
),
);
}
?
Expanded buildRowThree() {
return Expanded(
child: Row(
children: [
TextKey(
text: '十',
onTextInput: _textInputHandler,
),
TextKey(
text: '五',
onTextInput: _textInputHandler,
),
TextKey(
text: '周',
onTextInput: _textInputHandler,
),
TextKey(
text: '年',
onTextInput: _textInputHandler,
),
],
),
);
}
?
Expanded buildRowFour() {
return Expanded(
child: Row(
children: [
TextKey(
text: '生',
onTextInput: _textInputHandler,
),
TextKey(
text: '日',
onTextInput: _textInputHandler,
),
TextKey(
text: '快',
onTextInput: _textInputHandler,
),
TextKey(
text: '樂',
onTextInput: _textInputHandler,
),
TextKey(
text: '!',
onTextInput: _textInputHandler,
),
],
),
);
}
?
Expanded buildRowFive() {
return Expanded(
child: Row(
children: [
TextKey(
text: ' ??',
flex: 2,
onTextInput: _textInputHandler,
),
TextKey(
text: ' ??',
flex: 2,
onTextInput: _textInputHandler,
),
TextKey(
text: '??',
flex: 2,
onTextInput: _textInputHandler,
),
TextKey(
text: '??',
flex: 2,
onTextInput: _textInputHandler,
),
BackspaceKey(
onBackspace: _backspaceHandler,
),
],
),
);
}
}
有趣的部分:
- 鍵盤收集按鍵的回調并傳遞它們。這樣,任何使用
CustomKeyboard的人都會收到回調。 - 您可以看到第三行如何使用
flex??崭矜I的彎曲為4,而退格的默認彎曲為1。這使得空格鍵占用了后空鍵寬度的四倍。
在應用程序中使用鍵盤
現(xiàn)在,您可以像這樣使用自定義鍵盤小部件:

代碼看起來是這樣的:
CustomKeyboard(
onTextInput: (myText) {
_insertText(myText);
},
onBackspace: () {
_backspace();
},
),
處理文本輸入
以下是_insertText方法的樣子:
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,
);
}
_controller是TextField的TextEditingController。你必須記住,可能有一個選擇,所以如果有的話,請用密鑰傳遞的文本替換它。
感謝這個,以提供幫助。*
處理退格
您會認為退格很簡單,但有一些不同的情況需要考慮:
- 有一個選擇(刪除選擇)
- 光標在開頭(什么都不要做)
- 其他任何事情(刪除之前的角色)
以下是_backspace方法的實現(xiàn):
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;
}
即使刪除之前的角色也有點棘手。如果您在有表情符號或其他代理對時只回退單個代碼單元這將導致崩潰。作為上述代碼中的變通辦法,我檢查了上一個字符是否是UFT-16代理,如果是,則后退了兩個字符。(我從Flutter TextPainter源代碼中獲得了_isUtf16Surrogate方法。)然而,這仍然不是一個完美的解決方案,因為它不適用于像????或??????這樣的字素簇,它們由多個代理對組成。不過,至少它不會
以下是象形文字和表情符號鍵盤作為演示:

????????
如果您對此有意見,請參閱此堆棧溢出問題。
防止系統(tǒng)鍵盤顯示
如果您想將自定義鍵盤與aTextField一起使用,但系統(tǒng)鍵盤不斷彈出,那會有點煩人。這畢竟是默認行為。
防止系統(tǒng)鍵盤顯示的方法是將TextField的readOnly屬性設置為true。
TextField( ... showCursor: true, readOnly: true, ),
此外,將showCursor設置為true,使光標在您使用自定義鍵盤時仍然可以工作。
在系統(tǒng)鍵盤和自定義鍵盤之間切換
如果您想讓用戶選擇使用系統(tǒng)鍵盤或自定義鍵盤,您只需為readOnly使用不同的值進行重建。

以下是演示應用程序中TextField的設置方式:
class _KeyboardDemoState extends State<KeyboardDemo> {
TextEditingController _controller = TextEditingController();
bool _readOnly = true;
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Column(
children: [
...
TextField(
controller: _controller,
decoration: ...,
style: TextStyle(fontSize: 24),
autofocus: true,
showCursor: true,
readOnly: _readOnly,
),
IconButton(
icon: Icon(Icons.keyboard),
onPressed: () {
setState(() {
_readOnly = !_readOnly;
});
},
),
有趣的部分:
- 當按下鍵盤
IconButton時,更改_readOnly的值,然后重建布局。這會導致系統(tǒng)鍵盤隱藏或顯示。 - 將
Scaffold上的resizeToAvoidBottomInset設置為false,允許系統(tǒng)鍵盤覆蓋自定義鍵盤。另一個選項是在顯示系統(tǒng)鍵盤時隱藏自定義鍵盤。然而,當我在實驗中這樣做時,我發(fā)現(xiàn)我必須使用單獨的布爾值來隱藏自定義鍵盤,這樣我就可以延遲顯示它,直到系統(tǒng)鍵盤消失。否則,它會跳到系統(tǒng)鍵盤頂部一秒鐘。
就這樣!如您所見,制作自己的應用程序內鍵盤并不難。
完整代碼
以下是我在本文中使用的演示應用程序的完整代碼:
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(
appBar: AppBar(
title: Text("大前端之旅的自定義鍵盤"),
),
resizeToAvoidBottomInset: false,
body: Column(
children: [
Text("微信:xjg13690"),
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(),
buildRowFour(),
buildRowFive()
],
),
);
}
Expanded buildRowOne() {
return Expanded(
child: Row(
children: [
TextKey(
text: '堅',
onTextInput: _textInputHandler,
),
TextKey(
text: '果',
onTextInput: _textInputHandler,
),
TextKey(
text: '祝',
onTextInput: _textInputHandler,
),
],
),
);
}
Expanded buildRowTwo() {
return Expanded(
child: Row(
children: [
TextKey(
text: 'I',
onTextInput: _textInputHandler,
),
TextKey(
text: 'n',
onTextInput: _textInputHandler,
),
TextKey(
text: 'f',
onTextInput: _textInputHandler,
),
TextKey(
text: 'o',
onTextInput: _textInputHandler,
),
TextKey(
text: 'Q',
onTextInput: _textInputHandler,
),
],
),
);
}
Expanded buildRowThree() {
return Expanded(
child: Row(
children: [
TextKey(
text: '十',
onTextInput: _textInputHandler,
),
TextKey(
text: '五',
onTextInput: _textInputHandler,
),
TextKey(
text: '周',
onTextInput: _textInputHandler,
),
TextKey(
text: '年',
onTextInput: _textInputHandler,
),
],
),
);
}
Expanded buildRowFour() {
return Expanded(
child: Row(
children: [
TextKey(
text: '生',
onTextInput: _textInputHandler,
),
TextKey(
text: '日',
onTextInput: _textInputHandler,
),
TextKey(
text: '快',
onTextInput: _textInputHandler,
),
TextKey(
text: '樂',
onTextInput: _textInputHandler,
),
TextKey(
text: '!',
onTextInput: _textInputHandler,
),
],
),
);
}
Expanded buildRowFive() {
return Expanded(
child: Row(
children: [
TextKey(
text: ' ??',
flex: 2,
onTextInput: _textInputHandler,
),
TextKey(
text: ' ??',
flex: 2,
onTextInput: _textInputHandler,
),
TextKey(
text: '??',
flex: 2,
onTextInput: _textInputHandler,
),
TextKey(
text: '??',
flex: 2,
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),
),
),
),
),
),
);
}
}以上就是詳解Flutter自定義應用程序內鍵盤的實現(xiàn)方法的詳細內容,更多關于Flutter自定義鍵盤的資料請關注腳本之家其它相關文章!
相關文章
Android自定義ViewGroup之第一次接觸ViewGroup
這篇文章主要為大家詳細介紹了Android自定義ViewGroup之第一次接觸ViewGroup,感興趣的小伙伴們可以參考一下2016-06-06
Android應用中使用XmlSerializer序列化XML數(shù)據(jù)的教程
這篇文章主要介紹了Android應用中使用XmlSerializer序列化XML數(shù)據(jù)的教程,XmlSerializer序列化XML同時也是將數(shù)據(jù)寫為XML格式的基本方法,需要的朋友可以參考下2016-04-04
Android編程之非調用系統(tǒng)界面實現(xiàn)發(fā)送彩信的方法(MMS)
這篇文章主要介紹了Android編程之非調用系統(tǒng)界面實現(xiàn)發(fā)送彩信的方法,涉及Android源碼中的mms的使用技巧,需要的朋友可以參考下2016-01-01

