簡單項目實戰flutter(佈局篇)

這是一個在擼完兩個官方Demo以後,爲了實踐操做重寫了原來app的項目。 雖然這個app基本上只有一個頁面,算不上覆雜但能夠說內容豐富,涉及到的經常使用功能也很多,在花了三天假期的兩天擼完大部份內容以後,感受仍是學到了很多知識點,在此作一些總結概括,避免過幾天忘記了。java

項目地址:friday_today, 由於總體還沒徹底完成,主體代碼就沒有分幾個文件,都放在main.dart ,一共800多行android

這裏是原來的原生代碼,所有用kotlin寫的,也是基本上都在一個Activity裏面:FridayActivity和佈局文件activity_friday.xmlgit

再放兩張截圖對比,上面是flutter版本,下面是原生Android版本:github

flutter版本
Android版本

整體來講Flutter的佈局方式是和ReactNative相似的,State來控制狀態,以及Flex佈局之類。 flutter裏面的描述控件的是widget,描述佈局的也是widget,因而不得不面臨重重嵌套,寫佈局時十分懷念方便的ConstraintLayout,不過寫多了以後發現這種佈局方式有一個好處:就是很是容易寫一個方法返回一個通用widget,而後只須要調用方法就能夠重複構建相似的佈局,雖然在a ndroid能夠用include標籤(只能重用佈局),也能夠用自定義佈局(須要新建類比較麻煩),但從使用上都不如flutter這樣直接用一個方法封裝來的方便快捷。markdown

接下來從根佈局捋一捋用到的widget和一些踩到的坑:app

根佈局:Stack,Position和AspectRatio

app的界面主要分爲兩部分,用於展現的界面(包括背景和文字),和用於控制的界面(即圖上的黑色半透明有一堆按鈕的部分),其中展現部分有佔全屏幕和縮減爲正方形兩種模式,所以不論如何這兩個部分確定是重疊的,我用兩個方法分別構建這兩部分的組件,而後使用Stack來放置這兩個部分(最外層這個bodyScaffold的內容):async

body: Stack(
 alignment: Alignment.bottomCenter,
  children: <Widget>[
  // 展現部分須要總體居中
    Center(
    // 套的這層RepaintBoundary是用來截屏的,後面再說
      child: RepaintBoundary(
        key: screenKey,
        child: screenType == 1
	        // 寬高一比一的狀況
            ? AspectRatio(
                aspectRatio: 1 / 1,
                child: Container(
                  color: bgColor,
                  child: _buildShowContent(),
                ),
              )
             // 佔全屏幕的狀況
            : Container(
                color: bgColor,
                child: _buildShowContent(),
              ),
      ),
    ),
    _buildControlPanel(),
  ],
));
複製代碼

最初我覺得在子佈局中設置alignment爲botton就能夠把controlPanel部分固定在底部,但實際並無效果,這塊內容飛到了頂上,半透明背景也沒有出現,推測是controlPanel的內容佔滿了屏幕高度,就算是固定在底部也看不出來。因此解決方式是在controlPanel中給Column添加縱軸上的最大值mainAxisSize: MainAxisSize.min(參見_buildControlPanel()方法的代碼)。在最初瞎幾把亂試的時候誤打誤撞發現了一個能夠達成相同效果的方式,就是使用Pisitioned來固定。ide

// 使用Positioned把這部分固定在底部,而後left和right爲0使佈局撐開達到寬度match_parent的效果
Positioned(
  bottom: 0,
  left: 0,
  right: 0,
  child: _buildControlPanel(),
)
複製代碼

AspectRatio的做用是使子佈局寬高限制爲固定寬高比,屬性的做用都很顯而易見,就不贅述了。佈局

文字展現部分,Column,BoxDecoration

展現的部分佈局很簡單,就是從上到下襬上幾個文字,用Column就能夠實現,給Column設置在主軸上居中。flutter不像android全部View均可以設置marginpadding,而是要在外面套上Container,而後給Container設置設置各類屬性來修飾。 BoxDecoration則能夠很方便的給Container的子元素設置背景色,圓角,邊框,陰影等效果,讓我以爲比較實用的就是對於Button類有一個現成的半圓圓角方法,在android裏實現通常須要肯定高度,否則就只能等渲染完了再按照高度的一半設置圓角。post

/// 繪製中間顯示的部分
_buildShowContent() {
  return Column(
        mainAxisAlignment: MainAxisAlignment.center, // 子佈局在橫軸上居中
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(bottom: 20.0),
// padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20),
            height: 60.0,
            // 不是按鈕沒有現成的半圓方法,設置固定高度再加圓角
            decoration: BoxDecoration(
              color: bubbleColor, // 設置氣泡(文字的背景)顏色
              borderRadius: BorderRadius.circular(30.0),
            ),
            child: Center(// 使文字總體居中
              widthFactor: 1.3, // 寬度是文字寬度的1.3倍
              child: Text(
                langType == 0 ? "今天是週五嗎?" : "Is today Friday?",
                style: TextStyle(
                    fontSize: 25, color: textColor, fontFamily: fontName),
              ),
            ),
          ),
          Text(
            today.weekday == 5
                ? langType == 1 ? "YES!" : "是"
                : langType == 1 ? "NO" : "不是",
            style:
                TextStyle(fontSize: 90, color: textColor, fontFamily: fontName),
          ),
          Container(
            margin: EdgeInsets.only(top: 20.0),
            child: Text(
              "${weekdayToString(today.weekday)} ${today.year}.${today.month}.${today.day}",
              textAlign: TextAlign.center,
              style: TextStyle(color: textColor, fontFamily: fontName),
            ),
          ),
        ],
      );
}
複製代碼

第一行文字有一個白色圓角背景做爲氣泡,文字要在氣泡中居中,在android裏我使用gravity=center,而後設置好padding,最後根據渲染完以後文字的整個高度來設置圓角,但在這裏我固定了背景高度,因爲沒法知道文字的確切高度就不能用設置padding 的方法使文字居中了,解決方案是使用了一個Center包裹Text,寬度則由widthFactor設置爲文字寬度的倍數,flutter裏面用倍數設置寬度的操做讓我以爲很奇怪。

控制面板

控制面板由各類按鈕組成,總體是一個Column從上到下一共7行,各行用Row來排布按鈕,是比較規律的排列方式,由於按鈕有不少類似性,此處用了好幾個方法來封裝不一樣的按鈕:

/// 繪製整個控制面板
_buildControlPanel() {
  return Container(
    padding: EdgeInsets.all(12.0),
    color: Color.fromARGB(30, 0, 0, 0), // 半透明的黑色背景
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start, // 子佈局在橫軸上左對齊
      mainAxisSize: MainAxisSize.min, // 高度保持最小高度以固定在底部
      children: <Widget>[
        // 前三行分別是背景,氣泡和文字的顏色控制,用一個方法封裝
        _buildColorController(0),
        _buildColorController(1),
        _buildColorController(2),
        // 展現可選擇的字體
        Container(
          height: 30.0,
          margin: EdgeInsets.only(bottom: 8.0),
          // 中文字體只有4個可是英文有11個,須要滾動,因此使用ListView而不是Row
          child: new ListView(
            padding: EdgeInsets.all(0.0),
            scrollDirection: Axis.horizontal, // 設置橫向滾動
            children: _buildFontRow(langType), // 根據字體種類生成切換字體的按鈕組
          ),
        ),
        Container(
          margin: EdgeInsets.only(bottom: 8.0),
          child: Row(
            // 一行四個按鈕,用通用的方法生成,點擊事件也從外部傳入
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).square,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => setState(() {
                        screenType = 1;
                      })),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).full,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => setState(() {
                        screenType = 0;
                      })),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleCn,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => {_changeLangType(0)}),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleEn,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => {_changeLangType(1)}),
            ],
          ),
        ),
        // 四個功能按鈕是2X2,用兩個row
        Container(
          margin: EdgeInsets.only(bottom: 8.0),
          child: Row(
            children: <Widget>[
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).wallpaper,
                    style: TextStyle(
                        color: FridayColors.jikeWhite, fontSize: 14.0),
                  ),
                  40.0,
                  () => {_capturePng(1)}),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleShare,
                    style: TextStyle(
                      color: FridayColors.jikeWhite,
                      fontSize: 14.0,
                    ),
                  ),
                  40.0,
                  () => {_capturePng(2)}),
            ],
          ),
        ),
        Container(
          margin: EdgeInsets.only(bottom: 8.0),
          child: Row(
            children: <Widget>[
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).group,
                    style: TextStyle(
                        color: FridayColors.jikeWhite, fontSize: 14.0),
                  ),
                  40.0,
                  _toJike),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleSave,
                    style: TextStyle(
                      color: FridayColors.jikeWhite,
                      fontSize: 14.0,
                    ),
                  ),
                  40.0,
                  () => {_capturePng(0)}),
            ],
          ),
        )
      ],
    ),
  );
}
複製代碼

行:Expanded,Button

接下來繪製頭三行,由於是同樣的格式,封裝在下面這個方法裏:

/// 繪製切換顏色的三行 [type]背景/氣泡/字體
_buildColorController(int type) {
  return Container(
    margin: EdgeInsets.only(bottom: 8.0),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.start,  // 主軸上從左到右
      mainAxisSize: MainAxisSize.max, // 寬度佔滿
      children: <Widget>[
        Text(
          getTitleByType(type, context), // 標題由type獲取
          style: TextStyle(
            fontSize: 12.0,
          ),
        ),
        // 生成顏色按鈕
        _buildColorClickDot(type, FridayColors.jikeWhite),
        _buildColorClickDot(type, FridayColors.jikeYellow),
        _buildColorClickDot(type, FridayColors.jikeBlue),
        _buildColorClickDot(type, FridayColors.jikeBlack),
        // Expanded佔滿剩餘寬度
        Expanded(
          child: Container(
            height: 30.0,
            padding: EdgeInsets.all(2.0), // 這個padding用於擠壓縮小按鈕自己
            margin: EdgeInsets.only(left: 8.0),
            child: RaisedButton(// 有凸起陰影效果的按鈕
              child: Text(
                FridayLocalizations.of(context).moreColor,
                style: TextStyle(fontSize: 12.0, color: Colors.white),
              ),
              onPressed: () => {_showPickColorDialog(type)},
              color: Colors.black26,
              shape: StadiumBorder(), // 半圓形背景
            ),
          ),
        ),
        Expanded(
          child: Container(
            height: 30.0,
            padding: EdgeInsets.all(2.0),
            margin: EdgeInsets.only(left: 8.0),
            child: RaisedButton(
              padding: EdgeInsets.all(0.0),
              child: Text(
                FridayLocalizations.of(context).customColor,
                style: TextStyle(fontSize: 12.0, color: Colors.white),
              ),
              onPressed: () => {_showCustomColorDialog(type)},
              color: Colors.black26,
              shape: StadiumBorder(),
            ),
          ),
        ),
      ],
    ),
  );
}
複製代碼

最主要用到的空間就是RaisedButton,是有凸起陰影效果的按鈕,若是要沒有立體效果的可使用FlatButtonshape: StadiumBorder()能夠方便地使按鈕有半圓形效果,這裏順便放出三個顏色按鈕的生成方法:

/// 繪製點擊切換顏色的小圓點
_buildColorClickDot(int type, Color color) {
  return Container(
    margin: EdgeInsets.only(left: 8.0),
    child: Container(
      // 限制按鈕的寬度
      width: 20.0,
      height: 20.0,
      child: RaisedButton(
// onPressed: _changeColor(type, color), // 這樣寫顏色出不來
        onPressed: () => {_changeColor(type, color)},
        color: color,
        // 設置爲圓形按鈕,若是不添加這個shape就是20*20的正方形
        // 由於寬高同樣,半圓和圓的結果是同樣的,因此用shape: StadiumBorder()也ok
        shape: CircleBorder(
            side: BorderSide(
          color: Colors.transparent,
          width: 0,
        )),
      ),
    ),
  );
}
複製代碼

一開始我想像原生那樣用一個有顏色的View設置一個點擊事件就完事了,可是Flutter裏面不是啥均可以點擊的,若是不是Button類的widget,須要套一個GestureDetectorwidget來添加點擊事件(後面會用到),因此仍是直接用了Button,立體效果看起來感受比原來好一點。使用Button的時候起初感受很麻煩的就是它有一些自帶的padding等等,致使尺寸很很差控制,最後發現只要在在外面套上Container設置寬高就能夠了,智障如我。

一行最後兩個文字按鈕,原本也是總有padding致使文字放不下,甚至按鈕自己超出屏幕,最後設置了Expanded來使它們的寬度自適應,可是這樣在小屏上文字可能真的放不下,因而把文字減了字數→_→

其餘行也都是相似的結構,就不重複提了。

彈出部分:AlertDialog

選更多顏色和自定義顏色時都會彈出dialog,flutter有現成的Dialog類型控件,通常使用SimpleDialog顯示一個多行選項列表,AlertDialog顯示自定義的內容,並在最下面有幾個按鈕。 flutter有一個自帶的showDialog方法,接收一些參數,並使用一個builder來生成Dialog

/// 顯示自定義顏色的dialog
Future<void> _showCustomColorDialog(int type) async {
  return showDialog<void>(
    context: context,
    barrierDismissible: false, // user must tap button!
    builder: (BuildContext context) {
      return AlertDialog(
        contentPadding: EdgeInsets.all(16.0),
        title: Text(getCustomColorTitleByType(type, context)),
        content: ..., // 內容widget
        actions: <Widget>[
          FlatButton(
            child: Text('OK'),
            onPressed: () {
              _handleSubmitted(type, _inputController.text);
              Navigator.of(context).pop();
            },
          ),
        ],
      );
    },
  );
}
複製代碼

如下是dialogcontent部份內容:

可滑動Widget:GridView,SingleChildScrollView,Wrap

其中一個Dialog須要展現500多個顏色列表,最初我使用了一個可滑動的SingleChildScrollViewcontent,其中放了500多個按鈕,用的是從左往右一行一行的排列,用Wrap比ListView更合適

content: SingleChildScrollView(// 可滑動
  child: Center( // 設置居中避免兩側空白不對稱
    child: Wrap(
      spacing: 5.0,
      runSpacing: 5.0,
      children: getColorRows(type),
    ),
  ),
),
複製代碼

可是在我看了一下SingleChildScrollView的源碼以後,裏面推薦了一堆別的控件,Flutter提供的控件太多了使人困惑,通過幾回嘗試,最終發現使用GridView.count能夠完美實現,用法和Wrap很類似:

content: GridView.count(
  crossAxisCount: 8, // 一行的按鈕個數
  crossAxisSpacing: 5.0, //列間距
  mainAxisSpacing: 5.0, // 行間距
  children: getColorRows(type),
),
複製代碼

輸入與提示:TextField,Snackbar

另外一個Dialog則彈出文本輸入框供輸入六位或者八位顏色值,這裏涉及到文本輸入與控制,在官方Demo中有現成的例子,因此就直接拿來用了:

final TextEditingController _inputController = new TextEditingController();

...
content: TextField(
  controller: _inputController,
  decoration: InputDecoration(
      hintText: FridayLocalizations.of(context).hintInputColor,
      hintStyle: TextStyle(fontSize: 12.0)),
),
actions: <Widget>[
  FlatButton(
    child: Text('OK'),
    onPressed: () {
      _handleSubmitted(type, _inputController.text); // 點擊ok時提交
      Navigator.of(context).pop(); // dialog隱藏
    },
  ),
],
...

void _handleSubmitted(int type, String text) {
 _inputController.clear(); // 清除輸入的文字
  if (text.length != 8 && text.length != 6) {
    // 輸入的格式不正確,彈出提示
    (scaffoldKey.currentState as ScaffoldState).showSnackBar(new SnackBar(
      content: new Text(FridayLocalizations.of(context).noticeWrongInput),
    ));
  } else {
    // 設置顏色
    _changeColor(
        type, Color(int.parse(text.length == 8 ? "0x$text" : "0xFF$text")));
  }
}
複製代碼

這裏用到了一個showSnackBar來顯示SnackBar,有一個小坑,我先開始用網上搜索到的Scaffold.of(context).showSnackBar(snackBar);來顯示,結果一直報Scaffold.of() called with a context that does not contain a Scaffold.這個錯,因爲對flutter的context不夠了解,在查閱了一些資料後大體知道要把什麼widget拆出來,這樣就能經過context找到了(參考這篇文章),可是我並不想爲了顯示一個snackBar這麼作,最後找到了另外一個解決辦法,Scaffold.of(context)是爲了獲取一個ScaffoldState,因此能夠在Scaffold上添加一個key,再用這個key拿到state來調用方法:

GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();

...
@override
  Widget build(BuildContext context) {
    return Scaffold(
        key: scaffoldKey,
        body: ...
        );
}
...

...
scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: new Text(FridayLocalizations.of(context).noticeWrongInput),
    ));
...
複製代碼

整個頁面的大體內容就是如此,本來我打算頁面整出來就完事了又不是不能用.jpg, 但在寫這篇總結的時候也爲了探索是否是有別的實現方式作了一些嘗試,最終也進行了一些優化。 其實做爲一個習慣了原生的Android開發者,不少時候都會困惑於原生很容易實現的效果放到Flutter中使用widget要怎麼寫,好比如何實現WRAP_COTENTMATCH_PARENT(能夠參考這篇文章)等等,正是由於如此,我認爲須要寫更多的佈局才能逐漸習慣Flutter的代碼風格。 下一篇文章(若是有的話)會描述這個app的功能部分,包括截屏,保存圖片,分享,跳轉其餘app,SharedPreference保存數據,調用原生方法,多語言等等。

下篇傳送門:簡單項目實戰flutter(功能篇)

相關文章
相關標籤/搜索