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
,則無行數限制。 -
maxLength
和maxLengthEnforced
:maxLength
代表輸入框文字的最大長度,設定後輸入框右下角會顯示輸入的文字計數。maxLengthEnforced
決定當輸入文字長度超過maxLength
時是否阻止輸入,為true
時會阻止輸入,為false
時不會阻止輸入但輸入框會變紅。 -
onChanged
:輸入框內容改變時的回撥函式;注:內容改變事件也可以通過controller
來監聽。 -
onEditingComplete
和onSubmitted
:這兩個回撥都是在輸入框輸入完成時觸發,比如按了鍵盤的完成鍵(對號圖示)或搜尋鍵(?圖示)。不同的是兩個回撥簽名不同,onSubmitted
回撥是ValueChanged<String>
型別,它接收當前輸入內容做為引數,而onEditingComplete
不接收引數。 -
inputFormatters
:用於指定輸入格式;當使用者輸入內容改變時,會根據指定的格式來校驗。 -
cursorWidth
、cursorRadius
和cursorColor
:這三個屬性是用於自定義輸入框游標寬度、圓角和顏色的。
如:cursorColor: Colors.green
、cursorWidth: 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
直接獲取。
第一種方式比較簡單,所以就不說了,重點說第二種方式,以使用者名稱輸入框為例:
- 定義一個
controller
:
//定義一個controller
TextEditingController _unameController = TextEditingController();
複製程式碼
- 設定輸入框
controller
:
TextField(
autofocus: true,
controller: _unameController, //設定controller
...
)
複製程式碼
- 通過
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,
),
],
),
);
}
}
複製程式碼
執行效果:
監聽文字變化
監聽文字變化也有兩種方式:
- 設定
onChanged
回撥,如:
TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
)
複製程式碼
- 通過
controller
監聽,如:
@override
void initState() {
//監聽輸入改變
_unameController.addListener((){
print(_unameController.text);
});
}
複製程式碼
這兩種方式相比,onChanged
是專門用於監聽文字變化,而controller
的功能卻多一些,除了能監聽文字變化外,它還可以設定預設值、選擇文字,
例子:
- 建立一個
controller
:
TextEditingController _selectionController = TextEditingController();
複製程式碼
- 設定預設值,並從第三個字元開始選中後面的字元:
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
baseOffset: 2,
extentOffset: _selectionController.text.length
);
複製程式碼
- 設定
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,
),
],
),
);
}
}
複製程式碼
執行效果:
控制焦點
焦點可以通過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
繼承自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
,則會返回到上一個路由。此屬性通常用於攔截返回按鈕。 -
onChanged
:Form
的任意一個子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
FormState
為Form
的State
類,可以通過Form.of()
或GlobalKey
獲得。
我們可以通過它來對Form
的子孫FormField
進行統一操作。
常用的三個方法:
-
FormState.validate()
:呼叫此方法後,會呼叫Form
子孫FormField
的validate
回撥,如果有一個校驗失敗,則返回false
,所有校驗失敗項都會返回使用者返回的錯誤提示。 -
FormState.save()
:呼叫此方法後,會呼叫Form
子孫FormField
的save
回撥,用於儲存表單內容。 -
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)
來獲取,原因是,此處的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
的指向問題。