Flutter 入門與實戰(八):仿一個微信價值幾個億的頁面

島上碼農發表於2021-05-29

網傳微信支付頁面的第三方連結一個格子需要廣告費1一個億,微信支付頁非常適合做功能導航,本篇使用 ListView和 GridView 模仿了微信支付的頁面,同時介紹瞭如何裝飾一個元件的背景和邊緣樣式。

支付.png 左側是微信支付的介面,右側是開發完成後的效果,圖示是從 iconfont 上下載的。首先介紹一下本篇涉及到的元件。

帶裝飾效果的 Container

實際過程中我們經常會遇到一個容器需要額外的樣式,例如圓角,背景色等。在 Flutter 中,對於各種容器都有一個 decoration 的屬性,可以用於裝飾容器。典型的用法有設定背景色、圓角、邊框和陰影等,其中背景色可以使用漸變色。decoration 是一個 Decoration 物件,最常用的是 BoxDecoration,BoxDecoration 的屬性如下所示:

const BoxDecoration({
    this.color,
    this.image,
    this.border,
    this.borderRadius,
    this.boxShadow,
    this.gradient,
    this.backgroundBlendMode,
    this.shape = BoxShape.rectangle,
  }) 
複製程式碼

其中color為使用顏色填充容器,image 為 使用圖片作為背景,border 為邊框,borderRadius 為邊框圓角,boxShadow 為容器陰影,gradient 使用漸變色作為背景,backgroundBlendMode 是指與容器的混合模型,預設是覆蓋,shape 是背景形狀,預設是矩形。其中背景部分我們一般只會選擇一種。這裡以上面的綠色圓弧背景為例,還加上了一點點漸變(漸變色支援多個,可以根據需要調節),示例程式碼如下:

return Container(
      //......
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(4.0),
        gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Color(0xFF56AF6D),
              Color(0xFF56AA6D),
            ]),
      ),
  		//...
    );
複製程式碼

這裡設定了邊角為圓弧,半徑為4,使用漸變色填充,漸變方向為從頂部中央到底部中央,漸變色有兩個。

Row 行佈局和 Column列布局

這個在之前的第五篇列表篇介紹過,其中 Row 代表行佈局(即子元素按一行排布),Column 代表列布局(即子元素按一列排布)。具體可以參考Flutter 入門與實戰(五):來一個圖文並茂的列表

ListView列表元件

列表檢視,和之前的一篇一樣,只是本篇的用法不同,用於實現整個頁面可以按列表的方式進行滾動,直接將各個部分元件放入到列表的 children屬性中,而不是使用陣列構建列表元素,有點類似滾動檢視的用法。

GridView網格元件

GridView 用於將一個容器按行列劃分,可以指定主軸的元素個數(根據滾動方向定),之後自動按總元素的個數分別填充到網格,例如按縱向滾動時,則可以指定行方向一行有多少個網格,每個網格一個元素。超出一行數量後會自動換另一行。最簡單的用法是使用 GridView.count 方法構建 GridView,用法如下:

GridView.count(
   crossAxisSpacing: gridSpace,
   mainAxisSpacing: gridSpace,
   crossAxisCount: crossAxisCount,
   //設定以下兩個引數,禁止GridView的滾動,防止與 ListView 衝突
   shrinkWrap: true,
   physics: NeverScrollableScrollPhysics(),
   children: buttons.map((item) {
      return _getMenus(item['icon'], item['name'], color: textColor);
    }).toList(),
);
複製程式碼

這裡 crossAxisSpacing 是與滾動方向垂直的元素的間距,如按縱向(預設值)滾動,則是橫向行元素之間的間距。mainAxisSpacing 是與滾動方向相同的元素的間距。children 即網格中的元素。這裡需要注意的是,由於 本例中GridView是巢狀在 ListView 裡面的,兩個元件都是縱向滾動,這樣會引起衝突導致佈局無法滿足約束。因此,在這裡設定了 shrinkWrap 為 true 和 physics 為NeverScrollableScrollPhysics,以禁止 GridView 的滾動,從而滿足約束。

程式碼實現

  1. 首先來分析佈局,所有選單按鈕其實都是一樣的佈局,可以使用統一的列布局完成選單按鈕,提高複用性。選單按鈕從上到下一次為圖示、間距(圖示與文字之間)和選單名稱。實現程式碼如下:
Column _getMenus(String icon, String name, {Color color = Colors.black}) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      SizedBox(
        child: Image.asset(icon),
        width: 50,
        height: 50,
      ),
      SizedBox(
        height: 5,
      ),
      Text(name, style: TextStyle(fontSize: 14.0, color: color, height: 2)),
    ],
  );
複製程式碼

通過傳輸一個圖示名稱,選單名稱和可選的字型顏色(頂部區域和其他的文字顏色不同)來實現單個選單。

  1. 其次來看頂部區域,頂部區域只有兩個按鈕,使用帶裝飾的容器實現背景的裝飾和圓角。再採用行佈局將兩個選單按鈕在橫向均勻排布。同時,使用 Center 佈局將兩個選單保持中部居中。這裡指定了容器的高度,這是因為從美觀上看太矮了不太協調,實際開發要根據 UI 設計稿定。
Widget _headerGridButtons() {
    double height = 144;
    List<Map<String, String>> buttons = GridMockData.headerGrids();
    return Container(
      height: height,
      margin: EdgeInsets.fromLTRB(MARGIN, MARGIN, MARGIN, MARGIN / 2),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(4.0),
        gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Color(0xFF56AF6D),
              Color(0xFF56AA6D),
            ]),
      ),
      child: Center(
        child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: buttons
                .map((item) =>
                    _getMenus(item['icon'], item['name'], color: Colors.white))
                .toList()),
      ),
    );
  }
複製程式碼
  1. 其他選單佈局都是一樣,只是區域標題,選單數量、選單內容不同,因此可以統一封裝一個通用的方法來構建任意形式的選單,以及設定區域標題的字型樣式、圓角背景等屬性。選單均使用 GridView 實現網格式佈局,同時由於選單佈局相同,可以封裝一個通用的方法來指定網格一行按鈕的數量,按鈕字型顏色等屬性,實現程式碼的複用。
Widget _dynamicGridButtons(List<Map<String, String>> buttons, String title,
      {int crossAxisCount = 4}) {
    return Container(
      margin: EdgeInsets.fromLTRB(MARGIN, MARGIN, MARGIN, MARGIN / 2),
      padding: EdgeInsets.all(MARGIN),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(4.0),
        color: Colors.white,
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: TextStyle(color: Colors.grey[700]),
          ),
          SizedBox(height: 20),
          _gridButtons(buttons, crossAxisCount, textColor: Colors.black),
        ],
      ),
    );
  }

GridView _gridButtons(List<Map<String, String>> buttons, int crossAxisCount,
      {Color textColor = Colors.white}) {
    double gridSpace = 5.0;
    return GridView.count(
      crossAxisSpacing: gridSpace,
      mainAxisSpacing: gridSpace,
      crossAxisCount: crossAxisCount,
      //設定以下兩個引數,禁止GridView的滾動,防止與 ListView 衝突
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(),
      children: buttons.map((item) {
        return _getMenus(item['icon'], item['name'], color: textColor);
      }).toList(),
    );
  }
}
複製程式碼
  1. ListView 構建完整頁面:實際的整個頁面很簡單,只需要將各個區域放入到 ListView 的 children 屬性即可,從這裡也可以看出,將子元件儘可能細化,不但能夠提高程式碼複用性,還可以降低巢狀層級,提高程式碼的可讀性和可維護性。
@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: [
          _headerGridButtons(),
          _dynamicGridButtons(GridMockData.financeGrids(), '金融理財'),
          _dynamicGridButtons(GridMockData.serviceGrids(), '生活服務'),
          _dynamicGridButtons(GridMockData.thirdpartyGrids(), '購物消費'),
        ],
      ),
    );
  }
複製程式碼
  1. Mock 資料準備

按鈕資料均使用 Mock 資料,這裡只是返回一個 List<Map<String, String>>陣列物件,物件裡是每個選單的圖示檔名稱和選單名稱,下面是金融服務區域的選單 Mock方法。

static List<Map<String, String>> financeGrids() {
    return [
      {'name': '信用卡還款', 'icon': 'images/grid-buttons/grid-1-1.png'},
      {'name': '借錢', 'icon': 'images/grid-buttons/grid-1-2.png'},
      {'name': '理財', 'icon': 'images/grid-buttons/grid-1-3.png'},
      {'name': '保險', 'icon': 'images/grid-buttons/grid-1-4.png'},
    ];
  }
複製程式碼
  1. 其他待改進的地方:從程式碼中可以看出,訪問按鈕的時候是使用 Map 物件的鍵來訪問的,需要使用['name']或['icon']來訪問,這種方式非常不利於編碼,而且很容易拼寫錯誤。因此,實際使用中應當將 Json 物件(即 Map)轉換為實體類,這樣就可以通過訪問實體類的屬性來設定選單的引數,實際維護起來更為方便。

結語:Flutter 提供的基礎 UI 元件庫能夠滿足絕大部分複雜頁面佈局,通過各種佈局元件的組合即可完成。因此,熟悉基礎的佈局元件的特性十分重要。同時,需要注意元件的拆分和抽離完成子元件的封裝,提高複用性的同時也避免了巢狀層級過深的問題。

相關文章