輸入——Form和TextFormField

前言

最近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上,輸入的控件怎么選擇呢?源碼上的文檔已經(jīng)說的很清楚了。我們打開瀏覽器,Google “Flutter input”,就知道用啥了嘛對(duì)吧?這里就不對(duì)控件基礎(chǔ)介紹了,官方文檔都寫的清清楚楚的~

問題描述

屬性這些沒有啥好說的,官方文檔都說的很清楚。代碼如下所示:

 @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ì)整體下移。所以......這就是Flutter的bug了吧?

解決問題

順著源碼去簡單看了看繪制部分

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)慎使用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 特別說明 當(dāng)前博客平臺(tái)賬號(hào)已廢棄,如果有使用細(xì)節(jié)問題請(qǐng)前往我新博客平臺(tái)進(jìn)行討論交流。 個(gè)人博客平臺(tái) HuRuWo的...
    善篤有余劫閱讀 5,105評(píng)論 0 30
  • 一、前言 從 2015 年接觸 Flutter 到現(xiàn)在也有兩年多時(shí)間,在這期間我并沒有正真地去了解這個(gè)神奇的框架,...
    _番茄沙司閱讀 35,355評(píng)論 21 48
  • 本文參加#未完待續(xù),就要表白?;顒?dòng),本人承諾,文章內(nèi)容為原創(chuàng),且未在其他平臺(tái)發(fā)表過。 背起書包,踏上火車,我來到了...
    萌萌噠小超超閱讀 248評(píng)論 0 4
  • 青春中總會(huì)有那么一段難忘的初戀,或許對(duì)于我來說,用單戀會(huì)更加合適。 青少年時(shí)期受言情小說荼毒,經(jīng)?;孟胱约旱膼矍橛?..
    new_rammer閱讀 336評(píng)論 2 1
  • 文/今成將樂 想了很久都沒想好這篇名為“自卑的殺傷力到底有多可怕”的文章要怎么開篇。可就在剛剛,當(dāng)我輸入自卑二字...
    今成將樂閱讀 331評(píng)論 2 4

友情鏈接更多精彩內(nèi)容