網傳微信支付頁面的第三方連結一個格子需要廣告費1一個億,微信支付頁非常適合做功能導航,本篇使用 ListView和 GridView 模仿了微信支付的頁面,同時介紹瞭如何裝飾一個元件的背景和邊緣樣式。
左側是微信支付的介面,右側是開發完成後的效果,圖示是從 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 的滾動,從而滿足約束。
程式碼實現
- 首先來分析佈局,所有選單按鈕其實都是一樣的佈局,可以使用統一的列布局完成選單按鈕,提高複用性。選單按鈕從上到下一次為圖示、間距(圖示與文字之間)和選單名稱。實現程式碼如下:
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)),
],
);
複製程式碼
通過傳輸一個圖示名稱,選單名稱和可選的字型顏色(頂部區域和其他的文字顏色不同)來實現單個選單。
- 其次來看頂部區域,頂部區域只有兩個按鈕,使用帶裝飾的容器實現背景的裝飾和圓角。再採用行佈局將兩個選單按鈕在橫向均勻排布。同時,使用 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()),
),
);
}
複製程式碼
- 其他選單佈局都是一樣,只是區域標題,選單數量、選單內容不同,因此可以統一封裝一個通用的方法來構建任意形式的選單,以及設定區域標題的字型樣式、圓角背景等屬性。選單均使用 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(),
);
}
}
複製程式碼
- ListView 構建完整頁面:實際的整個頁面很簡單,只需要將各個區域放入到 ListView 的 children 屬性即可,從這裡也可以看出,將子元件儘可能細化,不但能夠提高程式碼複用性,還可以降低巢狀層級,提高程式碼的可讀性和可維護性。
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
_headerGridButtons(),
_dynamicGridButtons(GridMockData.financeGrids(), '金融理財'),
_dynamicGridButtons(GridMockData.serviceGrids(), '生活服務'),
_dynamicGridButtons(GridMockData.thirdpartyGrids(), '購物消費'),
],
),
);
}
複製程式碼
- 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'},
];
}
複製程式碼
- 其他待改進的地方:從程式碼中可以看出,訪問按鈕的時候是使用 Map 物件的鍵來訪問的,需要使用['name']或['icon']來訪問,這種方式非常不利於編碼,而且很容易拼寫錯誤。因此,實際使用中應當將 Json 物件(即 Map)轉換為實體類,這樣就可以通過訪問實體類的屬性來設定選單的引數,實際維護起來更為方便。
結語:Flutter 提供的基礎 UI 元件庫能夠滿足絕大部分複雜頁面佈局,通過各種佈局元件的組合即可完成。因此,熟悉基礎的佈局元件的特性十分重要。同時,需要注意元件的拆分和抽離完成子元件的封裝,提高複用性的同時也避免了巢狀層級過深的問題。