iOS開發(fā)Flutter探索-實(shí)現(xiàn)一個(gè)iOS-Grouped風(fēng)格的tableView(7)

前言

大家都知道混巧,在Flutter框架中糕殉,渲染并不像ReactNative或者Veex等通過(guò)JSCore來(lái)映射成原生組件奕锌,而是有自己的一套渲染引擎挤茄,這也是Flutter的強(qiáng)大之處山害。來(lái)上一張F(tuán)lutter架構(gòu)圖:


來(lái)源Flutter中文網(wǎng).png

從圖上可以看出纠俭,我們是在Framework層通過(guò)Dart語(yǔ)法進(jìn)行開發(fā),再往下層就是基于C++的一些引擎依賴浪慌。因此,如果我們想在Flutter中使用像iOS-Grouped風(fēng)格的TableView就需要使用Widget來(lái)自己構(gòu)建了冤荆。
今天我們就以微信通訊錄頁(yè)面UI來(lái)做實(shí)現(xiàn)一個(gè)Grouped風(fēng)格的ListView。先看下效果圖:


效果圖.gif

開始

  • 布局分析
    因?yàn)橛衧ection的存在权纤,在iOS下是很好實(shí)現(xiàn)的钓简,但是在Flutter中并沒(méi)有相應(yīng)的API或者回調(diào)來(lái)配置這部分,所有我把這部分歸類到Cell中汹想,然后通過(guò)數(shù)據(jù)來(lái)控制Section的顯示外邓。如圖:


    Simulator Screen Shot - iPhone 11 - 2020-06-18 at 15.21.07.png

整個(gè)cell外層通過(guò)一個(gè)Column()布局拆分成Section+Content兩部分,然后Content部分用Row()布局拆分成leftIcon和nickName部分,因?yàn)閚ickName 部分底部還有一跟分割線,所有nickName部分可以通過(guò)一個(gè)一個(gè)Column()布局分為上下兩個(gè)部分古掏。(還是那句話,UI布局思路非常多且靈活损话,以上只是個(gè)人的思路)。

  • 開始造
    這里還是基于之前創(chuàng)建的一個(gè)Tabbar項(xiàng)目槽唾,這里我們使用ListView.builder(),這個(gè)是可是實(shí)現(xiàn)Cell復(fù)用的ListView,內(nèi)部需要一個(gè)itemCount(就是numberOfRowsInSection:)和itemBuilder(是一個(gè)callback函數(shù),類似于cellForRowAtIndexPath:)丧枪;我這里把這個(gè)callback的實(shí)現(xiàn)寫在了外面的_cellForRowAtIndex方法中,這個(gè)方法在滾動(dòng)列表時(shí)光涂,也會(huì)實(shí)時(shí)回調(diào),跟cellForRowAtIndexPath一模一樣豪诲,一個(gè)需要返回一個(gè)UITableViewCell,一個(gè)需要返回一個(gè)Widget.
    以下是build()部分的代碼:
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('澤澤2'),
      ),
      body: Container(
        child: ListView.builder(  //具備復(fù)用能力的ListView()
          itemCount: _headerData.length+_listData.length,   // 數(shù)組個(gè)數(shù)
          itemBuilder: _cellForRowAtIndex,  //復(fù)用回調(diào)函數(shù)
      ),
      ),
    );
  }

_itemForRow()代碼塊:

 //cell復(fù)用回調(diào)
  Widget _cellForRowAtIndex(BuildContext context, int index){
      //頭部cell
      if(index < _headerData.length){
        FriendModel model = _headerData[index];
        return CustomCell(
            imageAssets: model.imageUrl,
            name: model.name,
        );
      }
      //其他cell
      return CustomCell(
        imageUrl: _listData[index - 4].imageUrl,
        name: _listData[index - 4].name,
        groupTitle: _listData[index - 4].indexLetter,
      );
  }

自定義Cell custom_cell.dart部分:

import 'package:flutter/material.dart';

class CustomCell extends StatefulWidget {
  final String imageUrl;
  final String name;
  final String groupTitle;
  final String imageAssets;

  const CustomCell({
    Key key,
    this.imageUrl,
    this.name,
    this.groupTitle,
    this.imageAssets,
  }) : super(key: key);

  @override
  _CustomCellState createState() => _CustomCellState();
}

class _CustomCellState extends State<CustomCell> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Container(
            alignment: Alignment.centerLeft,
            padding: EdgeInsets.only(left: 10),
            height: widget.groupTitle != null ? 30 : 0,
            color: Color.fromRGBO(236, 237, 237, 1.0),
//            child: Text(widget.groupTitle,style: TextStyle(color: Colors.white),),
            child: widget.groupTitle != null ? Text(widget.groupTitle,style: TextStyle(color: Color.fromRGBO(76, 75, 75, 1.0),),):null,
          ),//SectionHeader
          Container(
            height: 54,
            color: Colors.white,
            child: Row(
              children: [
                Container(
                  width:34,
                  height: 34,
                  margin: EdgeInsets.all(10),
                  //設(shè)置圓角
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(6.0),
                    image: DecorationImage(
                        image:widget.imageUrl != null ? NetworkImage(widget.imageUrl) : AssetImage(widget.imageAssets),
                    ),
                  ),
                ), //左圖
                Container(
//                  color: Colors.blue,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween, //上下對(duì)齊
                    children: [
                      Container(  //昵稱
//                        color: Colors.red,
                        height:53.5,
                        width: MediaQuery.of(context).size.width - 10 - 10 - 34,
                        alignment: Alignment.centerLeft,  //文本對(duì)齊
                        child: Text(
                          widget.name,
                          style: TextStyle(fontSize: 18),
                        ),
                      ),
                      Container(  //線
                         height: 0.5,
                         width: MediaQuery.of(context).size.width - 10 - 10 - 34,
                         color: Color.fromRGBO(246, 246, 246, 1.0),

                      )
                    ],
                  ),
                ),
              ],
            ),
          ),//Contents
        ],
      ),
    );
  }
}

目前看到的效果:

Simulator Screen Shot - iPhone 11 - 2020-06-18 at 14.37.07.png

現(xiàn)在看到的效果就是我們定制的這個(gè)完整Cell的樣子了(SectionHeader+Content),但是我們不需要每個(gè)cell都來(lái)顯示這個(gè)SectionHeader,怎么弄吶挂绰?我們通過(guò)model數(shù)據(jù)來(lái)做控制SectionHeader的顯示屎篱。這里在model中定義了一個(gè)groupTitle的字段,根據(jù)groupTitle 是否為 null 來(lái)控制SectionHeader的顯示與隱藏葵蒂;第二個(gè)問(wèn)題就是mode中包含相同groupTitle的Cell,只讓第一個(gè)顯示SectionHeader顯示交播,之后就不顯示,直到不同的groupTitle出現(xiàn)践付。思路還是數(shù)據(jù)驅(qū)動(dòng)UI的方式秦士。
首先把列表分成了4+n,因?yàn)樯厦娴?個(gè)cell是固定不變的,分別將model數(shù)據(jù)添加到_headerData_listData中永高,然后來(lái)到_cellForRowAtIndex中做一些判斷:

//cell復(fù)用回調(diào)
  Widget _cellForRowAtIndex(BuildContext context, int index){
      //頭部cell
      if(index < _headerData.length){
        FriendModel model = _headerData[index];
        return CustomCell(
            imageAssets: model.imageUrl,
            name: model.name,
        );
      }
      //其他cell
      //如果當(dāng)前model.indexLetter(對(duì)應(yīng)model中的indexLetter字段)與上一個(gè)model.indexLetter相同則不顯示
      //否則就顯示
      if(index > 4 && _listData[index - 4].indexLetter == _listData[index - 5].indexLetter){
        //groupTitle = null
        return CustomCell(
          imageUrl: _listData[index - 4].imageUrl,
          name: _listData[index - 4].name,
        );
      }

      return CustomCell(
        imageUrl: _listData[index - 4].imageUrl,
        name: _listData[index - 4].name,
        groupTitle: _listData[index - 4].indexLetter,
      );
  }

效果圖:


Simulator Screen Shot - iPhone 11 - 2020-06-18 at 15.16.43.png

這樣我們就將一個(gè)Group風(fēng)格的tableView就構(gòu)造完成了隧土。

補(bǔ)充

  • 常用的一些變量或者常亮或者一些方法可以單獨(dú)的抽離到一個(gè)dart文件中,比如獲取屏幕的寬高命爬,一些主題顏色等曹傀,比如我這里的app_config.dart文件
import 'package:flutter/material.dart';

//主題色調(diào)
final Color APP_ThemeColor = Color.fromRGBO(223, 223, 223, 1.0);
//屏幕的寬 等同于 [UIScreen mainScreen].bounds.size.width 因?yàn)檫@里需要一個(gè)context 所以需要定義成方法
double ScreenWidth(BuildContext context) => MediaQuery.of(context).size.width;
//屏幕的高
double ScreenHeight(BuildContext context) => MediaQuery.of(context).size.height;

使用app_config.dart

Container(  //線
     height: 0.5,
     width: ScreenWidth(context) - 10 - 10 - 34,
     color: Color.fromRGBO(246, 246, 246, 1.0),
)
  • 關(guān)于導(dǎo)航條(AppBar) 上如何配置左右視圖(比如iOS下的UINavigationItem),這里需要用到AppBar下的actions屬性饲宛,接收一個(gè)Widget數(shù)組皆愉,可以添加多個(gè)Widget,這里舉例一下
    我們添加一個(gè)Icon()艇抠,然后通過(guò)GestureDetector()Icon()一個(gè)點(diǎn)擊事件幕庐,然后通過(guò)Navigator實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)并傳參。
 @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('澤澤2'),
        actions: <Widget>[
          GestureDetector(
            child: Container(
              margin: EdgeInsets.only(right: 10),
              child: Icon(Icons.add),
            ),
            onTap: (){
              print('我被點(diǎn)擊了');
              Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context){
                return AddFriend(
                  cellName: '澤澤伐木類',
                );
              }));
            },
          ),
        ],
      ),
      body: Container(
        child: ListView.builder(  //具備復(fù)用能力的ListView()
          itemCount: _headerData.length+_listData.length,
          itemBuilder: _cellForRowAtIndex,
      ),
      ),
    );
  }
}
  • flutter 中的..級(jí)聯(lián)調(diào)用語(yǔ)法家淤,可以對(duì)同一個(gè)對(duì)象進(jìn)行一系列操作异剥,比如在構(gòu)造數(shù)據(jù)多次的調(diào)用addAll()的時(shí)候:
@override
  void initState() {     //這個(gè)方法需要在State類重寫 同iOS下的viewWillAppear()
    super.initState();
    //構(gòu)造下數(shù)據(jù) .. 語(yǔ)法糖
    //為了多加點(diǎn)測(cè)試數(shù)據(jù),對(duì)_listData進(jìn)行了兩次addAll(),然后根據(jù)字母進(jìn)行了排序
    _listData..addAll(datas)..addAll(datas)..sort((FriendModel a,FriendModel b){
        return a.indexLetter.compareTo(b.indexLetter);
    });
  }

這里等同于

@override
  void initState() {     //這個(gè)方法需要在State類重寫 同iOS下的viewWillAppear()
    super.initState();
    //構(gòu)造下數(shù)據(jù) .. 語(yǔ)法
    _listData.addAll(datas)絮重;
     _listData.addAll(datas);
    //按照字母進(jìn)行排序 跟NSArray類似
    _listData.sort((FriendModel a,FriendModel b){
      return a.indexLetter.compareTo(b.indexLetter);
    });
  }

更多Dart語(yǔ)法相關(guān)的內(nèi)容届吁,會(huì)在后面的文章中介紹

總結(jié)

本篇內(nèi)容大部分的內(nèi)容還是關(guān)于UI相關(guān)的東西,主要就是拓展一下在iOS中常見的一些UI布局绿鸣,遷移到Flutter中的一些思路疚沐;同時(shí)也簡(jiǎn)單的提到了一些Dart語(yǔ)法相關(guān)的東西,這個(gè)后續(xù)我們?cè)敿?xì)聊潮模;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末亮蛔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子擎厢,更是在濱河造成了極大的恐慌究流,老刑警劉巖辣吃,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異芬探,居然都是意外死亡神得,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門偷仿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)哩簿,“玉大人,你說(shuō)我怎么就攤上這事酝静〗诎瘢” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵别智,是天一觀的道長(zhǎng)宗苍。 經(jīng)常有香客問(wèn)我,道長(zhǎng)薄榛,這世上最難降的妖魔是什么讳窟? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮敞恋,結(jié)果婚禮上挪钓,老公的妹妹穿的比我還像新娘。我一直安慰自己耳舅,他們只是感情好碌上,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浦徊,像睡著了一般馏予。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盔性,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天霞丧,我揣著相機(jī)與錄音,去河邊找鬼冕香。 笑死蛹尝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悉尾。 我是一名探鬼主播突那,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼构眯!你這毒婦竟也來(lái)了愕难?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猫缭,沒(méi)想到半個(gè)月后葱弟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡猜丹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年芝加,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片射窒。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡藏杖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轮洋,到底是詐尸還是另有隱情制市,我是刑警寧澤抬旺,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布弊予,位于F島的核電站,受9級(jí)特大地震影響开财,放射性物質(zhì)發(fā)生泄漏汉柒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一责鳍、第九天 我趴在偏房一處隱蔽的房頂上張望碾褂。 院中可真熱鬧,春花似錦历葛、人聲如沸正塌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乓诽。三九已至,卻和暖如春咒程,著一層夾襖步出監(jiān)牢的瞬間鸠天,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工帐姻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稠集,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓饥瓷,卻偏偏與公主長(zhǎng)得像剥纷,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呢铆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348