使用輸入欄位


Flutter 中接受使用者文字輸入的元件,最基本的就是 TextField 了,雖說如此,TextField 上可用的特性也是很多,先來個基本的吧!

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title: Text('Openhome.cc'),
      ),
      body: SearchField()
    )
  )
);

class SearchField extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SearchField();
}

class _SearchField extends State<SearchField> {
  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(
        labelText: '搜尋',
        hintText: '搜尋文件',
        prefixIcon: Icon(Icons.search),
      ),
      textInputAction: TextInputAction.search,
      onSubmitted: (value) => searchDialog(context, value)
    );
  }
}

void searchDialog(context, value) {
  showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: Text("搜尋 $value"),
      content: Text("無搜尋結果"),
      actions: [
        FlatButton(
          child: Text("重新搜尋"),
          onPressed: () => Navigator.of(context).pop(),
        )
      ]
    )
  );
}

TextField 的外觀是由 decoration 特性指定,要留意的是,InputDecorationprefixIcon 指定的是 TextField 的圖示,而 textInputActiontextInputAction 設定的是手機鍵盤上輸入鍵的圖示,直接來看執行結果好了:

使用輸入欄位

TextFieldonSubmitted 事件,是在按下輸入鍵後觸發,value 會是 TextField 的文字,範例中使用了對話框,基本上是路由的原理,這之後再來談;TextField 還有 onChanged 事件,每次欄位內容變動時就會觸發。

TextFieldonEditingComplete 事件,也是在按下輸入鍵後、onSubmitted 前觸發,事件處理器不會接受引數,onEditingComplete 的內部預設行為,會將值送至 TextField 的控制器,然後在鍵盤輸入鍵類型為 "done""go""send""search" 等時,令 TextField 失焦,若是 "next""previous" 時則不會失焦,如果你想改變焦點行為,可以自行指定 onEditingComplete 事件。

如果想在 onSubmittedonChanged 以外的地方,處理文字欄位的輸入,該怎麼辦?這就要使用 controller,也就是方才談到的控制器,例如,你想要在按下登入鈕後,取得名稱、密碼欄位的值:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: Text('Openhome.cc')),
      body: Login(),
    )
  )
);

class Login extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Login();
}

class _Login extends State<Login> {
  final nameController = TextEditingController();   // 建立控制器
  final passwdController = TextEditingController(); // 建立控制器

  @override
  void dispose() {
    nameController.dispose();    // 釋放控制器
    passwdController.dispose();  // 釋放控制器
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          TextField(
            decoration: InputDecoration(
                labelText: '帳號',
                hintText: '使用者名稱或郵件',
                prefixIcon: Icon(Icons.person)
            ),
            controller: nameController,    // 設定控制器
          ),
          TextField(
            decoration: InputDecoration(
              labelText: '密碼',
              prefixIcon: Icon(Icons.lock),
            ),
            obscureText: true, // 隱藏輸入
            controller: passwdController,  // 設定控制器
          ),
          FlatButton(
            child: Text('登入'),
            onPressed: () {
              // 從控制器取得值
              print('名稱:${nameController.text}');
              print('密碼:${passwdController.text}');
            },
          )
        ],
      )
    );
  }
}

TextFieldcontroller 可設定控制器,想取得對應欄位的輸入值,可透過控制器的 text 特性,Flutter 官方文件中談到,如果使用了控制器,記得要在 dispose 中釋放控制器;如果你沒有設置 controllerTextField 的內部會自行建立一個,不過你無法取得這個自行建立的控制器。

來看一下執行結果:

使用輸入欄位

另外,範例中的 SingleChildScrollView 主要是為了避免以下的問題:

使用輸入欄位

雖然使用 Expanded 也可以避免繪製空間不夠的問題,不過 Expanded 會分配可繪製空間,而鍵盤出現時,可繪製空間變小,文字欄位、按鈕等也就會變小,這不是我們想要的行為,因此這邊使用的是 SingleChildScrollView,簡單來說,這個元件會將列出的 child 元件,以線性的捲動方式來繪製。

onSubmittedonChanged 只能註冊一個事件處理器,如果程式中有多個地方,對同一個 TextField 的這些事件有興趣,可以透過 TextEditingControlleraddListener 註冊處理器,TextField 每次狀態改變,像是焦點變動、輸入變更等,都會呼叫註冊的處理器。

如果對焦點變動的事件有興趣,可以透過 FocusNodeaddListener 註冊處理器,FocusNode 也用來控制元件焦點,例如,在上例中,如果在「帳號」欄位時,按下鍵盤輸入鍵,預設動作只是失去焦點,使用者還得自行點選下個欄位,若想在這個時候,直接將焦點移至「密碼」欄位,可以如下:

import 'package:flutter/material.dart';

同前...略

class _Login extends State<Login> {
  final nameController = TextEditingController();
  final passwdController = TextEditingController();
  final passwdFocus = FocusNode(); // 焦點

  @override
  void dispose() {
    nameController.dispose();
    passwdController.dispose();
    passwdFocus.dispose();         // 釋放焦點
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          TextField(
            decoration: InputDecoration(
                labelText: '帳號',
                hintText: '使用者名稱或郵件',
                prefixIcon: Icon(Icons.person)
            ),
            controller: nameController,
            // 焦點移至 passwdFocus
            onSubmitted: (_) => FocusScope.of(context).requestFocus(passwdFocus),
          ),
          TextField(
            decoration: InputDecoration(
              labelText: '密碼',
              prefixIcon: Icon(Icons.lock),
            ),
            obscureText: true,
            controller: passwdController,
            focusNode: passwdFocus,  // 設定焦點
          ),
          FlatButton(
            child: Text('登入'),
            onPressed: () {
              print('名稱:${nameController.text}');
              print('密碼:${passwdController.text}');
            },
          )
        ],
      ),
    );
  }
}

執行起來的效果如下:

使用輸入欄位