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

2021-03-06 17:59 更新

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

#3.7.1 TextField

TextField用于文本輸入,它提供了很多屬性,我們先簡單介紹一下主要屬性的作用,然后通過幾個示例來演示一下關(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:編輯框的控制器,通過它可以設(shè)置/獲取編輯框的內(nèi)容、選擇編輯內(nèi)容、監(jiān)聽編輯文本改變事件。大多數(shù)情況下我們都需要顯式提供一個controller來與文本框交互。如果沒有提供controller,則TextField內(nèi)部會自動創(chuàng)建一個。

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

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

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

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

圖3-24

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

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

  • autofocus: 是否自動獲取焦點。

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

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

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

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

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

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

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

  • cursorWidthcursorRadiuscursorColor:這三個屬性是用于自定義輸入框光標(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,
          ),
        ],
);

運行后,效果如圖3-25所示:

圖3-25

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

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

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

第一種方式比較簡單,不在舉例,我們來重點看一下第二種方式,我們以用戶名輸入框舉例:

定義一個controller

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

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

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

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

print(_unameController.text)

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

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

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

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

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

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

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

創(chuàng)建一個controller:

TextEditingController _selectionController =  TextEditingController();

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

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

設(shè)置controller:

TextField(
  controller: _selectionController,
)

運行效果如圖3-26所示:

#控制焦點

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

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

我們要實現(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("移動焦點"),
                  onPressed: () {
                    //將焦點從第一個TextField移到第二個TextField
                    // 這是一種寫法 FocusScope.of(context).requestFocus(focusNode2);
                    // 這是第二種寫法
                    if(null == focusScopeNode){
                      focusScopeNode = FocusScope.of(context);
                    }
                    focusScopeNode.requestFocus(focusNode2);
                  },
                ),
                RaisedButton(
                  child: Text("隱藏鍵盤"),
                  onPressed: () {
                    // 當(dāng)所有編輯框都失去焦點時鍵盤就會收起  
                    focusNode1.unfocus();
                    focusNode2.unfocus();
                  },
                ),
              ],
            );
          },
          ),
        ],
      ),
    );
  }


}

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

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

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

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

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

#自定義樣式

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

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

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

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

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

運行效果如圖3-28所示:

圖3-28

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

運行效果:

image-20180904150511545

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

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

#3.7.2 表單Form

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

#Form

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

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

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

#FormField

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

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

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

#FormState

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

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

#示例

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

  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, //開啟自動校驗
          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后,
                          // 調(diào)用validate()方法校驗用戶名密碼是否合法,校驗
                          // 通過后再提交數(shù)據(jù)。 
                          if((_formKey.currentState as FormState).validate()){
                            //驗證通過提交數(shù)據(jù)
                          }
                        },
                      ),
                    ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

運行后效果如圖3-29所示:

圖3-29

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

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號