## 輸入框及表單
Material widget庫中提供了豐富的輸入框及表單Widget。下面我們分別介紹一下。
### 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的外觀顯示,如提示文本、背景顏色、邊框等。
- keyboardType:用于設置該輸入框默認的鍵盤輸入類型,取值如下:
| TextInputType枚舉值 | 含義 | | ------------------- | --------------------------------------------------- | | text | 文本輸入鍵盤 | | multiline | 多行文本,需和maxLines配合使用(設為null或大于1) | | number | 數字;會彈出數字鍵盤 | | phone | 優化后的電話號碼輸入鍵盤;會彈出數字鍵盤并顯示"\* #" | | datetime | 優化后的日期輸入鍵盤;Android上會顯示“: -” | | emailAddress | 優化后的電子郵件地址;會顯示“@ .” | | url | 優化后的url輸入鍵盤; 會顯示“/ .” |
- textInputAction:鍵盤動作按鈕圖標(即回車鍵位圖標),它是一個枚舉值,有多個可選值,全部的取值列表讀者可以查看API文檔,下面是當值為`TextInputAction.search`時,原生Android系統下鍵盤樣式:

####
- style:正在編輯的文本樣式。
- textAlign: 輸入框內編輯文本在水平方向的對齊方式。
- autofocus: 是否自動獲取焦點。
- obscureText:是否隱藏正在編輯的文本,如用于輸入密碼的場景等,文本內容會用“?”替換。
- maxLines:輸入框的最大行數,默認為1;如果為`null`,則無行數限制。
- maxLength和maxLengthEnforced :maxLength代表輸入框文本的最大長度,設置后輸入框右下角會顯示輸入的文本計數。maxLengthEnforced決定當輸入文本長度超過maxLength時是否阻止輸入,為true時會阻止輸入,為false時不會阻止輸入但輸入框會變紅。
- onChange:輸入框內容改變時的回調函數;注:內容改變事件也可以通過controller來監聽。
- onEditingComplete和onSubmitted:這兩個回調都是在輸入框輸入完成時觸發,比如按了鍵盤的完成鍵(對號圖標)或搜索鍵(??圖標)。不同的是兩個回調簽名不同,onSubmitted回調是`ValueChanged<String>`類型,它接收當前輸入內容做為參數,而onEditingComplete不接收參數。
- inputFormatters:用于指定輸入格式;當用戶輸入內容改變時,會根據指定的格式來校驗。
- enable:如果為`false`,則輸入框會被禁用,禁用狀態不接收輸入和事件,同時顯示禁用態樣式(在其decoration中定義)。
- cursorWidth、cursorRadius和cursorColor:這三個屬性是用于自定義輸入框光標寬度、圓角和顏色的。
#### 示例:登錄輸入框
##### 布局
```
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,
),
],
);
```

##### 獲取輸入內容
獲取輸入內容有兩種方式:
1. 定義兩個變量,用于保存用戶名和密碼,然后在onChange觸發時,各自保存一下輸入內容。
2. 通過controller直接獲取。
第一種方式比較簡單,不在舉例,我們來重點看一下第二種方式,我們以用戶名輸入框舉例:
定義一個controller:
```
//定義一個controller
TextEditingController _unameController=new TextEditingController();
```
然后設置輸入框controller:
```
TextField(
autofocus: true,
controller: _unameController, //設置controller
...
)
```
通過controller獲取輸入框內容
```
print(_unameController.text)
```
##### 監聽文本變化
監聽文本變化也有兩種方式:
1. 設置onChange回調,如:
```
TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
)
```
2. 通過controller監聽,如:
```
@override
void initState() {
//監聽輸入改變
_unameController.addListener((){
print(_unameController.text);
});
}
```
兩種方式相比,onChanged是專門用于監聽文本變化,而controller的功能卻多一些,除了能監聽文本變化外,它還可以設置默認值、選擇文本,下面我們看一個例子:
創建一個controller:
```
TextEditingController _selectionController = new TextEditingController();
```
設置默認值,并從第三個字符開始選中后面的字符
```
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
baseOffset: 2,
extentOffset: _selectionController.text.length
);
```
設置controller:
```
TextField(
controller: _selectionController,
)
```
運行效果如下:

##### 控制焦點
焦點可以通過FocusNode和FocusScopeNode來控制,默認情況下,焦點由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和FocusScopeNode還有一些其它的方法,詳情可以查看API文檔。
##### 監聽焦點狀態改變事件
FocusNode繼承自ChangeNotifier,通過FocusNode可以監聽焦點的改變事件,如:
```
...
// 創建 focusNode
FocusNode focusNode = new FocusNode();
...
// focusNode綁定輸入框
TextField(focusNode: focusNode);
...
// 監聽焦點變化
focusNode.addListener((){
print(focusNode.hasFocus);
});
```
獲得焦點時`focusNode.hasFocus`值為`true`,失去焦點時為`false`。
##### 自定義樣式
雖然我們可以通過decoration屬性來定義輸入框樣式,但是有一些樣式如下劃線默認顏色及寬度都是不能直接自定義的,下面的代碼**沒有效果**:
```
TextField(
...
decoration: InputDecoration(
border: UnderlineInputBorder(
//下面代碼沒有效果
borderSide: BorderSide(
color: Colors.red,
width: 5.0
)),
prefixIcon: Icon(Icons.person)
),
),
```
之所以如此,是由于TextField在繪制下劃線時使用的顏色是主題色里面的`hintColor`,但提示文本顏色也是用的`hintColor`, 如果我們直接修改`hintColor`,那么下劃線和提示文本的顏色都會變。值得高興的是decoration中可以設置`hintStyle`,它可以覆蓋`hintColor`,并且主題中可以通過`inputDecorationTheme`來設置輸入框默認的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,
)
],
)
)
```
運行效果如下:

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

通過這種widget組合的方式,也可以定義背景圓角等。一般來說,優先通過decoration來自定義樣式,如果decoration實現不了,再用widget組合的方式。
> 思考題:在這個示例中,下劃線顏色是固定的,所以獲得焦點后顏色仍然為灰色,如何實現點擊后下滑線也變色呢?
### 表單Form
實際業務中,在正式向服務器提交數據前,都會對各個輸入框數據進行合法性校驗,但是對每一個TextField都分別進行校驗將會是一件很麻煩的事。還有,如果用戶想清除一組TextField的內容,除了一個一個清除有沒有什么更好的辦法呢?為此,Flutter提供了一個Form widget,它可以對輸入框進行分組,然后進行一些統一操作,如輸入內容校驗、輸入框重置以及輸入內容保存。
#### Form
Form繼承自StatefulWidget對象,它對應的狀態類為FormState。我們先看看Form類的定義:
```
Form({
@required Widget child,
bool autovalidate = false,
WillPopCallback onWillPop,
VoidCallback onChanged,
})
```
- autovalidate:是否自動校驗輸入內容;當為`true`時,每一個子FormField內容發生變化時都會自動校驗合法性,并直接顯示錯誤信息。否則,需要通過調用`FormState.validate()`來手動校驗。
- onWillPop:決定Form所在的路由是否可以直接返回(如點擊返回按鈕),該回調返回一個`Future`對象,如果Future的最終結果是false,則當前路由不會返回;如果為`true`,則會返回到上一個路由。此屬性通常用于攔截返回按鈕。
- onChanged:Form的任意一個子FormField內容發生變化時會觸發此回調。
#### FormField
Form的子孫元素必須是FormField類型,FormField是一個抽象類,定義幾個屬性,FormState內部通過它們來完成操作,FormField部分定義如下:
```
const FormField({
...
FormFieldSetter<T> onSaved, //保存回調
FormFieldValidator<T> validator, //驗證回調
T initialValue, //初始值
bool autovalidate = false, //是否自動校驗。
})
```
為了方便使用,Flutter提供了一個TextFormField widget,它繼承自FormField類,也是TextField的一個包裝類,所以除了FormField定義的屬性之外,它還包括TextField的屬性。
#### FormState
FormState為Form的State類,可以通過`Form.of()`或GlobalKey獲得。我們可以通過它來對Form的子孫FormField進行統一操作。我們看看其常用的三個方法:
- `FormState.validate()`:調用此方法后,會調用Form子孫FormField的validate回調,如果有一個校驗失敗,則返回false,所有校驗失敗項都會返回用戶返回的錯誤提示。
- `FormState.save()`:調用此方法后,會調用Form子孫FormField的save回調,用于保存表單內容
- `FormState.reset()`:調用此方法后,會將子孫FormField的內容清空。
#### 示例
我們修改一下上面用戶登錄的示例,在提交之前校驗:
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 PageScaffold(
title: "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)`來獲取,原因是,此處的context為FormTestRoute的context,而`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正是操作Widget所對應的Element的一個接口,由于Widget樹對應的Element都是不同的,所以context也都是不同的,有關context的更多內容會在后面高級部分詳細討論。Flutter中有很多“of(context)”這種方法,在使用時讀者一定要注意context是否正確。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示