iOS开发Flutter探索-实现一个iOS-Grouped风格的tableView(7)

前言

大家都知道,在Flutter框架中,渲染并不像ReactNative或者Veex等通过JSCore来映射成原生组件,而是有自己的一套渲染引擎,这也是Flutter的强大之处。来上一张Flutter架构图:


来源Flutter中文网.png

从图上可以看出,我们是在Framework层通过Dart语法进行开发,再往下层就是基于C++的一些引擎依赖。因此,如果我们想在Flutter中使用像iOS-Grouped风格的TableView就需要使用Widget来自己构建了。
今天我们就以微信通讯录页面UI来做实现一个Grouped风格的ListView。先看下效果图:


效果图.gif

开始

  • 布局分析
    因为有section的存在,在iOS下是很好实现的,但是在Flutter中并没有相应的API或者回调来配置这部分,所有我把这部分归类到Cell中,然后通过数据来控制Section的显示。如图:


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

整个cell外层通过一个Column()布局拆分成Section+Content两部分,然后Content部分用Row()布局拆分成leftIcon和nickName部分,因为nickName 部分底部还有一跟分割线,所有nickName部分可以通过一个一个Column()布局分为上下两个部分。(还是那句话,UI布局思路非常多且灵活,以上只是个人的思路)。

  • 开始造
    这里还是基于之前创建的一个Tabbar项目,这里我们使用ListView.builder(),这个是可是实现Cell复用的ListView,内部需要一个itemCount(就是numberOfRowsInSection:)和itemBuilder(是一个callback函数,类似于cellForRowAtIndexPath:);我这里把这个callback的实现写在了外面的_cellForRowAtIndex方法中,这个方法在滚动列表时,也会实时回调,跟cellForRowAtIndexPath一模一样,一个需要返回一个UITableViewCell,一个需要返回一个Widget.
    以下是build()部分的代码:
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('泽泽2'),
      ),
      body: Container(
        child: ListView.builder(  //具备复用能力的ListView()
          itemCount: _headerData.length+_listData.length,   // 数组个数
          itemBuilder: _cellForRowAtIndex,  //复用回调函数
      ),
      ),
    );
  }

_itemForRow()代码块:

 //cell复用回调
  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),
                  //设置圆角
                  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, //上下对齐
                    children: [
                      Container(  //昵称
//                        color: Colors.red,
                        height:53.5,
                        width: MediaQuery.of(context).size.width - 10 - 10 - 34,
                        alignment: Alignment.centerLeft,  //文本对齐
                        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

现在看到的效果就是我们定制的这个完整Cell的样子了(SectionHeader+Content),但是我们不需要每个cell都来显示这个SectionHeader,怎么弄呐?我们通过model数据来做控制SectionHeader的显示。这里在model中定义了一个groupTitle的字段,根据groupTitle 是否为 null 来控制SectionHeader的显示与隐藏;第二个问题就是mode中包含相同groupTitle的Cell,只让第一个显示SectionHeader显示,之后就不显示,直到不同的groupTitle出现。思路还是数据驱动UI的方式。
首先把列表分成了4+n,因为上面的4个cell是固定不变的,分别将model数据添加到_headerData_listData中,然后来到_cellForRowAtIndex中做一些判断:

//cell复用回调
  Widget _cellForRowAtIndex(BuildContext context, int index){
      //头部cell
      if(index < _headerData.length){
        FriendModel model = _headerData[index];
        return CustomCell(
            imageAssets: model.imageUrl,
            name: model.name,
        );
      }
      //其他cell
      //如果当前model.indexLetter(对应model中的indexLetter字段)与上一个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

这样我们就将一个Group风格的tableView就构造完成了。

补充

  • 常用的一些变量或者常亮或者一些方法可以单独的抽离到一个dart文件中,比如获取屏幕的宽高,一些主题颜色等,比如我这里的app_config.dart文件
import 'package:flutter/material.dart';

//主题色调
final Color APP_ThemeColor = Color.fromRGBO(223, 223, 223, 1.0);
//屏幕的宽 等同于 [UIScreen mainScreen].bounds.size.width 因为这里需要一个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),
)
  • 关于导航条(AppBar) 上如何配置左右视图(比如iOS下的UINavigationItem),这里需要用到AppBar下的actions属性,接收一个Widget数组,可以添加多个Widget,这里举例一下
    我们添加一个Icon(),然后通过GestureDetector()Icon()一个点击事件,然后通过Navigator实现页面跳转并传参。
 @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('我被点击了');
              Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context){
                return AddFriend(
                  cellName: '泽泽伐木类',
                );
              }));
            },
          ),
        ],
      ),
      body: Container(
        child: ListView.builder(  //具备复用能力的ListView()
          itemCount: _headerData.length+_listData.length,
          itemBuilder: _cellForRowAtIndex,
      ),
      ),
    );
  }
}
  • flutter 中的..级联调用语法,可以对同一个对象进行一系列操作,比如在构造数据多次的调用addAll()的时候:
@override
  void initState() {     //这个方法需要在State类重写 同iOS下的viewWillAppear()
    super.initState();
    //构造下数据 .. 语法糖
    //为了多加点测试数据,对_listData进行了两次addAll(),然后根据字母进行了排序
    _listData..addAll(datas)..addAll(datas)..sort((FriendModel a,FriendModel b){
        return a.indexLetter.compareTo(b.indexLetter);
    });
  }

这里等同于

@override
  void initState() {     //这个方法需要在State类重写 同iOS下的viewWillAppear()
    super.initState();
    //构造下数据 .. 语法
    _listData.addAll(datas);
     _listData.addAll(datas);
    //按照字母进行排序 跟NSArray类似
    _listData.sort((FriendModel a,FriendModel b){
      return a.indexLetter.compareTo(b.indexLetter);
    });
  }

更多Dart语法相关的内容,会在后面的文章中介绍

总结

本篇内容大部分的内容还是关于UI相关的东西,主要就是拓展一下在iOS中常见的一些UI布局,迁移到Flutter中的一些思路;同时也简单的提到了一些Dart语法相关的东西,这个后续我们详细聊;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342