Flutter實(shí)戰(zhàn) 輸入框及表單

2021-03-06 17:59 更新

Material 組件庫(kù)中提供了輸入框組件TextField和表單組件Form。下面我們分別介紹一下。

#3.7.1 TextField

TextField用于文本輸入,它提供了很多屬性,我們先簡(jiǎn)單介紹一下主要屬性的作用,然后通過(guò)幾個(gè)示例來(lái)演示一下關(guān)鍵屬性的用法。

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:編輯框的控制器,通過(guò)它可以設(shè)置/獲取編輯框的內(nèi)容、選擇編輯內(nèi)容、監(jiān)聽編輯文本改變事件。大多數(shù)情況下我們都需要顯式提供一個(gè)controller來(lái)與文本框交互。如果沒(méi)有提供controller,則TextField內(nèi)部會(huì)自動(dòng)創(chuàng)建一個(gè)。

  • focusNode:用于控制TextField是否占有當(dāng)前鍵盤的輸入焦點(diǎn)。它是我們和鍵盤交互的一個(gè)句柄(handle)。

  • InputDecoration:用于控制TextField的外觀顯示,如提示文本、背景顏色、邊框等。

  • keyboardType:用于設(shè)置該輸入框默認(rèn)的鍵盤輸入類型,取值如下:
TextInputType枚舉值 含義
text 文本輸入鍵盤
multiline 多行文本,需和 maxLines 配合使用(設(shè)為null或大于1)
number 數(shù)字;會(huì)彈出數(shù)字鍵盤
phone 優(yōu)化后的電話號(hào)碼輸入鍵盤;會(huì)彈出數(shù)字鍵盤并顯示“* #”
datetime 優(yōu)化后的日期輸入鍵盤;Android 上會(huì)顯示“: -”
emailAddress 優(yōu)化后的電子郵件地址;會(huì)顯示“@ .”
url 優(yōu)化后的url輸入鍵盤; 會(huì)顯示“/ .”

  • textInputAction:鍵盤動(dòng)作按鈕圖標(biāo)(即回車鍵位圖標(biāo)),它是一個(gè)枚舉值,有多個(gè)可選值,全部的取值列表讀者可以查看 API 文檔,下面是當(dāng)值為TextInputAction.search時(shí),原生Android系統(tǒng)下鍵盤樣式如圖3-24所示:

圖3-24

  • style:正在編輯的文本樣式。

  • textAlign: 輸入框內(nèi)編輯文本在水平方向的對(duì)齊方式。

  • autofocus: 是否自動(dòng)獲取焦點(diǎn)。

  • obscureText:是否隱藏正在編輯的文本,如用于輸入密碼的場(chǎng)景等,文本內(nèi)容會(huì)用“?”替換。

  • maxLines:輸入框的最大行數(shù),默認(rèn)為1;如果為null,則無(wú)行數(shù)限制。

  • maxLengthmaxLengthEnforcedmaxLength代表輸入框文本的最大長(zhǎng)度,設(shè)置后輸入框右下角會(huì)顯示輸入的文本計(jì)數(shù)。maxLengthEnforced決定當(dāng)輸入文本長(zhǎng)度超過(guò)maxLength時(shí)是否阻止輸入,為true時(shí)會(huì)阻止輸入,為false時(shí)不會(huì)阻止輸入但輸入框會(huì)變紅。

  • onChange:輸入框內(nèi)容改變時(shí)的回調(diào)函數(shù);注:內(nèi)容改變事件也可以通過(guò)controller來(lái)監(jiān)聽。

  • onEditingCompleteonSubmitted:這兩個(gè)回調(diào)都是在輸入框輸入完成時(shí)觸發(fā),比如按了鍵盤的完成鍵(對(duì)號(hào)圖標(biāo))或搜索鍵(????圖標(biāo))。不同的是兩個(gè)回調(diào)簽名不同,onSubmitted回調(diào)是ValueChanged<String>類型,它接收當(dāng)前輸入內(nèi)容做為參數(shù),而onEditingComplete不接收參數(shù)。

  • inputFormatters:用于指定輸入格式;當(dāng)用戶輸入內(nèi)容改變時(shí),會(huì)根據(jù)指定的格式來(lái)校驗(yàn)。

  • enable:如果為false,則輸入框會(huì)被禁用,禁用狀態(tài)不接收輸入和事件,同時(shí)顯示禁用態(tài)樣式(在其decoration中定義)。

  • cursorWidth、cursorRadiuscursorColor:這三個(gè)屬性是用于自定義輸入框光標(biāo)寬度、圓角和顏色的。

#示例:登錄輸入框

#布局

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

運(yùn)行后,效果如圖3-25所示:

圖3-25

#獲取輸入內(nèi)容

獲取輸入內(nèi)容有兩種方式:

  1. 定義兩個(gè)變量,用于保存用戶名和密碼,然后在onChange觸發(fā)時(shí),各自保存一下輸入內(nèi)容。
  2. 通過(guò)controller直接獲取。

第一種方式比較簡(jiǎn)單,不在舉例,我們來(lái)重點(diǎn)看一下第二種方式,我們以用戶名輸入框舉例:

定義一個(gè)controller

//定義一個(gè)controller
TextEditingController _unameController = TextEditingController();

然后設(shè)置輸入框controller:

TextField(
    autofocus: true,
    controller: _unameController, //設(shè)置controller
    ...
)

通過(guò) controller 獲取輸入框內(nèi)容

print(_unameController.text)

#監(jiān)聽文本變化

監(jiān)聽文本變化也有兩種方式:

  1. 設(shè)置onChange回調(diào),如:

   TextField(
       autofocus: true,
       onChanged: (v) {
         print("onChange: $v");
       }
   )

  1. 通過(guò)controller監(jiān)聽,如:

   @override
   void initState() {
     //監(jiān)聽輸入改變  
     _unameController.addListener((){
       print(_unameController.text);
     });
   }

兩種方式相比,onChanged是專門用于監(jiān)聽文本變化,而controller的功能卻多一些,除了能監(jiān)聽文本變化外,它還可以設(shè)置默認(rèn)值、選擇文本,下面我們看一個(gè)例子:

創(chuàng)建一個(gè)controller:

TextEditingController _selectionController =  TextEditingController();

設(shè)置默認(rèn)值,并從第三個(gè)字符開始選中后面的字符

_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
    baseOffset: 2,
    extentOffset: _selectionController.text.length
);

設(shè)置controller:

TextField(
  controller: _selectionController,
)

運(yùn)行效果如圖3-26所示:

#控制焦點(diǎn)

焦點(diǎn)可以通過(guò)FocusNodeFocusScopeNode來(lái)控制,默認(rèn)情況下,焦點(diǎn)由FocusScope來(lái)管理,它代表焦點(diǎn)控制范圍,可以在這個(gè)范圍內(nèi)可以通過(guò)FocusScopeNode在輸入框之間移動(dòng)焦點(diǎn)、設(shè)置默認(rèn)焦點(diǎn)等。我們可以通過(guò)FocusScope.of(context) 來(lái)獲取Widget樹中默認(rèn)的FocusScopeNode。下面看一個(gè)示例,在此示例中創(chuàng)建兩個(gè)TextField,第一個(gè)自動(dòng)獲取焦點(diǎn),然后創(chuàng)建兩個(gè)按鈕:

  • 點(diǎn)擊第一個(gè)按鈕可以將焦點(diǎn)從第一個(gè)TextField挪到第二個(gè)TextField
  • 點(diǎn)擊第二個(gè)按鈕可以關(guān)閉鍵盤。

我們要實(shí)現(xiàn)的效果如圖3-27所示:

圖3-27

代碼如下:

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,//關(guān)聯(lián)focusNode1
            decoration: InputDecoration(
                labelText: "input1"
            ),
          ),
          TextField(
            focusNode: focusNode2,//關(guān)聯(lián)focusNode2
            decoration: InputDecoration(
                labelText: "input2"
            ),
          ),
          Builder(builder: (ctx) {
            return Column(
              children: <Widget>[
                RaisedButton(
                  child: Text("移動(dòng)焦點(diǎn)"),
                  onPressed: () {
                    //將焦點(diǎn)從第一個(gè)TextField移到第二個(gè)TextField
                    // 這是一種寫法 FocusScope.of(context).requestFocus(focusNode2);
                    // 這是第二種寫法
                    if(null == focusScopeNode){
                      focusScopeNode = FocusScope.of(context);
                    }
                    focusScopeNode.requestFocus(focusNode2);
                  },
                ),
                RaisedButton(
                  child: Text("隱藏鍵盤"),
                  onPressed: () {
                    // 當(dāng)所有編輯框都失去焦點(diǎn)時(shí)鍵盤就會(huì)收起  
                    focusNode1.unfocus();
                    focusNode2.unfocus();
                  },
                ),
              ],
            );
          },
          ),
        ],
      ),
    );
  }


}

FocusNodeFocusScopeNode還有一些其它的方法,詳情可以查看API文檔。

#監(jiān)聽焦點(diǎn)狀態(tài)改變事件

FocusNode繼承自ChangeNotifier,通過(guò)FocusNode可以監(jiān)聽焦點(diǎn)的改變事件,如:

...
// 創(chuàng)建 focusNode   
FocusNode focusNode = new FocusNode();
...
// focusNode綁定輸入框   
TextField(focusNode: focusNode);
...
// 監(jiān)聽焦點(diǎn)變化    
focusNode.addListener((){
   print(focusNode.hasFocus);
});

獲得焦點(diǎn)時(shí)focusNode.hasFocus值為true,失去焦點(diǎn)時(shí)為false。

#自定義樣式

雖然我們可以通過(guò)decoration屬性來(lái)定義輸入框樣式,下面以自定義輸入框下劃線顏色為例來(lái)介紹一下:

TextField(
  decoration: InputDecoration(
    labelText: "請(qǐng)輸入用戶名",
    prefixIcon: Icon(Icons.person),
    // 未獲得焦點(diǎn)下劃線設(shè)為灰色
    enabledBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.grey),
    ),
    //獲得焦點(diǎn)下劃線設(shè)為藍(lán)色
    focusedBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.blue),
    ),
  ),
),

上面代碼我們直接通過(guò) InputDecoration 的 enabledBorder 和 focusedBorder 來(lái)分別設(shè)置了輸入框在未獲取焦點(diǎn)和獲得焦點(diǎn)后的下劃線顏色。另外,我們也可以通過(guò)主題來(lái)自定義輸入框的樣式,下面我們探索一下如何在不使用 enabledBorder 和 focusedBorder 的情況下來(lái)自定義下滑線顏色。

由于TextField在繪制下劃線時(shí)使用的顏色是主題色里面的hintColor,但提示文本顏色也是用的hintColor, 如果我們直接修改hintColor,那么下劃線和提示文本的顏色都會(huì)變。值得高興的是decoration中可以設(shè)置hintStyle,它可以覆蓋hintColor,并且主題中可以通過(guò)inputDecorationTheme來(lái)設(shè)置輸入框默認(rèn)的decoration。所以我們可以通過(guò)主題來(lái)自定義,代碼如下:

Theme(
  data: Theme.of(context).copyWith(
      hintColor: Colors.grey[200], //定義下劃線顏色
      inputDecorationTheme: InputDecorationTheme(
          labelStyle: TextStyle(color: Colors.grey),//定義label字體樣式
          hintStyle: TextStyle(color: Colors.grey, fontSize: 14.0)//定義提示文本樣式
      )
  ),
  child: Column(
    children: <Widget>[
      TextField(
        decoration: InputDecoration(
            labelText: "用戶名",
            hintText: "用戶名或郵箱",
            prefixIcon: Icon(Icons.person)
        ),
      ),
      TextField(
        decoration: InputDecoration(
            prefixIcon: Icon(Icons.lock),
            labelText: "密碼",
            hintText: "您的登錄密碼",
            hintStyle: TextStyle(color: Colors.grey, fontSize: 13.0)
        ),
        obscureText: true,
      )
    ],
  )
)

運(yùn)行效果如圖3-28所示:

圖3-28

我們成功的自定義了下劃線顏色和提問(wèn)文字樣式,細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn),通過(guò)這種方式自定義后,輸入框在獲取焦點(diǎn)時(shí),labelText不會(huì)高亮顯示了,正如上圖中的"用戶名"本應(yīng)該顯示藍(lán)色,但現(xiàn)在卻顯示為灰色,并且我們還是無(wú)法定義下劃線寬度。另一種靈活的方式是直接隱藏掉TextField本身的下劃線,然后通過(guò)Container去嵌套定義樣式,如:

Container(
  child: TextField(
    keyboardType: TextInputType.emailAddress,
    decoration: InputDecoration(
        labelText: "Email",
        hintText: "電子郵件地址",
        prefixIcon: Icon(Icons.email),
        border: InputBorder.none //隱藏下劃線
    )
  ),
  decoration: BoxDecoration(
      // 下滑線淺灰色,寬度1像素
      border: Border(bottom: BorderSide(color: Colors.grey[200], width: 1.0))
  ),
)

運(yùn)行效果:

image-20180904150511545

通過(guò)這種組件組合的方式,也可以定義背景圓角等。一般來(lái)說(shuō),優(yōu)先通過(guò)decoration來(lái)自定義樣式,如果decoration實(shí)現(xiàn)不了,再用 widget 組合的方式。

思考題:在這個(gè)示例中,下劃線顏色是固定的,所以獲得焦點(diǎn)后顏色仍然為灰色,如何實(shí)現(xiàn)點(diǎn)擊后下滑線也變色呢?

#3.7.2 表單Form

實(shí)際業(yè)務(wù)中,在正式向服務(wù)器提交數(shù)據(jù)前,都會(huì)對(duì)各個(gè)輸入框數(shù)據(jù)進(jìn)行合法性校驗(yàn),但是對(duì)每一個(gè)TextField都分別進(jìn)行校驗(yàn)將會(huì)是一件很麻煩的事。還有,如果用戶想清除一組TextField的內(nèi)容,除了一個(gè)一個(gè)清除有沒(méi)有什么更好的辦法呢?為此,F(xiàn)lutter 提供了一個(gè)Form 組件,它可以對(duì)輸入框進(jìn)行分組,然后進(jìn)行一些統(tǒng)一操作,如輸入內(nèi)容校驗(yàn)、輸入框重置以及輸入內(nèi)容保存。

#Form

Form繼承自StatefulWidget對(duì)象,它對(duì)應(yīng)的狀態(tài)類為FormState。我們先看看Form類的定義:

Form({
  @required Widget child,
  bool autovalidate = false,
  WillPopCallback onWillPop,
  VoidCallback onChanged,
})

  • autovalidate:是否自動(dòng)校驗(yàn)輸入內(nèi)容;當(dāng)為true時(shí),每一個(gè)子 FormField 內(nèi)容發(fā)生變化時(shí)都會(huì)自動(dòng)校驗(yàn)合法性,并直接顯示錯(cuò)誤信息。否則,需要通過(guò)調(diào)用FormState.validate()來(lái)手動(dòng)校驗(yàn)。
  • onWillPop:決定Form所在的路由是否可以直接返回(如點(diǎn)擊返回按鈕),該回調(diào)返回一個(gè)Future對(duì)象,如果 Future 的最終結(jié)果是false,則當(dāng)前路由不會(huì)返回;如果為true,則會(huì)返回到上一個(gè)路由。此屬性通常用于攔截返回按鈕。
  • onChangedForm的任意一個(gè)子FormField內(nèi)容發(fā)生變化時(shí)會(huì)觸發(fā)此回調(diào)。

#FormField

Form的子孫元素必須是FormField類型,FormField是一個(gè)抽象類,定義幾個(gè)屬性,FormState內(nèi)部通過(guò)它們來(lái)完成操作,FormField部分定義如下:

const FormField({
  ...
  FormFieldSetter<T> onSaved, //保存回調(diào)
  FormFieldValidator<T>  validator, //驗(yàn)證回調(diào)
  T initialValue, //初始值
  bool autovalidate = false, //是否自動(dòng)校驗(yàn)。
})

為了方便使用,F(xiàn)lutter 提供了一個(gè)TextFormField組件,它繼承自FormField類,也是TextField的一個(gè)包裝類,所以除了FormField定義的屬性之外,它還包括TextField的屬性。

#FormState

FormStateFormState類,可以通過(guò)Form.of()GlobalKey獲得。我們可以通過(guò)它來(lái)對(duì)Form的子孫FormField進(jìn)行統(tǒng)一操作。我們看看其常用的三個(gè)方法:

  • FormState.validate():調(diào)用此方法后,會(huì)調(diào)用Form子孫FormField的validate回調(diào),如果有一個(gè)校驗(yàn)失敗,則返回false,所有校驗(yàn)失敗項(xiàng)都會(huì)返回用戶返回的錯(cuò)誤提示。
  • FormState.save():調(diào)用此方法后,會(huì)調(diào)用Form子孫FormFieldsave回調(diào),用于保存表單內(nèi)容
  • FormState.reset():調(diào)用此方法后,會(huì)將子孫FormField的內(nèi)容清空。

#示例

我們修改一下上面用戶登錄的示例,在提交之前校驗(yàn):

  1. 用戶名不能為空,如果為空則提示“用戶名不能為空”。
  2. 密碼不能小于6位,如果小于6為則提示“密碼不能少于6位”。

完整代碼:

class FormTestRoute extends StatefulWidget {
  @override
  _FormTestRouteState createState() => new _FormTestRouteState();
}


class _FormTestRouteState extends State<FormTestRoute> {
  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, //設(shè)置globalKey,用于后面獲取FormState
          autovalidate: true, //開啟自動(dòng)校驗(yàn)
          child: Column(
            children: <Widget>[
              TextFormField(
                  autofocus: true,
                  controller: _unameController,
                  decoration: InputDecoration(
                      labelText: "用戶名",
                      hintText: "用戶名或郵箱",
                      icon: Icon(Icons.person)
                  ),
                  // 校驗(yàn)用戶名
                  validator: (v) {
                    return v
                        .trim()
                        .length > 0 ? null : "用戶名不能為空";
                  }


              ),
              TextFormField(
                  controller: _pwdController,
                  decoration: InputDecoration(
                      labelText: "密碼",
                      hintText: "您的登錄密碼",
                      icon: Icon(Icons.lock)
                  ),
                  obscureText: true,
                  //校驗(yàn)密碼
                  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: () {
                          //在這里不能通過(guò)此方式獲取FormState,context不對(duì)
                          //print(Form.of(context));

                            
                          // 通過(guò)_formKey.currentState 獲取FormState后,
                          // 調(diào)用validate()方法校驗(yàn)用戶名密碼是否合法,校驗(yàn)
                          // 通過(guò)后再提交數(shù)據(jù)。 
                          if((_formKey.currentState as FormState).validate()){
                            //驗(yàn)證通過(guò)提交數(shù)據(jù)
                          }
                        },
                      ),
                    ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

運(yùn)行后效果如圖3-29所示:

圖3-29

注意,登錄按鈕的onPressed方法中不能通過(guò)Form.of(context)來(lái)獲取,原因是,此處的contextFormTestRoute的 context,而Form.of(context)是根據(jù)所指定context向根去查找,而FormState是在FormTestRoute的子樹中,所以不行。正確的做法是通過(guò)Builder來(lái)構(gòu)建登錄按鈕,Builder會(huì)將widget節(jié)點(diǎn)的context作為回調(diào)參數(shù):

Expanded(
 // 通過(guò)Builder來(lái)獲取RaisedButton所在widget樹的真正context(Element) 
  child:Builder(builder: (context){
    return RaisedButton(
      ...
      onPressed: () {
        //由于本widget也是Form的子代widget,所以可以通過(guò)下面方式獲取FormState  
        if(Form.of(context).validate()){
          //驗(yàn)證通過(guò)提交數(shù)據(jù)
        }
      },
    );
  })
)

其實(shí)context正是操作 Widget 所對(duì)應(yīng)的Element的一個(gè)接口,由于 Widget 樹對(duì)應(yīng)的Element都是不同的,所以context也都是不同的,有關(guān)context的更多內(nèi)容會(huì)在后面高級(jí)部分詳細(xì)討論。Flutter 中有很多“of(context)”這種方法,讀者在使用時(shí)一定要注意context是否正確。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)