前言
最近Flutter發(fā)布了1.0版本,因?yàn)楣菊脹]有iOS(逃),作為公司唯一Android開發(fā)的我直接把原來的項(xiàng)目轉(zhuǎn)為Flutter,就可以一個(gè)人領(lǐng)兩個(gè)人的工資了

然而轉(zhuǎn)Flutter并沒有想象中的那么容易,雖然說Flutter本身對(duì)于Android開發(fā)者是比較友好的,但是現(xiàn)在資料也并不是很多,常常遇到問題上Google、StackOverFlow也不容易查到,只能硬著頭皮看fucking source和官方文檔。這里就記錄一下我的踩坑記錄。
正文
坑點(diǎn)1——文字不對(duì)齊
需求描述
如圖所示

很簡單對(duì)吧,在之前Android開發(fā)上,左邊是一個(gè)TextView,右邊是一個(gè)EditTextView,在父布局ConstraintLayout上,通過baseLine的約束,設(shè)置即可。
在Flutter上,輸入的控件怎么選擇呢?
問題描述
屬性這些沒有啥好說的,官方文檔都說的很清楚。代碼如下所示:
@override
Widget build(BuildContext context) {
return new Container(
color: AppColors.GREY_LIGHT,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Row(
children: <Widget>[
// 左邊部分文字
new Container(
padding: EdgeInsets.all(16.0).copyWith(right: 0.0),
child: new Text(
"姓名",
style: const TextStyle(color: Colors.black, fontSize: 14.0),
),
),
// 右邊部分輸入,用Expanded限制子控件的大小
new Expanded(
child: new TextField(
controller: _controller,
// 焦點(diǎn)控制,類似于Android中View的Focus
focusNode: _focusNode,
style: const TextStyle(color: Colors.black, fontSize: 14.0),
decoration: InputDecoration(
hintText: "請(qǐng)輸入姓名",
// 去掉下劃線
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.0),
),
),
),
],
),
_buildDivider(context)
],
),
);
}
本以為,就輕松實(shí)現(xiàn)了,結(jié)果如下圖所示,可以明顯的看到輸入文字相對(duì)于左邊的文字有一個(gè)明顯的向下的偏移。

本以為是文字沒有居中,在沒有輸入的時(shí)候看到

光標(biāo)、提示文字和左邊的文字是對(duì)齊的,所以排除居中問題。
然而,當(dāng)我輸入英文時(shí)。。。

居然神奇的對(duì)齊了。。。

但如果繼續(xù)輸入中文,會(huì)發(fā)現(xiàn)輸入部分所有的文字都會(huì)整體下移。
解決問題
順著源碼去簡單看了看繪制部分
void _paintCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
final Paint paint = Paint()
..color = _cursorColor;
final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset);
if (cursorRadius == null) {
canvas.drawRect(caretRect, paint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, cursorRadius);
canvas.drawRRect(caretRRect, paint);
}
if (caretRect != _lastCaretRect) {
_lastCaretRect = caretRect;
if (onCaretChanged != null)
onCaretChanged(caretRect);
}
}
可能是這個(gè)offset的問題?定位到offset
Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal;
Offset get _paintOffset {
switch (_viewportAxis) {
case Axis.horizontal:
return Offset(-offset.pixels, 0.0);
case Axis.vertical:
return Offset(0.0, -offset.pixels);
}
return null;
}
因?yàn)槲覍懙闹挥幸恍校詧?zhí)行的Axis.horizontal分支的代碼,顯然,沒有任何垂直方向的偏移。所以這個(gè)方向不對(duì)。
說到文字水平對(duì)齊,熟悉Android開發(fā)的同學(xué)都知道,在畫文字時(shí),水平對(duì)齊往往是baseline控制的,所以想著也可能是baseline的問題。shift-shift快捷鍵搜索baseline,看有沒有相關(guān)的信息。

進(jìn)入TextBaseline
/// A horizontal line used for aligning text.
enum TextBaseline {
/// The horizontal line used to align the bottom of glyphs for alphabetic characters.
alphabetic,
/// The horizontal line used to align ideographic characters.
ideographic,
}
這是一個(gè)枚舉類。。。看到這里大概就明白了,上面那個(gè)是拉丁文系的baseline對(duì)齊,下面是象形文字的對(duì)齊,我之前的寫法并不能保證Text和TextField用的同一個(gè)baseline,需要指明。
修改代碼后
style: const TextStyle(
color: Colors.black,
fontSize: 14.0,
textBaseline: TextBaseline.ideographic,
)
這里我們暫時(shí)去掉間距
decoration: InputDecoration(
hintText: "請(qǐng)輸入姓名",
// 去掉下劃線
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.0).copyWith(left: 0.0),
)

終于可以對(duì)齊了!
后續(xù)
為什么我都用的同一個(gè)TextStyle構(gòu)造,它們的baseline會(huì)不同?這里先留個(gè)坑,以后回來再補(bǔ)。。。
坑點(diǎn)2——Form.of(context)為空
問題解決
話不多說,直接上代碼,這是一個(gè)登陸頁面
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("登陸"),
centerTitle: true,
),
body: new DefaultTextStyle(
style: AppFonts.NORMAL_BLACK,
child: new Form(
autovalidate: _autoValidate,
onChanged: () {
if (!_autoValidate) {
setState(
() {
_autoValidate = true;
},
);
}
},
child: _buildContent(context),
),
),
);
}
Widget _buildContent(BuildContext context) {
return new Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Container(
margin: const EdgeInsets.all(16.0),
child: new TextFormField(
autovalidate: _autoValidate,
focusNode: _phoneFocus,
keyboardType: TextInputType.phone,
validator: (password) {
String error;
if (password.length <= 0) {
return "請(qǐng)輸入手機(jī)號(hào)";
}
return error;
},
decoration: InputDecoration(
labelText: "手機(jī)號(hào)",
icon: new Icon(Icons.phone_android),
),
),
),
new Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: new TextFormField(
textInputAction: TextInputAction.next,
autovalidate: _autoValidate,
focusNode: _passwordFocus,
validator: (password) {
if (password.length <= 0) {
return "請(qǐng)輸入密碼";
}
return null;
},
onSaved: (text) {
// TODO: 處理登陸
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) {
return new HomePage();
}),
);
},
obscureText: _obscureText,
decoration: InputDecoration(
labelText: "密碼",
icon: new Icon(Icons.lock),
suffixIcon: new IconButton(
icon: new Icon(Icons.remove_red_eye),
onPressed: () {
setState(
() {
_obscureText = !_obscureText;
},
);
}),
),
),
),
new Container(
margin: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 16.0)
.copyWith(top: 32.0),
child: new NormalButton(
onPressed: () {
final form = Form.of(context);
assert(form != null);
if (form.validate()) {
_phoneFocus.unfocus();
_passwordFocus.unfocus();
form.save();
}
},
text: "登陸",
),
),
],
);
}
為什么用Form.of(context)?

獲取到FormState文檔上說了有兩種方法,一種是GlobalKey,一種是Form.of(context),GlobalKey的生產(chǎn)代價(jià)比較大,所以選用后者。后者,但在運(yùn)行時(shí),始終獲取不到FormState,為什么呢?——read the fucking source。
進(jìn)入這個(gè)方法,看到它是通過查找父親節(jié)點(diǎn)來獲取Form的

在獲取到State的地方打上斷點(diǎn),調(diào)試
在FormFieldState build處中斷

進(jìn)去看一下,原來FormField在build時(shí)會(huì)在Form中注冊(cè),加入Form保存Field的一個(gè)set里這也是為什么把FormFiled及其子類放在Form的child中任何Widget里仍然可以通過FormState控制的原因。



說遠(yuǎn)了...回到正題。在build時(shí)可以通過context找到父節(jié)點(diǎn),而且,從上面的斷點(diǎn)的變量看出來,widget是這個(gè)context的成員,這跟我以前理解的context確實(shí)不一樣,所以就要定位到BuildContext源碼去。
于是乎。。。好像打開了新世界的大門。官方文檔中有這樣一段:

大概就是每一個(gè)Widget都有自己的BuildContext(原來這貨不是全局統(tǒng)一的?。?,如果沒注意這個(gè)問題可能會(huì)發(fā)生意想不到的后果。。??梢院唵蔚尿?yàn)證一下。


可以看到,這里傳入Form.of(context)的context是LoginPage的context,LoginPage,而Form是在LoginPage里build的,顯然,通過LoginPage向上查找是找不到的。。。
所以,有一下兩個(gè)解決方案:
- 使用GlobalKey(真香)
- 把需要獲取Form的控件做成一個(gè)Widget類
后續(xù)
傳入的context到底是什么?
以StatelessWidget為例,查看build(context)方法的引用

從這里我們就知道,原來對(duì)于StatelessWidget,context就是一個(gè)StatelessElement,StatelessElement是BuildContext的一個(gè)實(shí)現(xiàn)類

所以說,以后如果需要傳入context向上查找,要謹(jǐn)慎使用。