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


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

顯示文本鍵(包括空格鍵)的自定義寫(xiě)意紅色圓圈
將以下TextKey小部件添加到您的項(xiàng)目中:
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回調(diào)的形式將其值傳遞給鍵盤。
Backspace鍵
您還需要一個(gè)與TextKey小部件具有不同外觀和功能的退格鍵。

退格鍵
將以下小部件添加到您的項(xiàng)目中:
?
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代碼有點(diǎn)重復(fù),因此一些重構(gòu)是為了使其更加簡(jiǎn)介。onBackspace是VoidCallback,因?yàn)椴恍枰獙⑷魏挝谋緜鬟f回鍵盤。
將按鍵組成鍵盤
一旦有了按鍵,鍵盤就很容易布局,因?yàn)樗鼈冎皇橇兄械男小?/p>

包含三行的列
這是代碼。我省略了一些重復(fù)的部分,以便簡(jiǎn)潔。不過(guò),你可以在文章的末尾找到它。
?
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: '堅(jiān)',
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: '樂(lè)',
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,
),
],
),
);
}
}
有趣的部分:
- 鍵盤收集按鍵的回調(diào)并傳遞它們。這樣,任何使用
CustomKeyboard的人都會(huì)收到回調(diào)。 - 您可以看到第三行如何使用
flex。空格鍵的彎曲為4,而退格的默認(rèn)彎曲為1。這使得空格鍵占用了后空鍵寬度的四倍。
在應(yīng)用程序中使用鍵盤
現(xiàn)在,您可以像這樣使用自定義鍵盤小部件:

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

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

以下是演示應(yīng)用程序中TextField的設(shè)置方式:
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;
});
},
),
有趣的部分:
- 當(dāng)按下鍵盤
IconButton時(shí),更改_readOnly的值,然后重建布局。這會(huì)導(dǎo)致系統(tǒng)鍵盤隱藏或顯示。 - 將
Scaffold上的resizeToAvoidBottomInset設(shè)置為false,允許系統(tǒng)鍵盤覆蓋自定義鍵盤。另一個(gè)選項(xiàng)是在顯示系統(tǒng)鍵盤時(shí)隱藏自定義鍵盤。然而,當(dāng)我在實(shí)驗(yàn)中這樣做時(shí),我發(fā)現(xiàn)我必須使用單獨(dú)的布爾值來(lái)隱藏自定義鍵盤,這樣我就可以延遲顯示它,直到系統(tǒng)鍵盤消失。否則,它會(huì)跳到系統(tǒng)鍵盤頂部一秒鐘。
就這樣!如您所見(jiàn),制作自己的應(yīng)用程序內(nèi)鍵盤并不難。
完整代碼
以下是我在本文中使用的演示應(yī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: '堅(jiān)',
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: '樂(lè)',
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自定義應(yīng)用程序內(nèi)鍵盤的實(shí)現(xiàn)方法的詳細(xì)內(nèi)容,更多關(guān)于Flutter自定義鍵盤的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義ViewGroup之第一次接觸ViewGroup
這篇文章主要為大家詳細(xì)介紹了Android自定義ViewGroup之第一次接觸ViewGroup,感興趣的小伙伴們可以參考一下2016-06-06
Android計(jì)步模塊實(shí)例代碼(類似微信運(yùn)動(dòng))
本篇文章主要介紹了Android計(jì)步模塊實(shí)例代碼(類似微信運(yùn)動(dòng)),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
Android實(shí)現(xiàn)商城購(gòu)物車功能的實(shí)例代碼
最近公司項(xiàng)目做商城模塊,需要實(shí)現(xiàn)購(gòu)物車功能,主要實(shí)現(xiàn)了單選、全選,金額合計(jì),商品刪除,商品數(shù)量加減等功能,這篇文章主要介紹了Android實(shí)現(xiàn)商城購(gòu)物車功能,需要的朋友可以參考下2019-06-06
Android自定義View實(shí)現(xiàn)圓形進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)圓形進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
Android應(yīng)用中使用XmlSerializer序列化XML數(shù)據(jù)的教程
這篇文章主要介紹了Android應(yīng)用中使用XmlSerializer序列化XML數(shù)據(jù)的教程,XmlSerializer序列化XML同時(shí)也是將數(shù)據(jù)寫(xiě)為XML格式的基本方法,需要的朋友可以參考下2016-04-04
Android編程之非調(diào)用系統(tǒng)界面實(shí)現(xiàn)發(fā)送彩信的方法(MMS)
這篇文章主要介紹了Android編程之非調(diào)用系統(tǒng)界面實(shí)現(xiàn)發(fā)送彩信的方法,涉及Android源碼中的mms的使用技巧,需要的朋友可以參考下2016-01-01
Flutter實(shí)現(xiàn)簡(jiǎn)單的內(nèi)容高亮效果
內(nèi)容高亮并不陌生,特別是在搜索內(nèi)容頁(yè)面,可以說(shuō)四處可見(jiàn),這篇文章主要為大家介紹了如何使用Flutter實(shí)現(xiàn)簡(jiǎn)單的內(nèi)容高亮效果,需要的可以參考下2023-08-08
Android自定義View仿QQ運(yùn)動(dòng)步數(shù)效果
這篇文章主要為大家詳細(xì)介紹了Android自定義View仿QQ運(yùn)動(dòng)步數(shù)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11

