Flutter-Widget的基本使用

1,154 阅读10分钟

Flutter对UI控件的组织形式

  在Dart中强调万物皆对象,而Flutter则强调万物皆Widget,Flutter的重点是如何简洁快速地构建UI界面并能同时应用到多种终端设备上,因此Flutter把Widget看得格外重要,以至于Flutter的程序从整体上看来就是一层一层的Widget嵌套堆叠起来的。   Flutter项目自动生成了一个Hello World项目,基本功能是右下角有一个悬浮按钮,点击时中间的数字会显示点击了多少次。从代码层面看,对于Android来说,程序的入口还是一个Activity,这是看到AndroidManifest.xml中定义的就是MainActivity,MainActivity的onCreate方法中调用了GeneratedPluginRegistrant#registerWith方法,这里就会进到Flutter的环境,运行main.dart文件中的main函数。main函数的实现直接调用了runApp函数,传入的参数为一个MyApp对象。MyApp继承自StatelessWidget,从这里开始就全部是Widget了。   Flutter有两种Widget,分别是StatelessWidget和StatefulWidget,表示的是无状态控件和有状态控件。之所以要有这两种Widget,是因为Flutter希望将控件的状态管理与UI管理完全隔离开,State更像是Android中的ViewModel,把控件的状态字段声明在State中,而真正的布局创建则是在State对象的build函数中,StatefulWidget要求实现类实现createState方法返回一个State对象,而State的实现类需要继承自State类,并将StatefulWidget的实现类作为其范型参数,State的widget变量就表示State中持有的StatefulWidget对象。State实现类需要实现build方法,这跟StatelessWidget一样,需要返回一个Widget对象,也就是这个Widget要实现的UI布局。   和其他UI组件库一样,Flutter对每个Widget也提供了手势监听器,例如Demo项目中就为悬浮按钮添加了点击监听器,监听函数是_incrementCounter,在这个方法中调用了State的setState函数,传入了一个闭包,在闭包中将_counter自加1。这个setState有点像react中的setState函数,都是更改状态字段后会自动更新UI。

颜色的使用

Flutter有四种颜色的定义方式

Color c = const Color(0xFF42A5F5);//16进制的ARGB
Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5);
Color c = const Color.fromARGB(255, 66, 165, 245);
Color c = const Color.fromRGBO(66, 165, 245, 1.0);//opacity:不透明度

或者可以直接使用Flutter预定义的颜色,Colors类中定义了若干静态常量Color对象,可以在代码中使用,就像使用Android中的Color.BLUDE那样。

Text组件的使用

  Flutter的Text跟Android原生组件TextView类型,都是为了呈现简单文本的控件。Text继承自StatelessWidget,构造器参数列表为this.data, { Key key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, this.textScaleFactor, this.maxLines, this.semanticsLabel, this.textWidthBasis, } 其中style为TextStyle类型,可为文字定义前景色和背景色,textAlign表示文字对齐方式,而textDirection表示文字方向,只有两个取值rtl和ltr,分别表示从左到右排列和从右到左排列。maxLines表示最大行数。

Alignment

  Flutter用Alignment来表示对齐方式,还有继承自Alignment的FractionalOffset和与Alignment一样继承自AlignmentGeometry的AlignmentDirectional。其中Alignment构造器有x和y两个double参数,分别表示以组件中心为基准,x和y轴两个方向上的对齐,x负左正右,y负上正下,取值都是[-1,1];FractionalOffset则是以组件左上角为基准,这和Android中的标准坐标系一样,而参数则只有正数;AlignmentDirectional和Alignment比较相似,都是以组件的中心为基准,但和textDirection有关,默认textDirection为ltr,则负左正右,当textDirection为rtl时,则是负右正左了,但都是负上正下。

decoration相关

  作为decoration属性的value,UnderlineTabIndicator见名知意,一般用于Tab下面的指示线,比如说当哪个Tab为active时,则Tab的下方会有一条横线表示它被选中了,同时,这个decoration也可以被用于任何Container。这个在实际应用中,跟Text的下划线看起来差不多。
  decoration还可以选用BoxDecoration,表示四个边的边框,属性分别表示
color:颜色
image:图片
border:边框
borderRadius:圆角半径
boxShadow:阴影
gradient:渐变
backgroundBlendMode:背景融合模式
shape:形状

单容器控件

  Flutter的容器分两种,一种是多子Widget容器控件,类似于Android原生中的ViewGroup,其children属性是个数组。而另一类则是单子Widget容器控件,主要是为了方便子Widget管理样式,子Widget在其child属性中声明。
  最常用的单容器控件是Container,就是简单的容器,有属性
alignment:对齐方式
padding:内边距
width:宽度
height:高度
margin:外边距
color:背景颜色
decoration:背景样式,各种边框样式
foregroundDecoration:前景样式
constraints:宽高约束
transform:旋转平移等3D操作
child:子Widget demo见simpleContainerDemo
  Padding容器也比较简单,只能设置padding属性来确定内边距
  Center被用于快速确定子View位于父View的居中,并且还能用widthFactor和heightFactor来设置Center的高度和宽度,Center的width = 子控件的width* widthFactor,Center的 height = 子控件的 height* heightFactor
  Align是用于快速确定子View位于父View的对齐位置的,其中alignment可使用Alignment的值,并且Align也支持widthFactor和heightFactor属性来定制Align的size
  FittedBox用于确定填充类型
  AspectRatio用于确定固定宽高比
  Flutter中也有跟Android一样的约束布局的容器ConstrainedBox,可以设置最大或最小size,例如设置最小高度为50,就算height设置为30,最终高度也会是50

多容器控件

  既然有单容器控件就会有多容器控件,这类控件就跟Android原生中的ViewGroup一样,可以有多个child,其children是个数组。以排列方向为例,可分为Row和Column,分别是水平排列和竖直排列,也可以叫作行和列。Row和Column都有四个布局属性 mainAxisAligment:主轴对齐方式 crossAxisAlignment:次轴对齐方式 textDirection:排列方向,主要是指按序列是从左到右还是从右到左 verticalDirection:底部对齐还是顶部对齐 其中的主轴和次轴是相对而言的,Row的主轴是水平方向,次轴是竖直方向,而Column的主轴则是竖直方向,次轴是水平方向。mainAxisAligment和crossAxisAlignment属性的值都是MainAxisAlignment类型的,其中MainAxisAlignment有6种取值,分别是start、center、end、spaceBetween、spaceAround、spaceEvenly,而最后三种的区别是spaceBetween表示剩余区域平均分在各种子View中间,spaceAround表示剩余区域平均分给每个子View主轴两边,spaceEvenly表示每两个子View之间的间隔相等,包括最边上的子View与边的间距
  GridView是个表格控件,分为主轴和次轴,主轴是scrollDirection属性指定的方向,子控件由children属性给出,是个Widget的集合,GridView还可以设置次轴的插件个数,这样children会先铺满次轴,也就是在次轴方向上一行一行或者一列一列地铺。
  ListView跟Android原生中的ListView表现一致,但构造方式不一样,ListView也支持children,但如果想支持懒加载,则需要用到builder函数,通过index来构造子Widget。其中builder函数的itemExtent表示每一个item的高,如果不指定则高度由item自身决定,itemCount表示item的个数,index的范围是[0,itemCount),itemBuilder是一个函数,入参是BuildContext和index,返回值是一个Widget,当需要加载下一个item时,就会调用itemBuilder的函数。
  Expanded是一种空白容器,主要作用是撑开剩余空间,只能在Row、Column和Flex中使用。如果子控件的宽度或高度之和达不到父控件的,那Expanded就会占据所有的剩余空间。   ExpansionTile是个分组容器,就像QQ的好友列表一样,ExpansionTile主要分为标题部分和展开内容部分,标题部分又分为title标题文字和leading标题图标以及trailing标题的展开箭头。而内容区则是children属性,是个Widget的集合。backgroundColor控制背景色,onExpansionChanged监听打开关闭事件,initiallyExpanded表示初始时是展开还是折叠状态,默认是折叠状态。

图片

  Flutter的图片使用由Image类提供,Image提供了常用的图片加载static函数。Image.network用于加载网络图片,Image.asset用于加载asset的图片资源,Image.file用于加载sd card的图片,由于Flutter默认是不支持加载sd card文件的,所以需要使用path_provider库,而读写文件需要权限,Flutter又默认不支持权限请求,因此也要使用permission_handler。
  Flutter在pubspec.yaml文件中配置工程使用的一些属性,例如assets资源和dart依赖库。在flutter的assets下配置asset资源,可以直接配置资源所在的目录,例如在工程根目录下创建imgs目录用于存放asset图片资源,则可在assets下配置- imgs/,这样就可加载上imgs目录下的所有图片。而依赖库配置则是在dependencies下配置,默认已经配置好了flutter sdk,我们只需要再添加上我们所需要的第三方库即可。
  关于使用

Widget imgDemo() {
    _getLocalFile();
    return Column(children: [
      Row(children: <Widget>[Text("Image.network", style: demoTextStyle,), Image.network("http://pic1.win4000.com/pic/c/cf/cdc983699c.jpg", height: 200,)],),
      Row(children: <Widget>[Text("Image.asset", style: demoTextStyle,), Image.asset("imgs/girl.jpg", height: 200,)],),
      Row(children: <Widget>[Text("Image.file", style: demoTextStyle,), Image.file(File("$_storageDir/news_article/test.jpg"), height: 200,)],)
    ]);
  }

加载sd card的图片资源较为麻烦,是异步加载的,因此需要使用dart的异步编程,

Future<bool> _requestPermissions() async {
    Map<PermissionGroup, PermissionStatus> permissions =
        await PermissionHandler().requestPermissions([
      PermissionGroup.storage,
    ]);
    List<bool> results = permissions.values.toList().map((status) {
      return status == PermissionStatus.granted;
    }).toList();
    return !results.contains(false);
  }

  String _storageDir = '';
    _getLocalFile() async {
      if (!await _requestPermissions()) {
        return "";
      }
      String appDir = (await getApplicationDocumentsDirectory()).path;
      String tempDir = (await getTemporaryDirectory()).path;
      String storageDir = (await getExternalStorageDirectory()).path;
      setState(() {
        _storageDir = storageDir;
      });
      return storageDir;
  }

使用_storageDir来保存sd card根目录的值,在异步函数中获取到其值时,调用setState来同步给UI让界面刷新。这里还使用到了permission_handler来作动态权限请求。   除了普通图片,还有Icon我一般也认为是图片类,Icon的底层实现可能有多种,但说到底Icon还是一种图形,也应该被划到图片类中。Flutter的Icon由Icons提供,已经内置了非常多的Icon图标。更多Icon的查找可以前往material.io/tools/icons…

单选复选和开关控件

  Flutter的单选框是Radio,其有四个属性 value、groupValue,控制是否选中,当value=groupValue时为选中 onChanged选中状态变化时的回调 activeColor选中状态下的颜色 materialTapTargetSize表示点击区域 Radio不能给title,因此就有了RadioListTile,可以在Radio的右边添加上title

Widget radioDemo() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Radio(value: "red",
                groupValue: _radioValue,
                activeColor: Colors.red,
                onChanged: (value) { 
                  setState(() {
                    _radioValue = value;
                  });
                }
              ),
          Radio(value: "green",
                groupValue: _radioValue,
                activeColor: Colors.green,
                onChanged: (value) { 
                  setState(() {
                    _radioValue = value;
                  });
                }
              ),  
          Radio(value: "blue",
                groupValue: _radioValue,
                activeColor: Colors.blue,
                onChanged: (value) { 
                  setState(() {
                    _radioValue = value;
                  });
                }
              ),
          ],),
      Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
            RadioListTile<String>(value: "red", 
                          groupValue: _radioValue,
                          title: Text("red", style: demoTextStyle),
                          onChanged: (value) {
                            setState(() {
                              _radioValue = value;
                            });
                          }
            ),
            RadioListTile<String>(value: "green", 
                          groupValue: _radioValue,
                          title: Text("green", style: demoTextStyle),
                          onChanged: (value) {
                            setState(() {
                              _radioValue = value;
                            });
                          }
            ),
            RadioListTile<String>(value: "blue", 
                          groupValue: _radioValue,
                          title: Text("blue", style: demoTextStyle),
                          onChanged: (value) {
                            setState(() {
                              _radioValue = value;
                            });
                          }
            )
          ],
        )
    ],);
  }

  CheckBox和Radio的用法类似,但没有Radio的group,这是因为Radio是有分组的概念的,而CheckBox只能是单个控件。同样也提供了CheckboxListTile来为Checkbox增加title、subtitle和secondary

  var _checkBoxValue = true;
  var _checkBoxListTileValue = true;
  Widget checkBoxDemo() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text("$_checkBoxValue, $_checkBoxListTileValue", style: demoTextStyle,),
        Checkbox(value: _checkBoxValue, onChanged: (value) {
            setState(() {
              _checkBoxValue = value;
            });
        }),
        CheckboxListTile(value: _checkBoxListTileValue, title: Text("CheckboxListTile", style: demoTextStyle), activeColor: Colors.blue, selected: true, onChanged: (value) {
          setState(() {
            _checkBoxListTileValue = value;
          });
        })
      ],
    );
  }

  开关控件是Switch,总体分为track和thumb两部分,track表示Switch的底层,而thumb则表示开关的点击头部,Switch的诸多属性则是为了控制点击和track和thumb的样式。 value表示开关状态,只有true和false onChanged表示状态变化的回调 activeColor打开状态下的颜色,也即是打开状态下thumb的颜色 activeTrackColor打开状态下的track的颜色 inactivetrackColor关闭状态下tract的颜色 inactiveThumbColor关闭状态下的thumb的颜色 activeThumbImage打开状态下thumb的图片 inactiveThumbImage关闭状态下thumb的图片

var _switchValue = true;

  Widget switchDemo() {
    return Switch(value: _switchValue,
                  activeColor: Colors.blue,
                  activeTrackColor: Colors.green,
                  inactiveTrackColor: Colors.grey,
                  inactiveThumbColor: Colors.red,
                  inactiveThumbImage: AssetImage("imgs/girl.jpg"),
                  onChanged: (value) {
                    setState(() {
                      _switchValue = value;
                    });
                  }
    );
  }

滑动条和进度条

  滑动条是Slider而进度条是ProgressIndicator,Slider是在进度条的基础上添加了一个滑块,因此我们可以与滑动条交互,也就是拖动滑动条。Slider有如下一些属性 value表示滑动条当前的滑动块位置 onChanged表示值变化时的回调 onChangeStart开始滑动时回调一次 onChangeEnd滑动结束时回调一次 min表示最小值,max表示最大值 divisions表示整个滑动条分为几块,如果是5,则表示滑动条只能滑到5个位置 label表示一个文字提示,当滑动结果时,会在滑动条上显示一个气泡 activeColor表示滑动过的区域的颜色 inactiveColor表示未滑到的区域的颜色
  进度条主要有CircularProgressIndicator和LinearProgressIndicator两种,用法基本上一样,属性如下 backgroundColor表示未到达的区域颜色 valueColor表示到达的区域颜色 value表示进度值,为0-1的double值 以下代码是用Slider来同时控制两种进度条的值的demo

var _sliderValue = 0.0;

  Widget sliderProgressDemo() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Slider(value: _sliderValue,
          activeColor: Colors.green,
          inactiveColor: Colors.grey,
          min: 0.0,
          max: 1.0,
          divisions: 10,
          label: "$_sliderValue",
          onChanged: (value) {
            setState(() {
              _sliderValue = value;
            });
          }
        ),
        Container(height: 100,
          width: 100,
          child: CircularProgressIndicator(backgroundColor: Colors.yellow,
            value: _sliderValue,
            valueColor: AlwaysStoppedAnimation(Colors.green),
            semanticsLabel: "$_sliderValue",
            ),
        ),
        LinearProgressIndicator(backgroundColor: Colors.yellow,
          value: _sliderValue,
          valueColor: AlwaysStoppedAnimation(Colors.green),
          semanticsLabel: "$_sliderValue",)
      ],
    );
  }

  关于如何自定义这些控件的大小,由于这些控件都是match_parent的,因此可以自定义其父控件的大小,如果没有,则加一层父控件即可,例如上面的demo就给CircularProgressIndicator包了一层Container并设置了其宽高为100,因此CircularProgressIndicator的宽高就变成了100,这样就能自定义这些控件的大小尺寸了。

Dialog相关

  Flutter的Dialog都是基于showDialog函数来弹出的,showDialog函数有两个参数,分别是context和builder,context就是Widget的build函数的形参BuildContext,而builder参数是一个函数类型,形参是context,也是BuildContext类型的,返回值是Widget类型。Flutter SDK已经为我们实现了非常多的Dialog类型,都是继承自StatelessWidget,比如Android中常用的AlertDialog,其包括一个Text类型的title,一个Text类型的content,一个Widget数组类型的actions。AlertDialog是Android系统的Material风格的,对于iOS系统,常用的Dialog是CupertinoDialog,和AlertDialog的使用方式几乎一样,只不过是使用showCupertinoDialog。除了系统特定风格的Dialog,还可以直接使用Dialog来自定义布局,使用方式也跟AlertDialog一样,使用showDialog来弹出。
  除了使用showDialog函数来弹出,还有一种sheet类型的屏幕底部弹出框,使用showBottomSheet函数来弹出,使用方式跟showDialog类似,但要求其是在Scaffold的父Widget中使用,并且要求Scaffold的body是个Builder类型的

Widget dialogDemo(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      FlatButton(onPressed: () {
        showDialog(context: context, builder: (context) {
          return AlertDialog(
            title: Text("test", style: demoTextStyle),
            content: Text("test Content", style: demoTextStyle),
            actions: <Widget>[
              FlatButton(onPressed: () {
                Navigator.of(context).pop(true);
              }, child: Text("取消", style: demoTextStyle)),
              FlatButton(onPressed: () {
                
              }, child: Text("确认", style: demoTextStyle)),
            ],
          );
        });
      }, child: Text("Material Dialog", style: demoTextStyle,)),
      FlatButton(onPressed: () {
        showCupertinoDialog(context: context, builder: (context) {
          return CupertinoAlertDialog(
            title: Text("test", style: demoTextStyle),
            content: Text("test Content", style: demoTextStyle),
            actions: <Widget>[
              FlatButton(onPressed: () {
                Navigator.of(context).pop(true);
              }, child: Text("取消", style: demoTextStyle)),
              FlatButton(onPressed: () {
                
              }, child: Text("确认", style: demoTextStyle)),
            ],
          );
        });
      }, child: Text("Cupertino Dialog", style: demoTextStyle,)),
      FlatButton(onPressed: () {
        showDialog(context: context, builder: (context) {
          return Dialog(child: Row(
            children: <Widget>[
              Icon(Icons.warning),
              RaisedButton(onPressed: () {
                Navigator.of(context).pop(true);
              }, child: Text("cancel", style: demoTextStyle))
            ],
          ));
        });
      }, child: Text("custom Dialog", style: demoTextStyle,)),
      FlatButton(onPressed: () {
        showBottomSheet(context: context, builder: (context) {
          return Container(
            height: 200,
            width: double.infinity,
            child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              FlatButton(onPressed: () {

              }, child: Text("1", style: demoTextStyle)),
              FlatButton(onPressed: () {
                
              }, child: Text("2", style: demoTextStyle)),
              FlatButton(onPressed: () {
                
              }, child: Text("3", style: demoTextStyle))
            ],
          ),);
        });
      }, child: Text("bottom sheet", style: demoTextStyle,))
    ],);
  }

@override
  Widget build(BuildContext context) {
    // return colorDemo;
    return MaterialApp(
      title: "study",
      home: Scaffold(
        body: Builder(builder: (context) {
            return Center(
            child: Container(
              color: Colors.white,
              child: dialogDemo(context)
            ,),
          );
        }) 
      )
    );
  }