詳解Flutter自定義應(yīng)用程序內(nèi)鍵盤的實現(xiàn)方法
本文將向您展示如何創(chuàng)建自定義鍵盤小部件,用于在您自己的應(yīng)用程序中的Flutter TextField中輸入文本。使用案例包括特殊字符或語言的文本輸入,其中系統(tǒng)鍵盤可能不存在或用戶可能沒有安裝正確的鍵盤。
我們今天將制作一個更簡單的版本:
注意 :本文不會告訴您如何構(gòu)建用戶在任何應(yīng)用程序中安裝和使用的系統(tǒng)鍵盤。這只是一種基于小部件的方法,可以在您自己的應(yīng)用程序中使用。
完整的代碼在文章的底部。
創(chuàng)建關(guān)鍵小部件
Flutter的優(yōu)點是,通過組合更簡單的小部件,可以輕松構(gòu)建鍵盤等復雜布局。首先,您將創(chuàng)建幾個簡單的按鍵小部件。
文本鍵
我已經(jī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回調(diào)的形式將其值傳遞給鍵盤。
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
代碼有點重復,因此一些重構(gòu)是為了使其更加簡介。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, ), ], ), ); } }
有趣的部分:
- 鍵盤收集按鍵的回調(diào)并傳遞它們。這樣,任何使用
CustomKeyboard
的人都會收到回調(diào)。 - 您可以看到第三行如何使用
flex
??崭矜I的彎曲為4
,而退格的默認彎曲為1。這使得空格鍵占用了后空鍵寬度的四倍。
在應(yīng)用程序中使用鍵盤
現(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
屬性設(shè)置為true
。
TextField( ... showCursor: true, readOnly: true, ),
此外,將showCursor
設(shè)置為true
,使光標在您使用自定義鍵盤時仍然可以工作。
在系統(tǒng)鍵盤和自定義鍵盤之間切換
如果您想讓用戶選擇使用系統(tǒng)鍵盤或自定義鍵盤,您只需為readOnly
使用不同的值進行重建。
以下是演示應(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; }); }, ),
有趣的部分:
- 當按下鍵盤
IconButton
時,更改_readOnly
的值,然后重建布局。這會導致系統(tǒng)鍵盤隱藏或顯示。 - 將
Scaffold
上的resizeToAvoidBottomInset
設(shè)置為false
,允許系統(tǒng)鍵盤覆蓋自定義鍵盤。另一個選項是在顯示系統(tǒng)鍵盤時隱藏自定義鍵盤。然而,當我在實驗中這樣做時,我發(fā)現(xiàn)我必須使用單獨的布爾值來隱藏自定義鍵盤,這樣我就可以延遲顯示它,直到系統(tǒng)鍵盤消失。否則,它會跳到系統(tǒng)鍵盤頂部一秒鐘。
就這樣!如您所見,制作自己的應(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: '堅', 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自定義應(yīng)用程序內(nèi)鍵盤的實現(xiàn)方法的詳細內(nèi)容,更多關(guān)于Flutter自定義鍵盤的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義ViewGroup之第一次接觸ViewGroup
這篇文章主要為大家詳細介紹了Android自定義ViewGroup之第一次接觸ViewGroup,感興趣的小伙伴們可以參考一下2016-06-06Android應(yīng)用中使用XmlSerializer序列化XML數(shù)據(jù)的教程
這篇文章主要介紹了Android應(yīng)用中使用XmlSerializer序列化XML數(shù)據(jù)的教程,XmlSerializer序列化XML同時也是將數(shù)據(jù)寫為XML格式的基本方法,需要的朋友可以參考下2016-04-04Android編程之非調(diào)用系統(tǒng)界面實現(xiàn)發(fā)送彩信的方法(MMS)
這篇文章主要介紹了Android編程之非調(diào)用系統(tǒng)界面實現(xiàn)發(fā)送彩信的方法,涉及Android源碼中的mms的使用技巧,需要的朋友可以參考下2016-01-01