Flutter基础控件篇[3]

前言

Flutter基础控件篇[2],继续往下罗列。
这一篇主要记录Table、ListView、GridView、SingleChildScrollView、SimpleDialog、AlertDialog、自定义弹窗还有一个BottomSheet。

正文

Table

类似于Android的TableLayout,桌面布局,就跟手机桌面似的,几行几列。
基本使用:

Widget build(BuildContext context) {
    return Table(
      children: [
        TableRow(children: [
          Container(color: Colors.blue[200], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 100,),
          Container(color: Colors.blue[200], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 70,),
          Container(color: Colors.blue[200], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 70,),
        ]),
        TableRow(children: [
          Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 70,),
          Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 100, alignment: Alignment.center, child: Text('Table, 3行3列'),),
          Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 70,),
        ]),
        TableRow(children: [
          Container(color: Colors.blue[400], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 70,),
          Container(color: Colors.blue[400], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 70,),
          Container(color: Colors.blue[400], margin: EdgeInsets.fromLTRB(5, 0, 5, 0), width: 70, height: 100,),
        ])
      ],
      //横向宽度,权重,这里这样写分别占总宽度的1/4, 2/4, 1/4
      columnWidths: {0: FlexColumnWidth(1),1: FlexColumnWidth(2),2: FlexColumnWidth(1)},
//        columnWidths: {0: FixedColumnWidth(100),1: FixedColumnWidth(70),2: FixedColumnWidth(130)},//具体宽度,这里这样写宽度分别为100,70,130
      defaultVerticalAlignment: TableCellVerticalAlignment.middle, //纵向的对齐方式
    );
  }

主要属性:
children:子控件,参数是一个List<TableRow>,TableRow的子控件是一个List<Widget>,这样就构成了一个二维数组来排列子控件。List<Widget>具体布局每一行,List<TableRow>再把这每一行罗列起来,构成一个矩阵布局。
columnWidths:每一列的宽度,参数是一个Map<int, TableColumnWidth>,它规定了每一列展示的宽度。这里提供两种常用的写法:

  • columnWidths: {0: FixedColumnWidth(100),1: FixedColumnWidth(70),2: FixedColumnWidth(130)} ,宽度固定,这样写第1、2、3列的宽度分别是100,70,130;
  • columnWidths: {0: FlexColumnWidth(1),1: FlexColumnWidth(2),2: FlexColumnWidth(1)},按比例分配宽度,这里很像Android中的权重(Weight)。这样写总共将宽度分为4份,第一列占1/4,第二列占2/4,第三列占1/4;

border:背景边框,设置边框线,圆角之类。
defaultVerticalAlignment:纵向的对齐方式,参数是TableCellVerticalAlignment的几个固定值,top、middle、bottom、baseline、fill(填满高度);
defaultColumnWidth:默认每列宽度,统一设定每一行的宽度,按官方文档建议的值写 FlexColumnWidth(1.0),那就是均分,如果你写成一个固定值 FixedColumnWidth(70),那就是每列宽度都是70。

GridView

网格布局,名字个Android的GridVie一样,功能也基本一样。
基本使用:

Widget build(BuildContext context) {
    return GridView(
      scrollDirection: Axis.vertical,
      reverse: false,
      controller: scrollController,
//      primary: true,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, //横轴三个子widget
          childAspectRatio: 1.3 //宽高比为1
      ),
      children: <Widget>[
        Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 5, 5, 5), width: 70, height: 70,),
        Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 5, 5, 5), width: 70, height: 70,),
        Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 5, 5, 5), width: 70, height: 70,),
        Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 5, 5, 5), width: 70, height: 70,),
        Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 5, 5, 5), width: 70, height: 70,),
        Container(color: Colors.blue[300], margin: EdgeInsets.fromLTRB(5, 5, 5, 5), width: 70, height: 70,),
      ],
    );
  }

主要属性:
scrollDirection: 滚动方向,Axis.vertical(竖直方向);Axis.horizontal(水平方向);
reverse:子控件排序是否反转;
controller: 滚动控制器,监听和控制滚动用到,比如写下拉加载更多之类的功能就可以用到这个。可以参考ScrollController 滚动监听及控制
shrinkWrap:该属性表示是否根据子组件的总长度来设置GridView的长度,默认值为false 。默认情况下,GridView的会在滚动方向尽可能多的占用空间。当GridView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
gridDelegate: 控制子控件排列规则的参数,包括配置子控件横纵方向间距,纵轴方向子控件个数,子控件宽高比例等参数,详细文档参考GridView

然后还有几个不太被关注的属性:
primary: bool型,为true时,表示当子控件没有超出GridView的尺寸时,它依然是可滚动的,当scrollDirection是Axis.vertical并且controller不为null时它的默认值是true(试了一下当controller不为null,手动将primary设置为true的话会报错)。这个感觉不好理解,我试了一下,如果在子控件没有超出父控件尺寸时,把这个设置为true,效果如下:

primary: true 拖动时效果

拖动的时候会有这个拉到尽头的反馈效果,但好像对实际使用没啥影响。苹果手机没有试过,Android设备上这个属性应该不太重要。
cacheExtent:设置预加载的区域, cacheExtent 设置为 0.0,则关闭了“预加载”。
addAutomaticKeepAlives: 该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。(参考Flutter中文网的资料)

addRepaintBoundaries: 该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。(参考Flutter中文网的资料)

IndexedSemantics: 关于Semantics(语义),这里有一篇文章讲得稍微详细一点,Flutter中的Semantics

ListView

列表控件,这个在实际开发过程中使用的最多了。
这里写法基本分为两类,第一类写的很死,适合列表项比较固定的情况:

ListView(
  shrinkWrap: true, 
  padding: const EdgeInsets.all(20.0),
  children: <Widget>[
    const Text('I\'m dedicating every day to you'),
    const Text('Domestic life was never quite my style'),
    const Text('When you smile, you knock me out, I fall apart'),
    const Text('And I thought I was so smart'),
  ],
);

这样写基本上列表的数目和内容都是固定的,实际情况中应该很少会用这种写法。大多数实际需求中,列表展示的具体内容是未知的,列表的条数也是未知的,甚至有时候稍微复杂的列表,根据不同的数据类型,它的item布局都不相同,只有在获取到具体的数据之后,我们才知道这些信息,所以为了灵活的处理和展示列表数据,下面两种写法比较多一点:
1. ListView.builder

ListView.builder(
    itemCount: data.length,
    itemExtent: 50.0, //强制高度为50.0
    itemBuilder: (BuildContext context, int index) {
      return ListTile(title: Text("$index"));
    }
);

itemBuilder作为列表项的布局构建器,具体构建每个位置上的item布局,并展示相应的内容。itemCount配置列表的item条数,可以根据具体的数据来灵活配置。
2. ListView.separated
相对于ListView.builder,ListView.separated增加了一个分割线的构造器separatorBuilder,分割线在实际开发中也是很常用到的。

 Widget build(BuildContext context) {
    return ListView.separated(
        itemBuilder: (BuildContext context, int index) {
          return Container(
              height: 60,
              child: FlatButton( onPressed: () {}, child: Text(menus[index],)));
        },
        separatorBuilder: (BuildContext context, int index) {
          return Container(height: 1,color: Color(0xffe2e8ed),);
        },
        itemCount: menus.length);
  }

ListView主要属性:
itemExtent: 此参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度);
itemCount: item条数;
itemBuilderseparatorBuilder的作用在上面代码中都能看得比较清晰;
其他的包括scrollDirectionreversecontrollershrinkWrapaddRepaintBoundariesaddAutomaticKeepAlives等等这些基本上和GridView是一样的,直接参考上面GridView就可以了。

SingleChildScrollView

单一子控件的滚动布局,跟Android的ScrollView基本一样,Android的ScrollView也是只能有一个子控件,不然就会报错。
基本使用很简单:

Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.vertical,
      reverse:false,
      controller:controller,
      child: Column(
        children: <Widget>[
            //...
        ],
      ),
    );
  }

弹窗

在Flutter中,对话框会有两种风格,调用showDialog()方法展示的是material风格的对话框,调用showCupertinoDialog()方法展示的是ios风格的对话框。
而这两个方法其实都会去调用showGeneralDialog()方法,可以从源码中看到最后是利用Navigator.of(context, rootNavigator: true).push()一个页面。
所以,反正都是跳一个页面,那么当需要自定义自己的弹窗的时候,直接自己写一个弹窗样式的页面,调用showDialog()跳过去就完了。
下面是两个基本的弹窗模板

SimpleDialog
void _showSimpleDialog(BuildContext context) {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          return SimpleDialog(
            title: Text('SimpleDialog'),
            titlePadding: EdgeInsets.fromLTRB(20, 20, 20, 10),
            contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 20),
            shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(8))), //圆角矩形
            children: <Widget>[
              SimpleDialogOption(
                child: Text('button1'),
                onPressed: () {
                  WidgetUtils.showToast('button1');
                },
              ),
              SimpleDialogOption(
                child: Text('button2'),
                onPressed: () {
                  WidgetUtils.showToast('button2');
                },
              )
            ],
          );
        });
  }

效果是这样:


SimpleDialog
AlertDialog
void _showAlertDialog(BuildContext context) {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('AlertDialog'),
            content: Text('这是一个AlertDialog'),
            titlePadding: EdgeInsets.fromLTRB(20, 20, 20, 10),
            contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 20),
            shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(8))),
            //圆角矩形
            actions: <Widget>[
              RaisedButton(
                child: Text('button1'),
                color: Colors.white,
                disabledColor: Colors.white,
              ),
              RaisedButton(
                child: Text('button2'),
                color: Colors.white,
                disabledColor: Colors.white,
              ),
            ],
          );
        });
  }

效果如下:


AlertDialog
BottomSheet

从底部弹窗,可以直接调用showModalBottomSheet方法:

showModalBottomSheet(
context: context,
  builder: (BuildContext context) {
    return Container(
        height: 150,
        color: Colors.white,
        child: Column(
          children: <Widget>[
            Container(
              child:FlatButton(child:Text('item1', style: TextStyle(fontSize: 17)),onPressed: (){},color: Colors.white,),
              alignment: Alignment.center,
              height: 50,
            ),
            Container(
              child:FlatButton(child:Text('item2', style: TextStyle(fontSize: 17)),onPressed: (){},color: Colors.white,),
              alignment: Alignment.center,
              height: 50,
            ),
            Container(
              child:FlatButton(child:Text('item3', style: TextStyle(fontSize: 17)),onPressed: (){},color: Colors.white,),
              alignment: Alignment.center,
              height: 50,
            ),
          ],
        ));
  }
)

上面的代码效果如下:


BottomSheet

按模板弹窗实际使用应该比较少,用到的属性也比较简单,看文档基本都能明白。
然后看看自定义弹窗,其实也很简单。直接调用showDialog()方法,里面的布局自己随意写,背景,样式,窗体内容,窗体位置等等等等,自由发挥。下面是一个简单的样板。

void _showMyDialog(BuildContext context) {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          return GestureDetector(
              onTap: () {
                Navigator.pop(context);//点击外部阴影 弹窗消失
              },
              child: Container(
                color: Colors.transparent,
                alignment: Alignment.center,
                child: GestureDetector(
                  onTap: () {},//点击弹窗主体,自定义事件,覆盖父类的点击事件,避免弹窗消失
                  child: Container(
                    width: 240,
                    height: 160,
                    color: Colors.white,
                    alignment: Alignment.center,
                    child: Text(
                      '我是自定义的diaolg',
                      style: TextStyle(fontSize: 15,color: Colors.black,decoration: TextDecoration.none),
                    ),
                  ),
                ),
              ));
        });
  }

效果:


自定义弹窗

由于自定义了点击事件,点击外部阴影弹窗会消失,点击内部控件,可以根据需求作各种处理,然后内部的内容可以自主控制,灵活方便。

尾声

本来以为三篇基本上可以列出常用控件,现在看来还不够。接下来还有Drawer,BottomNavigationBar、NestedScrollView配合TabBar等做一些常见效果,后面再继续梳理。这篇笔记先写到这里。
以上。

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