從0開始寫一個基於Flutter的開源中國客戶端(6)——各個靜態頁面的實現

上一篇中我記錄了基於Flutter的開源中國客戶端的總體佈局框架的搭建,本篇記錄的是每一個頁面的靜態實現,關於具體的數據加載和存儲,放在下一篇中記錄,但願本身在溫故知新的同時,能給Flutter初學者一些幫助。android

索引 文章
1 從0開始寫一個基於Flutter的開源中國客戶端(1)
Flutter簡介及開發環境搭建 | 掘金技術徵文
2 從0開始寫一個基於Flutter的開源中國客戶端(2)
Dart語法基礎
3 從0開始寫一個基於Flutter的開源中國客戶端(3)
初識Flutter & 經常使用的Widgets
4 從0開始寫一個基於Flutter的開源中國客戶端(4)
Flutter佈局基礎
5 從0開始寫一個基於Flutter的開源中國客戶端(5)
App總體佈局框架搭建
👉6 從0開始寫一個基於Flutter的開源中國客戶端(6)
各個靜態頁面的實現
7 從0開始寫一個基於Flutter的開源中國客戶端(7)
App網絡請求和數據存儲
8 從0開始寫一個基於Flutter的開源中國客戶端(8)
插件的使用

在基於Flutter的開源中國客戶端中,使用得最多的就是ListView組件了,基本上80%的頁面都須要用列表展現,下面分別說明每一個頁面的實現過程。git

側滑菜單頁面的實現

上一篇中咱們僅僅在側滑菜單中放置了一個Center組件並顯示了一行文本,這一篇中須要實現的側滑菜單效果以下圖:github

側滑菜單的頭部是一個封面圖,下面是一個菜單列表,咱們能夠將封面圖和各個菜單都看成ListView的Item,因此這裏涉及到了ListView的子Item的多佈局。api

上一篇的代碼裏咱們是直接爲MaterialApp添加了一個drawer參數並new了一個Drawer對象,爲了合理組織代碼,這裏咱們在lib/目錄下新建一個widgets/目錄,用於存放咱們自定義的一些組件,並新建dart文件MyDrawer.dart,因爲該頁面不須要刷新,因此咱們在MyDrawer.dart中定義無狀態的組件MyDrawer,在該組件中定義須要用到的以下幾個變量:數組

class MyDrawer extends StatelessWidget {
  // 菜單文本前面的圖標大小
  static const double IMAGE_ICON_WIDTH = 30.0;
  // 菜單後面的箭頭的圖標大小
  static const double ARROW_ICON_WIDTH = 16.0;
  // 菜單後面的箭頭圖片
  var rightArrowIcon = new Image.asset(
    'images/ic_arrow_right.png',
    width: ARROW_ICON_WIDTH,
    height: ARROW_ICON_WIDTH,
  );
  // 菜單的文本
  List menuTitles = ['發佈動彈', '動彈小黑屋', '關於', '設置'];
  // 菜單文本前面的圖標
  List menuIcons = [
    './images/leftmenu/ic_fabu.png',
    './images/leftmenu/ic_xiaoheiwu.png',
    './images/leftmenu/ic_about.png',
    './images/leftmenu/ic_settings.png'
  ];
  // 菜單文本的樣式
  TextStyle menuStyle = new TextStyle(
    fontSize: 15.0,
  );
  // 省略後續代碼
  // ...
 }
複製代碼

MyDrawer類的build方法中,返回一個ListView組件便可:緩存

@override
  Widget build(BuildContext context) {
    return new ConstrainedBox(
      constraints: const BoxConstraints.expand(width: 304.0),
      child: new Material(
        elevation: 16.0,
        child: new Container(
          decoration: new BoxDecoration(
            color: const Color(0xFFFFFFFF),
          ),
          child: new ListView.builder(
            itemCount: menuTitles.length * 2 + 1,
            itemBuilder: renderRow,
          ),
        ),
      ),
    );
  }
複製代碼

build方法中的ConstraintedBox組件和Material組件都是直接參考的Drawer類的源碼,constraints參數指定了側滑菜單的寬度,elevation參數控制的是Drawer後面的陰影的大小,默認值就是16(因此這裏能夠不指定elevation參數),最主要的是ListView的命名構造方法build,itemCount參數表明item的個數,這裏之因此是menuTitles.length * 2 + 1,其中的*2是將分割線算入到item中了,+1則是把頂部的封面圖算入到item中了。下面是關鍵的renderRow方法:bash

Widget renderRow(BuildContext context, int index) {
    if (index == 0) {
      // render cover image
      var img = new Image.asset(
        'images/cover_img.jpg',
        width: 304.0,
        height: 304.0,
      );
      return new Container(
        width: 304.0,
        height: 304.0,
        margin: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 10.0),
        child: img,
      );
    }
    // 捨去以前的封面圖
    index -= 1;
    // 若是是奇數則渲染分割線
    if (index.isOdd) {
      return new Divider();
    }
    // 偶數,就除2取整,而後渲染菜單item
    index = index ~/ 2;
    // 菜單item組件
    var listItemContent = new Padding(
      // 設置item的外邊距
      padding: const EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0),
      // Row組件構成item的一行
      child: new Row(
        children: <Widget>[
          // 菜單item的圖標
          getIconImage(menuIcons[index]),
          // 菜單item的文本
          new Expanded(
            child: new Text(
              menuTitles[index],
              style: menuStyle,
            )
          ),
          rightArrowIcon
        ],
      ),
    );

    return new InkWell(
      child: listItemContent,
      onTap: () {
        print("click list item $index");
      },
    );
  }
複製代碼

renderRow方法體較長,主要是由於涉及到3個不一樣佈局的渲染:頭部封面圖、分割線、菜單item。以上代碼中已有相關注釋,其中有幾點須要注意:網絡

  1. 在渲染菜單item文本時用到了Expanded組件,該組件相似於在Android中佈局時添加android:layout_weight="1"屬性,上面使用Expanded包裹的Text組件在水平方向上會佔據除icon和箭頭圖標外的剩餘的全部空間;
  2. 最後返回了一個InkWell組件,用於給菜單item添加點擊事件,可是在Drawer中點擊菜單時並無水波紋擴散的效果(不知道是什麼緣由)。

資訊列表頁面的實現

本篇要實現的資訊列表頁面以下圖所示:app

資訊列表的頭部是一個輪播圖,能夠左右滑動切換不一樣的資訊,下面是一個列表,顯示了資訊的標題,發佈時間,評論數,資訊圖等信息。框架

輪播圖的實現

輪播圖主要使用了Flutter內置的TabBarView組件,該組件相似於Android中的ViewPager,能夠左右滑動切換頁面。爲了合理組織代碼,咱們將輪播圖單獨抽出來做爲一個自定義組件,在widgets/目錄下新建SlideView.dart文件並添加以下代碼:

import 'package:flutter/material.dart';

class SlideView extends StatefulWidget {
  var data;

  // data表示輪播圖中的數據
  SlideView(data) {
    this.data = data;
  }

  @override
  State<StatefulWidget> createState() {
    // 能夠在構造方法中傳參供SlideViewState使用
    // 或者也能夠不傳參數,直接在SlideViewState中經過this.widget.data訪問SlideView中的data變量
    return new SlideViewState(data);
  }
}

class SlideViewState extends State<SlideView> with SingleTickerProviderStateMixin {
  // TabController爲TabBarView組件的控制器
  TabController tabController;
  List slideData;

  SlideViewState(data) {
    slideData = data;
  }

  @override
  void initState() {
    super.initState();
    // 初始化控制器
    tabController = new TabController(length: slideData == null ? 0 : slideData.length, vsync: this);
  }

  @override
  void dispose() {
    // 銷燬
    tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> items = [];
    if (slideData != null && slideData.length > 0) {
      for (var i = 0; i < slideData.length; i++) {
        var item = slideData[i];
        // 圖片URL
        var imgUrl = item['imgUrl'];
        // 資訊標題
        var title = item['title'];
        // 資訊詳情URL
        var detailUrl = item['detailUrl'];
        items.add(new GestureDetector(
          onTap: () {
            // 點擊頁面跳轉到詳情
          },
          child: new Stack( // Stack組件用於將資訊標題文本放置到圖片上面
            children: <Widget>[
              // 加載網絡圖片
              new Image.network(imgUrl),
              new Container(
                // 標題容器寬度跟屏幕寬度一致
                width: MediaQuery.of(context).size.width,
                // 背景爲黑色,加入透明度
                color: const Color(0x50000000),
                // 標題文本加入內邊距
                child: new Padding(
                  padding: const EdgeInsets.all(6.0),
                  // 字體大小爲15,顏色爲白色
                  child: new Text(title, style: new TextStyle(color: Colors.white, fontSize: 15.0)),
                )
              )
            ],
          ),
        ));
      }
    }
    return new TabBarView(
      controller: tabController,
      children: items,
    );
  }

}
複製代碼

TabBarView組件主要的參數是controller和children,controller表明這個TabBarView的控制器,children表示這個組件中的各個頁面。SliderView中的data是在new這個對象時經過構造方法傳入的,data是一個map數組,map中包含imgUrl title detailUrl3個字段。

注意:本項目的輪播圖裏沒有加入小圓點頁面指示器,小夥伴們可自行添加相關代碼。

輪播圖和列表的組合

上面實現了自定義的輪播圖組件,下面就須要將這個組件和列表組合起來。

因爲資訊列表的item佈局稍微有些複雜,因此這裏有必要進行拆分,總體上能夠將item分爲左右兩部分,左邊展現了資訊標題,時間,評論數等信息,右邊展現了資訊的圖片。因此總體是一個Row組件,而左邊又是一個Column組件,Column組件的第一列是標題,第二列又是一個Row組件,其中有時間、做者頭像、評論數等信息。下面直接上NewsListPage.dart的代碼,在代碼中作詳細的註釋:

import 'package:flutter/material.dart';
import 'package:flutter_osc/widgets/SlideView.dart';

// 資訊列表頁面
class NewsListPage extends StatelessWidget {

  // 輪播圖的數據
  var slideData = [];
  // 列表的數據(輪播圖數據和列表數據分開,可是實際上輪播圖和列表中的item同屬於ListView的item)
  var listData = [];
  // 列表中資訊標題的樣式
  TextStyle titleTextStyle = new TextStyle(fontSize: 15.0);
  // 時間文本的樣式
  TextStyle subtitleStyle = new TextStyle(color: const Color(0xFFB5BDC0), fontSize: 12.0);

  NewsListPage() {
    // 這裏作數據初始化,加入一些測試數據
    for (int i = 0; i < 3; i++) {
      Map map = new Map();
      // 輪播圖的資訊標題
      map['title'] = 'Python 之父透露退位隱情,與核心開發團隊產生隔閡';
      // 輪播圖的詳情URL
      map['detailUrl'] = 'https://www.oschina.net/news/98455/guido-van-rossum-resigns';
      // 輪播圖的圖片URL
      map['imgUrl'] = 'https://static.oschina.net/uploads/img/201807/30113144_1SRR.png';
      slideData.add(map);
    }
    for (int i = 0; i < 30; i++) {
      Map map = new Map();
      // 列表item的標題
      map['title'] = 'J2Cache 2.3.23 發佈,支持 memcached 二級緩存';
      // 列表item的做者頭像URL
      map['authorImg'] = 'https://static.oschina.net/uploads/user/0/12_50.jpg?t=1421200584000';
      // 列表item的時間文本
      map['timeStr'] = '2018/7/30';
      // 列表item的資訊圖片
      map['thumb'] = 'https://static.oschina.net/uploads/logo/j2cache_N3NcX.png';
      // 列表item的評論數
      map['commCount'] = 5;
      listData.add(map);
    }
  }

  @override
  Widget build(BuildContext context) {
    return new ListView.builder(
      // 這裏itemCount是將輪播圖組件、分割線和列表items都做爲ListView的item算了
      itemCount: listData.length * 2 + 1,
      itemBuilder: (context, i) => renderRow(i)
    );
  }

  // 渲染列表item
  Widget renderRow(i) {
    // i爲0時渲染輪播圖
    if (i == 0) {
      return new Container(
        height: 180.0,
        child: new SlideView(slideData),
      );
    }
    // i > 0時
    i -= 1;
    // i爲奇數,渲染分割線
    if (i.isOdd) {
      return new Divider(height: 1.0);
    }
    // 將i取整
    i = i ~/ 2;
    // 獲得列表item的數據
    var itemData = listData[i];
    // 表明列表item中的標題這一行
    var titleRow = new Row(
      children: <Widget>[
        // 標題充滿一整行,因此用Expanded組件包裹
        new Expanded(
          child: new Text(itemData['title'], style: titleTextStyle),
        )
      ],
    );
    // 時間這一行包含了做者頭像、時間、評論數這幾個
    var timeRow = new Row(
      children: <Widget>[
        // 這是做者頭像,使用了圓形頭像
        new Container(
          width: 20.0,
          height: 20.0,
          decoration: new BoxDecoration(
            // 經過指定shape屬性設置圖片爲圓形
            shape: BoxShape.circle,
            color: const Color(0xFFECECEC),
            image: new DecorationImage(
              image: new NetworkImage(itemData['authorImg']), fit: BoxFit.cover),
            border: new Border.all(
              color: const Color(0xFFECECEC),
              width: 2.0,
            ),
          ),
        ),
        // 這是時間文本
        new Padding(
          padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
          child: new Text(
            itemData['timeStr'],
            style: subtitleStyle,
          ),
        ),
        // 這是評論數,評論數由一個評論圖標和具體的評論數構成,因此是一個Row組件
        new Expanded(
          flex: 1,
          child: new Row(
            // 爲了讓評論數顯示在最右側,因此須要外面的Expanded和這裏的MainAxisAlignment.end
            mainAxisAlignment: MainAxisAlignment.end,
            children: <Widget>[
              new Text("${itemData['commCount']}", style: subtitleStyle),
              new Image.asset('./images/ic_comment.png', width: 16.0, height: 16.0),
            ],
          ),
        )
      ],
    );
    var thumbImgUrl = itemData['thumb'];
    // 這是item右側的資訊圖片,先設置一個默認的圖片
    var thumbImg = new Container(
      margin: const EdgeInsets.all(10.0),
      width: 60.0,
      height: 60.0,
      decoration: new BoxDecoration(
        shape: BoxShape.circle,
        color: const Color(0xFFECECEC),
        image: new DecorationImage(
          image: new ExactAssetImage('./images/ic_img_default.jpg'),
          fit: BoxFit.cover),
        border: new Border.all(
          color: const Color(0xFFECECEC),
          width: 2.0,
        ),
      ),
    );
    // 若是上面的thumbImgUrl不爲空,就把以前thumbImg默認的圖片替換成網絡圖片
    if (thumbImgUrl != null && thumbImgUrl.length > 0) {
      thumbImg = new Container(
        margin: const EdgeInsets.all(10.0),
        width: 60.0,
        height: 60.0,
        decoration: new BoxDecoration(
          shape: BoxShape.circle,
          color: const Color(0xFFECECEC),
          image: new DecorationImage(
              image: new NetworkImage(thumbImgUrl), fit: BoxFit.cover),
          border: new Border.all(
            color: const Color(0xFFECECEC),
            width: 2.0,
          ),
        ),
      );
    }
    // 這裏的row表明了一個ListItem的一行
    var row = new Row(
      children: <Widget>[
        // 左邊是標題,時間,評論數等信息
        new Expanded(
          flex: 1,
          child: new Padding(
            padding: const EdgeInsets.all(10.0),
            child: new Column(
              children: <Widget>[
                titleRow,
                new Padding(
                  padding: const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 0.0),
                  child: timeRow,
                )
              ],
            ),
          ),
        ),
        // 右邊是資訊圖片
        new Padding(
          padding: const EdgeInsets.all(6.0),
          child: new Container(
            width: 100.0,
            height: 80.0,
            color: const Color(0xFFECECEC),
            child: new Center(
              child: thumbImg,
            ),
          ),
        )
      ],
    );
    // 用InkWell包裹row,讓row能夠點擊
    return new InkWell(
      child: row,
      onTap: () {
      },
    );
  }
}
複製代碼

動彈列表頁面的實現

動彈列表要實現的效果以下圖:

爲了區分普通的動彈和熱門動彈,須要使用兩個Tab來分別展現不一樣的頁面,這裏使用的是Flutter提供的DefaultTabController組件,該組件的用法也比較簡單,下面是TweetsList.dart的build方法的代碼:

@override
  Widget build(BuildContext context) {
    // 獲取屏幕寬度
    screenWidth = MediaQuery.of(context).size.width;
    return new DefaultTabController(
      length: 2,
      child: new Scaffold(
        appBar: new TabBar(
          tabs: <Widget>[
            new Tab(text: "動彈列表"),
            new Tab(text: "熱門動彈")
          ],
        ),
        body: new TabBarView(
          children: <Widget>[getNormalListView(), getHotListView()],
        )),
    );
  }
  
    // 獲取普通動彈列表
  Widget getNormalListView() {
    return new ListView.builder(
      itemCount: normalTweetsList.length * 2 - 1,
      itemBuilder: (context, i) => renderNormalRow(i)
    );
  }

  // 獲取熱門動彈列表
  Widget getHotListView() {
    return new ListView.builder(
      itemCount: hotTweetsList.length * 2 - 1,
      itemBuilder: (context, i) => renderHotRow(i),
    );
  }
  
  // 渲染普通動彈列表Item
  renderHotRow(i) {
    if (i.isOdd) {
      return new Divider(
        height: 1.0,
      );
    } else {
      i = i ~/ 2;
      return getRowWidget(hotTweetsList[i]);
    }
  }

  // 渲染熱門動彈列表Item
  renderNormalRow(i) {
    if (i.isOdd) {
      return new Divider(
        height: 1.0,
      );
    } else {
      i = i ~/ 2;
      return getRowWidget(normalTweetsList[i]);
    }
  }
複製代碼

在TabBarView中,children參數是一個數組,表明不一樣的頁面,這裏使用兩個方法分別返回普通的動彈列表和熱門動彈列表,編碼實現動彈列表前,先定義以下一些變量供後面使用,並在TweetsList類的構造方法中初始化這些變量:

import 'package:flutter/material.dart';

// 動彈列表頁面
class TweetsListPage extends StatelessWidget {

  // 熱門動彈數據
  List hotTweetsList = [];
  // 普通動彈數據
  List normalTweetsList = [];
  // 動彈做者文本樣式
  TextStyle authorTextStyle;
  // 動彈時間文本樣式
  TextStyle subtitleStyle;
  // 屏幕寬度
  double screenWidth;

  // 構造方法中作數據初始化
  TweetsListPage() {
    authorTextStyle = new TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold);
    subtitleStyle = new TextStyle(fontSize: 12.0, color: const Color(0xFFB5BDC0));
    // 添加測試數據
    for (int i = 0; i < 20; i++) {
      Map<String, dynamic> map = new Map();
      // 動彈發佈時間
      map['pubDate'] = '2018-7-30';
      // 動彈文字內容
      map['body'] = '早上七點十分起牀,四十出門,花二十多分鐘到公司,必須在八點半以前打卡;下午一點上班到六點,而後加班兩個小時;八點左右離開公司,呼呼登自行車到健身房鍛鍊一個多小時。到家已經十點多,而後準備次日的午餐,接着收拾廚房,而後洗澡,吹頭髮,等能坐下來吹頭髮時已經快十二點了。感受很累。';
      // 動彈做者暱稱
      map['author'] = '紅薯';
      // 動彈評論數
      map['commentCount'] = 10;
      // 動彈做者頭像URL
      map['portrait'] = 'https://static.oschina.net/uploads/user/0/12_50.jpg?t=1421200584000';
      // 動彈中的圖片,多張圖片用英文逗號隔開
      map['imgSmall'] = 'https://b-ssl.duitang.com/uploads/item/201508/27/20150827135810_hGjQ8.thumb.700_0.jpeg,https://b-ssl.duitang.com/uploads/item/201508/27/20150827135810_hGjQ8.thumb.700_0.jpeg,https://b-ssl.duitang.com/uploads/item/201508/27/20150827135810_hGjQ8.thumb.700_0.jpeg,https://b-ssl.duitang.com/uploads/item/201508/27/20150827135810_hGjQ8.thumb.700_0.jpeg';
      hotTweetsList.add(map);
      normalTweetsList.add(map);
    }
  }
}
複製代碼

有了測試數據,下面最主要的是實現列表的展現,而列表展現最爲麻煩的,是列表item的渲染。每一個item中要展現用戶頭像,用戶暱稱,動彈發佈時間,動彈評論數,若是動彈中有圖片,還須要以九宮格的方式顯示圖片。簡單分析下動彈列表的item,應該是用Column組件展現,Column組件的第一行顯示用戶頭像、暱稱、發佈動彈的時間,第二行應該顯示動彈的內容,第三行是可展現可不展現的九宮格,若是動彈中有圖片,則顯示,不然不限時,第四行是動彈評論數,顯示在右下角。下面分小步來實現列表item的渲染:

第一行,顯示用戶頭像,暱稱和發佈時間

這一行用個Row組件展現便可,代碼以下:

// 列表item的第一行,顯示動彈做者頭像、暱稱、評論數
var authorRow = new Row(
  children: <Widget>[
    // 用戶頭像
    new Container(
      width: 35.0,
      height: 35.0,
      decoration: new BoxDecoration(
        // 頭像顯示爲圓形
        shape: BoxShape.circle,
        color: Colors.transparent,
        image: new DecorationImage(
          image: new NetworkImage(listItem['portrait']),
          fit: BoxFit.cover),
        // 頭像邊框
        border: new Border.all(
          color: Colors.white,
          width: 2.0,
        ),
      ),
    ),
    // 動彈做者的暱稱
    new Padding(
      padding: const EdgeInsets.fromLTRB(6.0, 0.0, 0.0, 0.0),
      child: new Text(
        listItem['author'],
        style: new TextStyle(fontSize: 16.0)
      )
    ),
    // 動彈評論數,顯示在最右邊
    new Expanded(
      child: new Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          new Text(
            '${listItem['commentCount']}',
            style: subtitleStyle,
          ),
          new Image.asset(
            './images/ic_comment.png',
            width: 16.0,
            height: 16.0,
          )
        ],
      ),
    )
  ],
);
複製代碼

第二行,顯示動彈內容

這一行僅僅是一段文本,因此代碼比較簡單:

// 動彈內容,純文本展現
var _body = listItem['body'];
var contentRow = new Row(
  children: <Widget>[
    new Expanded(child: new Text(_body))
  ],
);
複製代碼

第三行,顯示動彈中的圖片,沒有圖片則不展現這一行

以九宮格的形式顯示圖片稍微麻煩些,這也是爲何以前咱們要在build方法中獲取屏幕的寬度,由於要根據這個寬度來計算九宮格中圖片的寬度。另外,九宮格中的圖片URL是以字符串形式給出的,以英文逗號隔開的,因此須要對圖片URL作分割處理。若是動彈中有圖片,可能有1~9張,下面用一個方法來肯定用九宮格顯示時,總共有幾行:

// 獲取行數,n表示圖片的張數
  // 若是n取餘不爲0,則行數爲n取整+1,不然n取整就是行數
  int getRow(int n) {
    int a = n % 3; // 取餘
    int b = n ~/ 3; // 取整
    if (a != 0) {
      return b + 1;
    }
    return b;
  }
複製代碼

好比一共有9張圖片,9 % 3爲0,則一共有9 ~/3 = 3行,若是一共有5張圖片,5 % 3 != 0,則行數爲5 ~/ 3再+1即兩行。

下面是生成九宮格圖片的代碼:

// 動彈中的圖片數據,字符串,多張圖片以英文逗號分隔
    String imgSmall = listItem['imgSmall'];
    if (imgSmall != null && imgSmall.length > 0) {
      // 動彈中有圖片
      List<String> list = imgSmall.split(",");
      List<String> imgUrlList = new List<String>();
      // 開源中國的openapi給出的圖片,有多是相對地址,因此用下面的代碼將相對地址補全
      for (String s in list) {
        if (s.startsWith("http")) {
          imgUrlList.add(s);
        } else {
          imgUrlList.add("https://static.oschina.net/uploads/space/" + s);
        }
      }
      List<Widget> imgList = [];
      List<List<Widget>> rows = [];
      num len = imgUrlList.length;
      // 經過雙重for循環,生成每一張圖片組件
      for (var row = 0; row < getRow(len); row++) { // row表示九宮格的行數,可能有1行2行或3行
        List<Widget> rowArr = [];
        for (var col = 0; col < 3; col++) { // col爲列數,固定有3列
          num index = row * 3 + col;
          double cellWidth = (screenWidth - 100) / 3;
          if (index < len) {
            rowArr.add(new Padding(
              padding: const EdgeInsets.all(2.0),
              child: new Image.network(imgUrlList[index],
                  width: cellWidth, height: cellWidth),
            ));
          }
        }
        rows.add(rowArr);
      }
      for (var row in rows) {
        imgList.add(new Row(
          children: row,
        ));
      }
      columns.add(new Padding(
        padding: const EdgeInsets.fromLTRB(52.0, 5.0, 10.0, 0.0),
        child: new Column(
          children: imgList,
        ),
      ));
    }
複製代碼

上面代碼的最後有個columns變量,表明的是整個item的一個列布局,在生成九宮格佈局前,已經將第一行和第二行添加到columns中:

var columns = <Widget>[
  // 這是item中第一行
  new Padding(
    padding: const EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 2.0),
    child: authorRow,
  ),
  // 這是item中第二行
  new Padding(
    padding: const EdgeInsets.fromLTRB(52.0, 0.0, 10.0, 0.0),
    child: contentRow,
  ),
];
複製代碼

若是動彈中有圖片,則columns中還要添加九宮格圖片組件。

第四行,顯示動彈發佈時間 這一行佈局比較簡單:

var timeRow = new Row(
  mainAxisAlignment: MainAxisAlignment.end,
  children: <Widget>[
    new Text(
      listItem['pubDate'],
      style: subtitleStyle,
    )
  ],
);

columns.add(new Padding(
  padding: const EdgeInsets.fromLTRB(0.0, 10.0, 10.0, 6.0),
  child: timeRow,
));  
複製代碼

最後返回一個用一個InkWell組件包裹的columns便可:

return new InkWell(
  child: new Column(
    children: columns,
  ),
  onTap: () {
    // 跳轉到動彈詳情
  }
    );
複製代碼

「發現」頁面的實現

本篇要實現的發現頁面效果圖以下:

該頁面就是一個簡單的ListView,可是稍微有些不一樣的是,ListView中的分割線有的長,有的短,有的分割線之間還有空白區域分隔,爲了實現這個佈局,我用了一種方法是將長短不一樣的分割線,或者兩條分割線間的空白區域,都用不一樣的字符串來標記,在渲染列表的時候,根據不一樣的字符串來渲染不一樣的組件,代碼很容易理解,因此這裏直接放源碼連接了:源碼,源碼中已有詳細註釋。

「個人」頁面的實現

本篇要實現的個人頁面效果圖以下:

這個頁面也比較簡單,頭部的綠色區域也屬於ListView的一部分,也是ListView的多佈局,具體實現方式就不細說了,直接放代碼:源碼

源碼

本篇相關的全部源碼都在GitHub上demo-flutter-osc項目的v0.2分支

後記

本篇主要記錄的是基於Flutter的開源中國客戶端各個靜態頁面的實現,僅限於UI,具體的網絡請求,數據存儲和其餘邏輯在下一篇中作記錄。

個人開源項目

  1. 基於Google Flutter的開源中國客戶端,但願你們給個Star支持一下,源碼:
  1. 基於Flutter的俄羅斯方塊小遊戲,但願你們給個Star支持一下,源碼:
上一篇 下一篇
從0開始寫一個基於Flutter的開源中國客戶端(5)
——App總體佈局框架搭建
從0開始寫一個基於Flutter的開源中國客戶端(7)——App網絡請求和數據存儲
相關文章
相關標籤/搜索