天天看點

在Flutter中自定義應用程式内鍵盤

在Flutter中自定義應用程式内鍵盤

本文将向您展示如何建立自定義鍵盤小部件,用于在您自己的應用程式中的Flutter TextField中輸入文本。使用案例包括特殊字元或語言的文本輸入,其中系統鍵盤可能不存在或使用者可能沒有安裝正确的鍵盤。

我們今天将制作一個更簡單的版本:

在Flutter中自定義應用程式内鍵盤
在Flutter中自定義應用程式内鍵盤

注意 *:本文不會告訴您如何建構使用者在任何應用程式中安裝和使用的系統鍵盤。這隻是一種基于小部件的方法,可以在您自己的應用程式中使用。

完整的代碼在文章的底部。

建立關鍵小部件

Flutter的優點是,通過組合更簡單的小部件,可以輕松建構鍵盤等複雜布局。首先,您将建立幾個簡單的按鍵小部件。

文本鍵

我已經圈出了由您首先制作的​

​TextKey​

​小部件制作的鍵。

在Flutter中自定義應用程式内鍵盤

顯示文本鍵(包括空格鍵)的自定義寫意紅色圓圈

将以下​

​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​

    ​屬性允許您的按鍵均勻分布在一行之間,甚至占據行的更大比例(如上圖中的空格鍵)。
  • 按下按鍵後,它将以anonTextInput回調的形式将其值傳遞給鍵盤。

Backspace鍵

您還需要一個與​

​TextKey​

​小部件具有不同外觀和功能的倒退鍵。

在Flutter中自定義應用程式内鍵盤

倒退鍵

将以下小部件添加到您的項目中:

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​

    ​,因為不需要将任何文本傳遞回鍵盤。

将按鍵組成鍵盤

一旦有了按鍵,鍵盤就很容易布局,因為它們隻是列中的行。

在Flutter中自定義應用程式内鍵盤

包含三行的列

這是代碼。我省略了一些重複的部分,以便簡潔。不過,你可以在文章的末尾找到它。

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​

    ​​。空格鍵的彎曲為​

    ​4​

    ​,而倒退的預設彎曲為1。這使得空格鍵占用了後空鍵寬度的四倍。

在應用程式中使用鍵盤

現在,您可以像這樣使用自定義鍵盤小部件:

在Flutter中自定義應用程式内鍵盤

代碼看起來是這樣的:

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​

​。你必須記住,可能有一個選擇,是以如果有的話,請用密鑰傳遞的文本替換它。

感謝這個,以提供幫助。*

處理倒退

您會認為倒退很簡單,但有一些不同的情況需要考慮:

  1. 有一個選擇(删除選擇)
  2. 光标在開頭(什麼都不要做)
  3. 其他任何事情(删除之前的角色)

以下是​

​_backspace​

​方法的實作:

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​

​方法。)然而,這仍然不是一個完美的解決方案,因為它不适用于像🇪🇬或👨‍👩‍👧這樣的字素簇,它們由多個代理對組成。不過,至少它不會

以下是象形文字和表情符号鍵盤作為示範:

在Flutter中自定義應用程式内鍵盤

🤓😷👨‍👩‍

如果您對此有意見,請參閱此堆棧溢出問題。

防止系統鍵盤顯示

如果您想将自定義鍵盤與aTextField一起使用,但系統鍵盤不斷彈出,那會有點煩人。這畢竟是預設行為。

防止系統鍵盤顯示的方法是将​

​TextField​

​​的​

​readOnly​

​​屬性設定為​

​true​

​。

TextField(
  ...
  showCursor: true,
  readOnly: true,
),      

此外,将​

​showCursor​

​​設定為​

​true​

​,使光标在您使用自定義鍵盤時仍然可以工作。

在系統鍵盤和自定義鍵盤之間切換

如果您想讓使用者選擇使用系統鍵盤或自定義鍵盤,您隻需為​

​readOnly​

​使用不同的值進行重建。

在Flutter中自定義應用程式内鍵盤

以下是示範應用程式中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​

    ​的值,然後重建布局。這會導緻系統鍵盤隐藏或顯示。
  • 将​

    ​Scaffold​

    ​​上的​

    ​resizeToAvoidBottomInset​

    ​​設定為​

    ​false​

    ​,允許系統鍵盤覆寫自定義鍵盤。另一個選項是在顯示系統鍵盤時隐藏自定義鍵盤。然而,當我在實驗中這樣做時,我發現我必須使用單獨的布爾值來隐藏自定義鍵盤,這樣我就可以延遲顯示它,直到系統鍵盤消失。否則,它會跳到系統鍵盤頂部一秒鐘。

完整代碼

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),
              ),
            ),
          ),
        ),
      ),
    );
  }
}