Flutter 基礎控制元件篇-->輸入框(TextField)、表單(Form)

夜夕i發表於2019-10-06

Flutter 的Material元件庫中提供了輸入框元件TextField和表單元件Form

輸入框(TextField)

TextField主要用於文字輸入。

原始碼示例

建構函式如下:

const TextField({
  ...
  TextEditingController controller, 
  FocusNode focusNode,
  InputDecoration decoration = const InputDecoration(),
  TextInputType keyboardType,
  TextInputAction textInputAction,
  TextStyle style,
  TextAlign textAlign = TextAlign.start,
  bool autofocus = false,
  bool obscureText = false,
  int maxLines = 1,
  int maxLength,
  bool maxLengthEnforced = true,
  ValueChanged<String> onChanged,
  VoidCallback onEditingComplete,
  ValueChanged<String> onSubmitted,
  List<TextInputFormatter> inputFormatters,
  bool enabled,
  this.cursorWidth = 2.0,
  this.cursorRadius,
  this.cursorColor,
  ...
})
複製程式碼

不重要的就沒粘

屬性解釋

  • controller:編輯框的控制器,通過它可以設定/獲取編輯框的內容、選擇編輯內容、監聽編輯文字改變事件等。
    大多數情況下我們都需要顯式提供一個controller來與文字框互動。如果沒有提供controller,則TextField內部會自動建立一個。

  • focusNode:用於控制TextField是否佔有當前鍵盤的輸入焦點。它是我們和鍵盤互動的一個控制程式碼(handle)。

  • InputDecoration:用於控制TextField的外觀顯示,如提示文字、背景顏色、邊框等。 原始碼示例(給出原始碼,自己下去嘗試一下,不做過多解釋)

const InputDecoration({
	this.icon,
	this.labelText,
	this.labelStyle,
	this.helperText,
	this.helperStyle,
	this.hintText,
	this.hintStyle,
	this.hintMaxLines,
	this.errorText,
	this.errorStyle,
	this.errorMaxLines,
	this.hasFloatingPlaceholder = true,
	this.isDense,
	this.contentPadding,
	this.prefixIcon,
	this.prefix,
	this.prefixText,
	this.prefixStyle,
	this.suffixIcon,
	this.suffix,
	this.suffixText,
	this.suffixStyle,
	this.counter,
	this.counterText,
	this.counterStyle,
	this.filled,
	this.fillColor,
	this.errorBorder,
	this.focusedBorder,
	this.focusedErrorBorder,
	this.disabledBorder,
	this.enabledBorder,
	this.border,
	this.enabled = true,
	this.semanticCounterText,
	this.alignLabelWithHint,
})
複製程式碼
  • keyboardType:用於設定該輸入框預設的鍵盤輸入型別,TextInputType列舉值如下:

    • text:文字輸入鍵盤
    • multiline:多行文字,需和maxLines配合使用(設為null或大於1)
    • number:數字;會彈出數字鍵盤
    • phone:優化後的電話號碼輸入鍵盤;會彈出數字鍵盤並顯示* #
    • datetime:優化後的日期輸入鍵盤;Android上會顯示: -
    • emailAddress:優化後的電子郵件地址;會顯示@ .
    • url:優化後的url輸入鍵盤; 會顯示/ .
  • textInputAction:鍵盤動作按鈕圖示(即Enter鍵點陣圖標),它是一個列舉值,有多個可選值。全部取值請參考官方文件:api.flutter.dev
    示例:當值為TextInputAction.search時,原生Android系統下鍵盤樣式如下:

圖片載入失敗!

  • style:正在編輯的文字樣式有關屬性在TextStyle裡設定

  • textAlign:輸入框內編輯文字在水平方向的對齊方式。
    如:textAlign: TextAlign.right,則是從後往左顯示

  • autofocus:是否自動獲取焦點。值為true或者false

  • obscureText:是否隱藏正在編輯的文字,如用於輸入密碼的場景等,文字內容會用“•”替換。

  • maxLines:輸入框的最大行數,預設為1;如果為null,則無行數限制。

  • maxLengthmaxLengthEnforcedmaxLength代表輸入框文字的最大長度,設定後輸入框右下角會顯示輸入的文字計數。maxLengthEnforced決定當輸入文字長度超過maxLength時是否阻止輸入,為true時會阻止輸入,為false時不會阻止輸入但輸入框會變紅。

  • onChanged:輸入框內容改變時的回撥函式;注:內容改變事件也可以通過controller來監聽。

  • onEditingCompleteonSubmitted:這兩個回撥都是在輸入框輸入完成時觸發,比如按了鍵盤的完成鍵(對號圖示)或搜尋鍵(?圖示)。不同的是兩個回撥簽名不同,onSubmitted回撥是ValueChanged<String>型別,它接收當前輸入內容做為引數,而onEditingComplete不接收引數。

  • inputFormatters:用於指定輸入格式;當使用者輸入內容改變時,會根據指定的格式來校驗。

  • cursorWidthcursorRadiuscursorColor:這三個屬性是用於自定義輸入框游標寬度、圓角和顏色的。
    如:cursorColor: Colors.greencursorWidth: 10,cursorRadius: Radius.circular(4),

程式碼示例

TextField(
	keyboardType: TextInputType.url,
	decoration: InputDecoration(
	  labelText: 'xxx',
	  prefix: Icon(Icons.lock),
	  // enabled: false
	),
	textInputAction: TextInputAction.next,
	style: TextStyle(color: Colors.red),
	textAlign: TextAlign.right,
	autofocus: false,
	// obscureText: true,
	maxLines: null,
	maxLength: 10,
	maxLengthEnforced: true,
	cursorColor: Colors.green,
	cursorWidth: 10,
	cursorRadius: Radius.circular(4),
),
複製程式碼

執行效果:

圖片載入失敗!

重要內容

Column(
        children: <Widget>[
          TextField(
            autofocus: true,
            decoration: InputDecoration(
                labelText: "使用者名稱",
                hintText: "使用者名稱或郵箱",
                prefixIcon: Icon(Icons.person)
            ),
          ),
          TextField(
            decoration: InputDecoration(
                labelText: "密碼",
                hintText: "您的登入密碼",
                prefixIcon: Icon(Icons.lock)
            ),
            obscureText: true,
          ),
        ],
);
複製程式碼

執行效果:

圖片載入失敗!

獲取輸入內容

獲取輸入內容有兩種方式:

  • 定義兩個變數,用於儲存使用者名稱和密碼,然後在onChanged觸發時,各自儲存一下輸入內容。

  • 通過controller直接獲取。

第一種方式比較簡單,所以就不說了,重點說第二種方式,以使用者名稱輸入框為例:

  1. 定義一個controller
//定義一個controller
TextEditingController _unameController = TextEditingController();
複製程式碼
  1. 設定輸入框controller
TextField(
    autofocus: true,
    controller: _unameController, //設定controller
    ...
)
複製程式碼
  1. 通過controller獲取輸入框內容:
print(_unameController.text)
複製程式碼

程式碼示例:

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  //定義一個controller
  TextEditingController _unameController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("輸入框"),
      ),
      body: Column(
        children: <Widget>[
          TextField(
            autofocus: true,
            controller: _unameController, //設定
            decoration: InputDecoration(
              labelText: "使用者名稱",
              hintText: "使用者名稱或郵箱",
              prefixIcon: Icon(Icons.person),
            ),
            onChanged: (String) => {  //輸入框改變時觸發onChanged
              print(_unameController.text)  //列印輸入框的內容
            },
          ),
          TextField(
            decoration: InputDecoration(
                labelText: "密碼",
                hintText: "您的登入密碼",
                prefixIcon: Icon(Icons.lock)),
            obscureText: true,
          ),
        ],
      ),
    );
  }
}
複製程式碼

執行效果:

圖片載入失敗!

監聽文字變化

監聽文字變化也有兩種方式:

  1. 設定onChanged回撥,如:
TextField(
    autofocus: true,
    onChanged: (v) {
      print("onChange: $v");
    }
)
複製程式碼
  1. 通過controller監聽,如:
@override
void initState() {
  //監聽輸入改變  
  _unameController.addListener((){
    print(_unameController.text);
  });
}
複製程式碼

這兩種方式相比,onChanged是專門用於監聽文字變化,而controller的功能卻多一些,除了能監聽文字變化外,它還可以設定預設值、選擇文字,

例子:

  1. 建立一個controller
TextEditingController _selectionController =  TextEditingController();
複製程式碼
  1. 設定預設值,並從第三個字元開始選中後面的字元:
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
    baseOffset: 2,
    extentOffset: _selectionController.text.length
);
複製程式碼
  1. 設定controller
TextField(
  controller: _selectionController,
)
複製程式碼

程式碼:

class _CategoryPageState extends State<CategoryPage> {
  TextEditingController _selectionController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    _selectionController.text = "我好喜歡你吖!";
    _selectionController.selection = TextSelection(
        baseOffset: 2, extentOffset: _selectionController.text.length);
    return Scaffold(
      appBar: AppBar(
        title: Text("輸入框"),
      ),
      body: Column(
        children: <Widget>[
          TextField(
            controller: _selectionController,
          ),
        ],
      ),
    );
  }
}
複製程式碼

執行效果:

圖片載入失敗!

控制焦點

焦點可以通過FocusNodeFocusScopeNode來控制。
預設情況下,焦點由FocusScope來管理,它代表焦點控制範圍,可以在這個範圍內可以通過FocusScopeNode在輸入框之間移動焦點、設定預設焦點等。
我們可以通過FocusScope.of(context)來獲取Widget樹中預設的FocusScopeNode

示例要求:
建立兩個TextField,第一個自動獲取焦點,然後建立兩個按鈕:點選第一個按鈕可以將焦點從第一個TextField挪到第二個TextField,點選第二個按鈕可以關閉鍵盤。

程式碼如下:

class FocusTestRoute extends StatefulWidget {
  @override
  _FocusTestRouteState createState() => new _FocusTestRouteState();
}

class _FocusTestRouteState extends State<FocusTestRoute> {
  FocusNode focusNode1 = new FocusNode();
  FocusNode focusNode2 = new FocusNode();
  FocusScopeNode focusScopeNode;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: <Widget>[
          TextField(
            autofocus: true, 
            focusNode: focusNode1,//關聯focusNode1
            decoration: InputDecoration(
                labelText: "input1"
            ),
          ),
          TextField(
            focusNode: focusNode2,//關聯focusNode2
            decoration: InputDecoration(
                labelText: "input2"
            ),
          ),
          Builder(builder: (ctx) {
            return Column(
              children: <Widget>[
                RaisedButton(
                  child: Text("移動焦點"),
                  onPressed: () {
                    //將焦點從第一個TextField移到第二個TextField
                    // 這是一種寫法 FocusScope.of(context).requestFocus(focusNode2);
                    // 這是第二種寫法
                    if(null == focusScopeNode){
                      focusScopeNode = FocusScope.of(context);
                    }
                    focusScopeNode.requestFocus(focusNode2);
                  },
                ),
                RaisedButton(
                  child: Text("隱藏鍵盤"),
                  onPressed: () {
                    // 當所有編輯框都失去焦點時鍵盤就會收起  
                    focusNode1.unfocus();
                    focusNode2.unfocus();
                  },
                ),
              ],
            );
          },
          ),
        ],
      ),
    );
  }
}
複製程式碼

因為我這個模擬器,他不顯示鍵盤,所以當輸入框獲取焦點的時候,下面會顯示一個鍵盤(我這個不顯示)

圖片載入失敗!

監聽焦點狀態改變事件

FocusNode繼承自ChangeNotifier,通過FocusNode可以監聽焦點的改變事件,如:

...
// 建立 focusNode   
FocusNode focusNode = new FocusNode();
...
// focusNode繫結輸入框   
TextField(focusNode: focusNode);
...
// 監聽焦點變化    
focusNode.addListener((){
   print(focusNode.hasFocus);
});
複製程式碼

獲得焦點時focusNode.hasFocus值為true,失去焦點時為false

表單(Form)

前面介紹的輸入框可以單個操作,但是往往在開發中,需要多多個輸入框同時進行操作,比如清空所有輸入框的內容。

為此,Flutter提供了一個Form元件,它可以對輸入框進行分組,然後進行一些統一操作,如輸入內容校驗、輸入框重置以及輸入內容儲存等。

Form繼承自StatefulWidget物件,它對應的狀態類為FormState

原始碼示例

建構函式如下:

Form({
  @required Widget child,
  bool autovalidate = false,
  WillPopCallback onWillPop,
  VoidCallback onChanged,
})
複製程式碼

屬性解釋

  • autovalidate:是否自動校驗輸入內容;當為true時,每一個子FormField內容發生變化時都會自動校驗合法性,並直接顯示錯誤資訊。否則,需要通過呼叫FormState.validate()來手動校驗。

  • onWillPop:決定Form所在的路由是否可以直接返回(如點選返回按鈕),該回撥返回一個Future物件,如果Future的最終結果是false,則當前路由不會返回;如果為true,則會返回到上一個路由。此屬性通常用於攔截返回按鈕。

  • onChangedForm的任意一個子FormField內容發生變化時會觸發此回撥。

FormField

Form的子孫元素必須是FormField型別,FormField是一個抽象類,定義幾個屬性,FormState內部通過它們來完成操作,

FormField部分建構函式如下:

const FormField({
  ...
  FormFieldSetter<T> onSaved, //儲存回撥
  FormFieldValidator<T>  validator, //驗證回撥
  T initialValue, //初始值
  bool autovalidate = false, //是否自動校驗。
})
複製程式碼

為了方便使用,Flutter提供了一個TextFormField元件,它繼承自FormField類,也是TextField的一個包裝類,所以除了FormField定義的屬性之外,它還包括TextField的屬性。

FormState

FormStateFormState類,可以通過Form.of()GlobalKey獲得。
我們可以通過它來對Form的子孫FormField進行統一操作。

常用的三個方法:

  • FormState.validate():呼叫此方法後,會呼叫Form子孫FormFieldvalidate回撥,如果有一個校驗失敗,則返回false,所有校驗失敗項都會返回使用者返回的錯誤提示。

  • FormState.save():呼叫此方法後,會呼叫Form子孫FormFieldsave回撥,用於儲存表單內容。

  • FormState.reset():呼叫此方法後,會將子孫FormField的內容清空。

程式碼示例

修改上面使用者登入的示例,在提交之前校驗:

  • 使用者名稱不能為空,如果為空則提示“使用者名稱不能為空”。
  • 密碼不能小於6位,如果小於6為則提示“密碼不能少於6位”。

程式碼:

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  TextEditingController _unameController = new TextEditingController();
  TextEditingController _pwdController = new TextEditingController();
  GlobalKey _formKey = new GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Form Test"),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
        child: Form(
          key: _formKey, //設定globalKey,用於後面獲取FormState
          autovalidate: true, //開啟自動校驗
          child: Column(
            children: <Widget>[
              TextFormField(
                  autofocus: true,
                  controller: _unameController,
                  decoration: InputDecoration(
                      labelText: "使用者名稱",
                      hintText: "使用者名稱或郵箱",
                      icon: Icon(Icons.person)),
                  // 校驗使用者名稱
                  validator: (v) {
                    return v.trim().length > 0 ? null : "使用者名稱不能為空";
                  }),
              TextFormField(
                  controller: _pwdController,
                  decoration: InputDecoration(
                      labelText: "密碼",
                      hintText: "您的登入密碼",
                      icon: Icon(Icons.lock)),
                  obscureText: true,
                  //校驗密碼
                  validator: (v) {
                    return v.trim().length > 5 ? null : "密碼不能少於6位";
                  }),
              // 登入按鈕
              Padding(
                padding: const EdgeInsets.only(top: 28.0),
                child: Row(
                  children: <Widget>[
                    Expanded(
                      child: RaisedButton(
                        padding: EdgeInsets.all(15.0),
                        child: Text("登入"),
                        color: Theme.of(context).primaryColor,
                        textColor: Colors.white,
                        onPressed: () {
                          //在這裡不能通過此方式獲取FormState,context不對
                          //print(Form.of(context));

                          // 通過_formKey.currentState 獲取FormState後,
                          // 呼叫validate()方法校驗使用者名稱密碼是否合法,校驗
                          // 通過後再提交資料。
                          if ((_formKey.currentState as FormState).validate()) {
                            //驗證通過提交資料
                          }
                        },
                      ),
                    ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
複製程式碼

執行效果:

圖片載入失敗!

注意,登入按鈕的onPressed方法中不能通過Form.of(context)來獲取,原因是,此處的contextFormTestRoutecontext,而Form.of(context)是根據所指定context向根去查詢,而FormState是在FormTestRoute的子樹中,所以不行。正確的做法是通過Builder來構建登入按鈕,Builder會將widget節點的context作為回撥引數:

Expanded(
 // 通過Builder來獲取RaisedButton所在widget樹的真正context(Element) 
  child:Builder(builder: (context){
    return RaisedButton(
      ...
      onPressed: () {
        //由於本widget也是Form的子代widget,所以可以通過下面方式獲取FormState  
        if(Form.of(context).validate()){
          //驗證通過提交資料
        }
      },
    );
  })
)
複製程式碼

一定要注意context的指向問題。


V_V

相關文章