跟隨《Flutter實戰(zhàn)·第二版》學(xué)習(xí)殷费,建議直接看原書
Material 組件庫中提供了輸入框組件TextField和表單組件Form
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,
this.maxLengthEnforcement,
ToolbarOptions? toolbarOptions,
ValueChanged<String> onChanged,
VoidCallback onEditingComplete,
ValueChanged<String> onSubmitted,
List<TextInputFormatter> inputFormatters,
bool enabled,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
this.onTap,
...
})
- controller:編輯框的控制器拘悦,通過它可以設(shè)置/獲取編輯框的內(nèi)容齿兔、選擇編輯內(nèi)容、監(jiān)聽編輯文本改變事件础米。大多數(shù)情況下我們都需要顯式提供一個controller來與文本框交互分苇。如果沒有提供controller,則TextField內(nèi)部會自動創(chuàng)建一個
- focusNode:用于控制TextField是否占有當前鍵盤的輸入焦點屁桑。它是我們和鍵盤交互的一個句柄(handle)
- InputDecoration:用于控制TextField的外觀顯示,如提示文本蘑斧、背景顏色靖秩、邊框等
-
keyboardType:用于設(shè)置該輸入框默認的鍵盤輸入類型,取值如下:
-
textInputAction:鍵盤動作按鈕圖標(即回車鍵位圖標)竖瘾,它是一個枚舉值沟突,有多個可選值,全部的取值列表讀者可以查看API文檔准浴,下面是當值為TextInputAction.search時事扭,原生Android系統(tǒng)下鍵盤樣式所示:
- style:正在編輯的文本樣式。
- textAlign: 輸入框內(nèi)編輯文本在水平方向的對齊方式乐横。
- autofocus: 是否自動獲取焦點求橄。
- obscureText:是否隱藏正在編輯的文本今野,如用于輸入密碼的場景等,文本內(nèi)容會用“?”替換
- maxLines:輸入框的最大行數(shù)罐农,默認為1条霜;如果為null,則無行數(shù)限制涵亏。
- maxLength和maxLengthEnforcement :maxLength代表輸入框文本的最大長度宰睡,設(shè)置后輸入框右下角會顯示輸入的文本計數(shù)。maxLengthEnforcement決定當輸入文本長度超過maxLength時如何處理气筋,如截斷队贱、超出等流强。
- toolbarOptions:長按或鼠標右擊時出現(xiàn)的菜單,包括 copy、cut索守、paste 以及 selectAll捏题。
- onChange:輸入框內(nèi)容改變時的回調(diào)函數(shù)官觅;注:內(nèi)容改變事件也可以通過controller來監(jiān)聽摇展。
- onEditingComplete和onSubmitted:這兩個回調(diào)都是在輸入框輸入完成時觸發(fā),比如按了鍵盤的完成鍵(對號圖標)或搜索鍵(??圖標)瓤球。不同的是兩個回調(diào)簽名不同融欧,onSubmitted回調(diào)是ValueChanged<String>類型,它接收當前輸入內(nèi)容做為參數(shù)卦羡,而onEditingComplete不接收參數(shù)噪馏。
- inputFormatters:用于指定輸入格式;當用戶輸入內(nèi)容改變時虹茶,會根據(jù)指定的格式來校驗逝薪。
- enable:如果為false,則輸入框會被禁用蝴罪,禁用狀態(tài)不接收輸入和事件董济,同時顯示禁用態(tài)樣式(在其decoration中定義)。
- cursorWidth要门、cursorRadius和cursorColor:這三個屬性是用于自定義輸入框光標寬度虏肾、圓角和顏色的
示例:登錄輸入框
布局
Column(
children: [
TextField(
autofocus: true,
decoration: InputDecoration(
// labelText: "用戶名",
hintText: "用戶名或郵箱",
prefixIcon: Icon(Icons.person),
),
),
TextField(
decoration: InputDecoration(
labelText: "密碼",
hintText: "您的登錄密碼",
prefixIcon: Icon(Icons.lock),
),
textInputAction: TextInputAction.search,
obscureText: true,
)
],
)
獲取輸入內(nèi)容
獲取輸入內(nèi)容有兩種方式:
- 定義兩個變量,用于保存用戶名和密碼欢搜,然后在onChange觸發(fā)時封豪,各自保存一下輸入內(nèi)容
- 通過controller直接獲取。
第一種方式比較簡單炒瘟,不在舉例吹埠,我們來重點看一下第二種方式,我們以用戶名輸入框舉例:
定義一個controller:
//定義一個controller
TextEditingController _unameController = TextEditingController();
然后設(shè)置輸入框controller:
TextField(
autofocus: true,
controller: _unameController, //設(shè)置controller
...
)
通過controller獲取輸入框內(nèi)容
print(_unameController.text)
監(jiān)聽文本變化
- 設(shè)置onChange回調(diào),如:
TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
)
- 通過controller監(jiān)聽缘琅,如:
@override
void initState() {
//監(jiān)聽輸入改變
_unameController.addListener((){
print(_unameController.text);
});
}
兩種方式相比粘都,onChanged是專門用于監(jiān)聽文本變化,而controller的功能卻多一些刷袍,除了能監(jiān)聽文本變化外翩隧,它還可以設(shè)置默認值、選擇文本呻纹,下面我們看一個例子:
創(chuàng)建一個controller:
TextEditingController _selectionController = TextEditingController();
設(shè)置默認值堆生,并從第三個字符開始選中后面的字符
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
baseOffset: 2,
extentOffset: _selectionController.text.length
);
設(shè)置controller:
TextField(
controller: _selectionController,
)
效果
控制焦點
焦點可以通過FocusNode和FocusScopeNode來控制,默認情況下雷酪,焦點由FocusScope來管理淑仆,它代表焦點控制范圍,可以在這個范圍內(nèi)可以通過FocusScopeNode在輸入框之間移動焦點哥力、設(shè)置默認焦點等糯景。我們可以通過FocusScope.of(context) 來獲取Widget樹中默認的FocusScopeNode。下面看一個示例省骂,在此示例中創(chuàng)建兩個TextField,第一個自動獲取焦點最住,然后創(chuàng)建兩個按鈕:
- 點擊第一個按鈕可以將焦點從第一個TextField挪到第二個TextField钞澳。
- 點擊第二個按鈕可以關(guān)閉鍵盤
代碼如下:
Navigator.push(context, MaterialPageRoute(builder: (context){
return FocusTestRoute();
}));
class FocusTestRoute extends StatefulWidget {
@override
_FocusTestRouteState createState() => _FocusTestRouteState();
}
class _FocusTestRouteState extends State<FocusTestRoute> {
FocusNode node1 = FocusNode();
FocusNode node2 = FocusNode();
FocusScopeNode? scopeNode;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("hhh"),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
autofocus: true,
focusNode: node1, // 關(guān)聯(lián)node1
decoration: InputDecoration(
labelText: "input1"
),
),
TextField(
autofocus: true,
focusNode: node2, // 關(guān)聯(lián)node2
decoration: InputDecoration(
labelText: "input2"
),
),
Builder(builder: (ctx) {
return Column(
children: [
ElevatedButton(
child: Text("移動焦點"),
onPressed: () {
//將焦點從第一個TextField移到第二個TextField
// 這是一種寫法 FocusScope.of(context).requestFocus(focusNode2);
// 這是第二種寫法
if(null == scopeNode){
scopeNode = FocusScope.of(context);
}
scopeNode!.requestFocus(node2);
},
),
ElevatedButton(
child: Text("隱藏鍵盤"),
onPressed: () {
// 當所有編輯框都失去焦點時鍵盤就會收起
node1.unfocus();
node2.unfocus();
},
),
],
);
}),
],
),
),
);
}
}
監(jiān)聽焦點狀態(tài)改變事件
FocusNode繼承自ChangeNotifier,通過FocusNode可以監(jiān)聽焦點的改變事件涨缚,如:
// 創(chuàng)建focusNode
FocusNode focusNode = FocusNode();
TextField(focusNode: 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è)為藍色
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
),
),
),
上面代碼我們直接通過InputDecoration的enabledBorder和focusedBorder來分別設(shè)置了輸入框在未獲取焦點和獲得焦點后的下劃線顏色脓魏。
另外兰吟,我們也可以通過主題來自定義輸入框的樣式,下面我們探索一下如何在不使用enabledBorder和focusedBorder的情況下來自定義下滑線顏色
由于TextField在繪制下劃線時使用的顏色是主題色里面的hintColor茂翔,但提示文本顏色也是用的hintColor混蔼, 如果我們直接修改hintColor,那么下劃線和提示文本的顏色都會變珊燎。值得高興的是decoration中可以設(shè)置hintStyle惭嚣,它可以覆蓋hintColor,并且主題中可以通過inputDecorationTheme來設(shè)置輸入框默認的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,
)
],
)
)
效果:
我們成功的自定義了下劃線顏色和提問文字樣式,細心的讀者可能已經(jīng)發(fā)現(xiàn)谋国,通過這種方式自定義后槽地,輸入框在獲取焦點時,labelText不會高亮顯示了,正如上圖中的"用戶名"本應(yīng)該顯示藍色捌蚊,但現(xiàn)在卻顯示為灰色集畅,并且我們還是無法定義下劃線寬度。
另一種靈活的方式是直接隱藏掉TextField本身的下劃線逢勾,然后通過Container去嵌套定義樣式牡整,如:
Container(
child: TextField(
decoration: InputDecoration(
labelText: "Email",
hintText: "電子郵件地址",
prefixIcon: Icon(Icons.email),
border: InputBorder.none,
),
),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey, width: 1.0)),
),
),
通過這種組件組合的方式,也可以定義背景圓角等溺拱。一般來說逃贝,優(yōu)先通過decoration來自定義樣式,如果decoration實現(xiàn)不了迫摔,再用widget組合的方式沐扳。
表單Form
實際業(yè)務(wù)中,在正式向服務(wù)器提交數(shù)據(jù)前句占,都會對各個輸入框數(shù)據(jù)進行合法性校驗沪摄,但是對每一個TextField都分別進行校驗將會是一件很麻煩的事。還有纱烘,如果用戶想清除一組TextField的內(nèi)容杨拐,除了一個一個清除有沒有什么更好的辦法呢?為此擂啥,F(xiàn)lutter提供了一個Form 組件哄陶,它可以對輸入框進行分組,然后進行一些統(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)容;當為true時资锰,每一個子 FormField 內(nèi)容發(fā)生變化時都會自動校驗合法性敢课,并直接顯示錯誤信息。否則绷杜,需要通過調(diào)用FormState.validate()
來手動校驗翎猛。 -
onWillPop
:決定Form所在的路由是否可以直接返回(如點擊返回按鈕),該回調(diào)返回一個Future對象接剩,如果 Future 的最終結(jié)果是false切厘,則當前路由不會返回;如果為true懊缺,則會返回到上一個路由疫稿。此屬性通常用于攔截返回按鈕培他。 -
onChanged
:Form的任意一個子FormField內(nèi)容發(fā)生變化時會觸發(fā)此回調(diào)。
FormField
Form的子孫元素必須是FormField類型遗座,F(xiàn)ormField是一個抽象類舀凛,定義幾個屬性,F(xiàn)ormState內(nèi)部通過它們來完成操作途蒋,F(xiàn)ormField部分定義如下:
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
FormState為Form的State類,可以通過Form.of()或GlobalKey獲得畜隶。我們可以通過它來對Form的子孫FormField進行統(tǒng)一操作壁肋。
我們看看其常用的三個方法:
- FormState.validate():調(diào)用此方法后,會調(diào)用Form子孫FormField的validate回調(diào)籽慢,如果有一個校驗失敗,則返回false箱亿,所有校驗失敗項都會返回用戶返回的錯誤提示乙帮。
- FormState.save():調(diào)用此方法后,會調(diào)用Form子孫FormField的save回調(diào)极景,用于保存表單內(nèi)容
- FormState.reset():調(diào)用此方法后,會將子孫FormField的內(nèi)容清空
示例
我們修改一下上面用戶登錄的示例驾茴,在提交之前校驗:
- 用戶名不能為空盼樟,如果為空則提示“用戶名不能為空”。
- 密碼不能小于 6 位锈至,如果小于 6 為則提示“密碼不能少于 6 位”晨缴。
class FormTestRoute extends StatefulWidget {
@override
_FormTestRouteState createState() => _FormTestRouteState();
}
class _FormTestRouteState extends State<FormTestRoute> {
TextEditingController _unameController = TextEditingController();
TextEditingController _pwdController = TextEditingController();
GlobalKey _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("Form Test"),
),
body: Form(
key: _formKey, // 設(shè)置globalkey 用于后面獲取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
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(
autofocus: true,
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),
child: Row(
children: <Widget>[
Expanded(
child: ElevatedButton(
child: Padding(
padding: EdgeInsets.all(16),
child: Text("登錄"),
),
// 通過_formKey.currentState 獲取FormState后,
// 調(diào)用validate()方法校驗用戶名密碼是否合法峡捡,校驗
// 通過后再提交數(shù)據(jù)击碗。
onPressed: (){
if ((_formKey.currentState as FormState).validate()) {
print("驗證通過提交數(shù)據(jù)");
}
},
),
),
],
),
),
],
),
),
);
}
}
注意,登錄按鈕的onPressed方法中不能通過Form.of(context)來獲取们拙,原因是稍途,此處的context為FormTestRoute的context,而Form.of(context)是根據(jù)所指定context向根去查找砚婆,而FormState是在FormTestRoute的子樹中械拍,所以不行。正確的做法是通過Builder來構(gòu)建登錄按鈕,Builder會將widget節(jié)點的context作為回調(diào)參數(shù):
Expanded(
// 通過Builder來獲取ElevatedButton所在widget樹的真正context(Element)
child:Builder(builder: (context){
return ElevatedButton(
...
onPressed: () {
//由于本widget也是Form的子代widget坷虑,所以可以通過下面方式獲取FormState
if(Form.of(context).validate()){
//驗證通過提交數(shù)據(jù)
}
},
);
})
)
其實context正是操作Widget所對應(yīng)的Element的一個接口甲馋,由于Widget樹對應(yīng)的Element都是不同的,所以context也都是不同的
Flutter中有很多“of(context)”這種方法迄损,讀者在使用時一定要注意context是否正確定躏。