Flutter学习之认知基础组件

18,753 阅读8分钟

一、前言

前一天,学习了Dart语法,对Dart的语法和特性有了更深一步的了解。今天,来学习Flutter的基础控件,身为Android开发者都知道,一开始入坑Android就要熟悉学习其控件,如:TextViewImageViewButtonListViewRecycleView等。为什么要学习呢?因为平时的开发都离不开这些控件,UI的呈现都是有这些控件组成的,因此,其重要性就不用说了。对于Flutter来讲,基础控件(widget)就更加重要了。FlutterAndroid有所不一样,Android布局包含布局(RelativeLayout,LinearLayout,ConstrainLayou)和组件。Flutter的一切都是Widget,包括最顶层布局也是Widget,一个页面有很多很多的Widget组合而成,Widget也称为装饰品,窗口小部件。

二、Widget简介

Flutter里,UI控件就是WidgetWidget根据不同的功能可以分为结构元素(如按钮或菜单),文本样式(字体或者颜色方案),布局属性(如填充,对齐,居中),可以这么理解,一个flutter的页面是有一棵树型的Widget组成,包括根节点,树枝和树叶,全都是Widget,只是Widget嵌套Widget,那就可以用下面这张图来表示:

树形图
Flutter中,Widget是一切的基础,作为响应式渲染,属于MVVM的实现机制,通过修改数据,再用setState设置数据,Flutter会自动通过绑定的数据更新Widget,所以在平时开发中,开发者需要的就是实现Widget界面,和数据绑定起来。在平时,用的最多就是StatelessWidgetStatefulWidget这两种WidgetStatelessWidget表示无状态的,StatefulWidget表示有状态的。这里怎么理解呢?在Flutter中每个页面都是一帧,无状态就是保持在那一帧,总而言之就是不能跟用户交互,当有状态的Widget当数据更新时,其实是绘制了新的Widget,也就是UI发生了变化,只是State实现了跨帧数据同步保存。这里给大家说下,在Android Studio看源码的两个工具:

源码图
左边一栏Structure结构(看当前文件,win下的快捷键是(Alt+7))和右边Hierarchy继承关系(看当前类,win下快捷键是F4)都可以帮助你阅读源码。因为StatelessWidgetStatefulWidget用的最多,现在只需要用到这两个,就先学习这两个Widget

1.StatelessWidget

源码StatelessWidget只有三个方法:

StatelessWidget

  • const StatelessWidget({Key key}):super(key:key):初始化子类的[key]。这个key类是WidgetElementSemanticsNode唯一标识符,是用来控制Widget数中替换Widget的时候使用的。
  • StatelessElement createElement():创建一个[StatelessElement]来管理这个小部件在树中的位置,源码解释:子类重写此方法是不常见的,那这个方法也不用管,只需要知道这个方法用来管理自身在Widget树中的位置。
  • Widget build(BuildContext context):描述这部件呈现用户界面的部分。对于StatelessWidget,当Widget第一次插入到树中,或者父节点更改了配置和所依赖的[InheritedWidget]改变,都会被重新调用。

这里说下如何启动一个Flutter应用,并使用Flutter框架:

import 'package:flutter/material.dart';
void main() {
  return runApp(Widget app);
}

其实就是在main()函数中调用runApp函数。下面直接直接上例子,继承StatelessWidget,通过build方法返回一个控件:

import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial风格的小控件
void main(){
  //运行程序
  runApp(MyApp(null));
}
//继承无状态的StatelessWidget 使程序自身变为Wiget
class MyApp extends StatelessWidget{

  //要显示的内容
  final String text;

  //数据内容可以通过构造方法传递进来
  MyApp(this.text);

  //重写build方法 返回你需要的控件
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      //红色背景
      color: Colors.red,
      //高度 现在没用 会撑满整个屏幕
      height: 200,
      //宽度 运行效果会撑满整个屏幕
      width: 200,
      //内容居中
      alignment: Alignment.center,
      //Text控件
      child: new Text(
          //Dart语法中 ?? 表示如果text为空,就会返回??号的内容
          text ?? "my name is Knight",
        textDirection: TextDirection.ltr,//需要加上这句不然报 RichText widgets require a Directionality widget ancestor.
      ),

    );
  }
}

WidgetWidget之间通过child进行嵌套,有些Widget只能有一个child。就像上面的Container,有些Widget可以有多个child,像Colum布局。上面例子根布局是ContainerContainer嵌套了Text

2.StatefulWidget

什么是有状态的控件呢?状态是在创建控件可以同步读取信息,并且在控件的生命周期内可以改变,当控件状态发生改变时使用State.setState来及时更新,源码也是只有三个方法:

StatefulWidget
前两个方法和StatelessWidget一样的,而createState()这个方法源码注释是:在Widget树中给定的位置创建此可变状态的小部件,子类应该重写此方法返回新建的,关联子类的实例。当调用一个StatefulWidget,框架就会调用createState这个方法,当一个StatefulWidgetWidget树中移除,再次插入树中,那么会再次调用createState来创建一个新的State对象,这样做简化了State对象的生命周期。 需要创建管理的是主要是StateStatefulWidget用起来麻烦一些,他需要一个State,例子如下:

//继承StatefulWidget
class StateWidget extends StatefulWidget{
  
   @override
   State createState(){
     return _StateWidget();
   }
}



class _StateWidget extends State<StateWidget>{
  
  //重写build方法
  @override
  Widget build(BuildContext context){

  }
}

简单观察上面代码,大致流程还是和StatelessWidget一样的,build方法照样返回Widget,不过在StatefulWidget将这个方法放在createState里面。这里细想一下,也知道为什么要这样做,因为当状态改变,就会回调createState方法,重新调用build方法重新创建UI,下面通过每两秒改变UI这个例子来加深理解:

import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial风格的小控件
import 'dart:async';//记得导库
void main(){
  //运行程序
  runApp(StateWidget());
}
//控件继承State
class _StateWidget extends State<StateWidget>{
  int Number = 0;
  String text;
  //构造函数
  _StateWidget(this.text);

  @override
  void initState(){
    //初始化,这个函数在控件的生命周期内调用一次
    super.initState();
    print("进入initState");
    //3秒后改变text的内容
    new Future.delayed(const Duration(seconds: 3),(){
      setState(() {
        Number++;
        text = "已经改变数值,数值现在是$Number";
      });

    });
  }

  @override
  void dispose(){
    //销毁
    super.dispose();
    print('销毁');
  }

  @override
  void didChangeDependencies(){
    //在initState之后调
     super.didChangeDependencies();
     print('进入didChange');
  }

  //重写build方法
  @override
  Widget build(BuildContext context){
    return Container(
      //红色背景
      color: Colors.red,
      //内容居中
      alignment: Alignment.center,
      //Text控件
      child: new Text(
        //Dart语法中 ?? 表示如果text为空,就会返回??号的内容
        text ?? "没改变数值",
        textDirection: TextDirection.ltr,//需要加上这句不然报 RichText widgets require a Directionality widget ancestor.
      ),

    );
  }
}

上面例子可以知道知道:在State可以动态更改数据,在调用setState后,改变的数据会除法Widget重新构建,上面代码还写了三个生命周期方法,这里简单说一下:

  • initState:初始化操作
  • didChangeDependencies:在initState之后调用,可以获取其他State
  • dispose:销毁

平时开发中在build实现布局的摆放,把数据添加Widget,通过setState改变数据。那如果很高频率取改变数据,性能肯定受影响,以下三点可以减少重新构建有状态控件的影响:

  1. 树根上尽量不用状态控件,因为如果数据有变化树根每次都更新,那就是整棵树都要重建,把状态用在树叶上,这样更新的时候只会更新自己。
  2. 减少build方法所创建的节点数量和控件数量。
  3. 利用缓存,如果子树中不更改,将子树中缓存起来,每次使用其子树时重新使用它,学会重用思想。
  4. 尽可能使用const修饰控件。 怎么去选择有状态和无状态,最简单就是可以跟用户进行交互应该使用StatefulWidget,例如:点击,滑动屏幕信息流数据更新,如果只是仅仅显示数据,那就可以选择使用StatelessWidget创建一个无状态控件。

三、Flutter页面

Flutter有显示的Widget和完整页面呈现的Widget,常见的有MaterialAppScaffoldAppbarTextImageFlatButton,下面以表格形式简单列一下:

Flutter页面元素
下面一个个简单上例子介绍:

1.MaterialApp

import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial风格的小控件
void main(){
  //运行程序
  runApp(MyApp());
}

//用无状态控件显示
class MyApp extends StatelessWidget{
  
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      //标题
      title:'Widget_Demo',
      //主题色
      theme:ThemeData(
        //设置为蓝色
        primarySwatch: Colors.blue
      ),
      //这是一个Widget对象,用来定义当前应用打开的时候,所显示的界面
      home:MyHomePage(),
    );
  }
}


class MyHomePage extends StatelessWidget{
  
  @override
  Widget build(BuildContext context){
     return Scaffold(
       //设置appbar
       appBar:new AppBar(
         title:new Text('This is a Demo'),
       ),
       //主体
       body:new Center(
         //在屏幕中央显示一个文本
         child:new Text('Hello'),
       ),
     );
  }
}

效果如下图:

MaterialApp效果图
上面可以看到MaterialApp作为了主界面入口。

2.Scaffold

上面例子home:MyHomePage()这里返回了ScaffoldWidget,而这个Widget正是我们所看到的页面,看到Scaffold包含了appBarbody,一开始说到,Scaffold也包含Drawers,下面实现一下:

  @override
  Widget build(BuildContext context){
     return Scaffold(
       //设置appbar
       appBar:new AppBar(
         title:new Text('This is a Demo'),
       ),
       //主体
       body:new Center(
         //在屏幕中央显示一个文本
         child:new Text('Hello'),
       ),
       //左侧抽屉
       drawer:Drawer(
         //添加一个空的ListView
         child:ListView(),
       ),
     );
  }

效果如下:

抽屉实现一
下面往抽屉里添加点东西,就添加ListView,代码如下:

       //左侧抽屉
       drawer:Drawer(
         child:ListView(
           //设置padding
           padding:EdgeInsets.zero,
           children: <Widget>[
             //据说这里可以替换自定义的header
             //userHeader,
             ListTile(
               //标题内容
               title: Text("This is Item_one"),
               //前置图标
               leading: new CircleAvatar(child:new Icon(Icons.scanner),),
             ),
             ListTile(
               //标题内容
               title: Text("This is Item_two"),
               //前置图标
               leading: new CircleAvatar(child:new Icon(Icons.list),),
             ),
             ListTile(
               //标题内容
               title: Text("This is Item_three"),
               //前置图标
               leading: new CircleAvatar(child:new Icon(Icons.score),),
             ),
           ],
         ),
       ),

运行效果就是抽屉里加了三行内容的ListView

3.AppBar

下面设置一些AppBar属性,玩玩:

      //设置appbar
      appBar: new AppBar(
        //AppBar内容显示
        title: new Text('This is a Demo'),
        //前置图标
        leading: new Icon(Icons.home),
        //背景颜色 改为红色
        backgroundColor: Colors.red,
        //设置为标题内容居中
        centerTitle: true,
        //一个 Widget 列表,代表 Toolbar 中所显示的菜单,
        // 对于常用的菜单,通常使用 IconButton 来表示;对于不常用的菜单通常使用 PopupMenuButton 来显示为三个点,点击后弹出二级菜单
        actions: <Widget>[
          //IconButton
          new IconButton(
            //图标
            icon: new Icon(Icons.add_a_photo),
            //提示
            tooltip: 'Add photo',
            //点击事件
            onPressed: () {},
          ),
          //菜单弹出按钮
          new PopupMenuButton<String>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<String>>[
                new PopupMenuItem<String>(
                    value: "one", child: new Text('This one')),
                new PopupMenuItem<String>(
                    value: "two", child: new Text('This two')),
              ];
            },
            //选择点击事件
            onSelected: (String action) {
              switch (action) {
                case "one":
                //增加点击逻辑
                  break;
                case "two":
                //增加点击逻辑
                  break;
              }
            },
          ),
        ],
      ),

效果如下:

AppBar学习
可以看到,上面Appbar上加了前置图标、拍照图标、菜单弹出按钮、阴影。

4.Text

下面用Text来展示文本,把上面例子用文本显示中间的Hello单独抽出来,如下:

   //主体
 body: new Center(
      //在屏幕中央显示一个文本 改为自定义样式
      child: new CustomTextStyle('This is a Text'),
 ),
      
//单独文本样式
class CustomTextStyle extends StatelessWidget{
  String text;
  //构造函数 参数外部传进来
  CustomTextStyle(this.text);
  @override
  Widget build(BuildContext context){
    return Text(text ?? "Hello");

  }
}

下面把文本字体大小修改,字体样式修改,背景颜色改改:

//文本 : 单独文本样式
class CustomTextStyle extends StatelessWidget {
  Paint pg = Paint();
  String text;

  //构造函数 参数外部传进来
  CustomTextStyle(this.text);

  @override
  Widget build(BuildContext context) {
    //设置画笔颜色为黑色
    pg.color = Color(0xFF000000);
    return Text(
      text ?? "Hello",
      style: TextStyle(
          //颜色
          color: Colors.blue,
          //字体大小
          fontSize: 14,
          //字体加粗
          fontWeight: FontWeight.bold,
          //文本背景颜色
          background: pg),
    );
  }
}

上面效果是:

文本样式
还有很多的属性,根据需要去设置就行:

 const TextStyle({
    this.inherit = true,
    this.color,//文本样式
    this.fontSize,//字体大小
    this.fontWeight,//绘制文本时的字体粗细
    this.fontStyle,//字体变体
    this.letterSpacing,//水平字母之间的空间间隔(逻辑像素为单位),可以负值
    this.wordSpacing,//单词之间添加的空间间隔(逻辑像素为单位),可以负值
    this.textBaseline,//对齐文本的水平线
    this.height,//文本行与行的高度,作为字体代销的倍数
    this.locale,//用于选择区域定字形的语言环境
    this.foreground,//文本的前景色,不能与color共同设置
    this.background,//文本背景色
    this.shadows,//Flutter Decoration背景设定(边框,圆角,阴影,渐变等)
    this.decoration,//绘制文本装饰,添加上下划线,删除线
    this.decorationColor,//文本装饰的颜色
    this.decorationStyle,//文本装饰的样式,控制画虚线,点,波浪线
    this.debugLabel,
    String fontFamily,//使用字体的名称
    String package,
  })

5.RichText

这是显示丰富样式的文本,这什么意思呢?Text只能显示一种样式的文字,如果想在一段文字中显示多种样式,就好像Android里面的SpannableString,就需要使用RichText,直接上例子:

//富文本样式
class RichWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RichText(
        text: TextSpan(
          text: 'This is RichText',
          style: new TextStyle(
            //false的时候不显示
              inherit: true,
              //字体大小
              fontSize: 16,
              //黑色
              color: Colors.black
          ),
          children: <TextSpan>[
            new TextSpan(
              text: 'Android艺术探索',
              style: new TextStyle(
                color: Colors.redAccent,
                //字体粗细
                fontWeight: FontWeight.bold,
              ),

            ),

            new TextSpan(text: '第一行代码'),
            new TextSpan(
              text: 'Android进阶之光',
              style: new TextStyle(
                color: Colors.indigo,
                //字体样式
                fontSize: 20,
              ),
            )
          ],
        )
    );
  }
}

//屏幕中间改为富文本widget
  //主体
      body: new Center(
        //Text在屏幕中央显示一个文本 改为自定义样式
        //child: new CustomTextStyle('This is a Text'),
        //富文本
          child:new RichWidget()
      ),

效果如下:

富文本

6.TextField

下面看看文本输入框,文本输入框平时会经常用到:

body: new Center(
   //Text在屏幕中央显示一个文本 改为自定义样式
   //child: new CustomTextStyle('This is a Text'),
   //富文本
   //child:new RichWidget()
   //文本输入框
     child:new TextFieldWidget()
),

//文本输入框
class TextFieldWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
      return TextField();
  }
}

上面例子只能输入文本内容,如果想要获取输入框内容,就要添加一个controller,通过这个controller添加通知来获取TextField的值,我们一般点击按钮或者需要跟后台交互就要读取controller.text的值:

class MyHomePage extends StatelessWidget {
  //获取TextEditingController
  final editController = TextEditingController();
            //IconButton
          new IconButton(
            //图标
            icon: new Icon(Icons.add_a_photo),
            //提示
            tooltip: 'Add photo',
            //点击事件
            onPressed: () {
              //输出
              print('text inputted: ${editController.text}');
              //Toast
              Fluttertoast.showToast(
                  msg:'text inputted: ${editController.text}',
                  toastLength: Toast.LENGTH_SHORT,
                  gravity: ToastGravity.CENTER,
                  timeInSecForIos: 1,
              );
            },
          ),
     ....
       //主体
      body: new Center(
        //Text在屏幕中央显示一个文本 改为自定义样式
        //child: new CustomTextStyle('This is a Text'),

        //富文本
        //child:new RichWidget()

        //文本输入框 以构造函数传递controller
          child:new TextFieldWidget(editController)
      ),
}
//文本输入框
class TextFieldWidget extends StatelessWidget{

  final controller;
  //构造函数传值
  TextFieldWidget(this.controller);
  @override
  Widget build(BuildContext context){
      return TextField(
        controller: controller,
      );
  }
}

注意上面用到了ToastToast库这里很简单需要两步:

  1. pubspec.yaml添加依赖库fluttertoast: ^2.1.1
  2. 导入import 'package:fluttertoast/fluttertoast.dart';

重新运行即可,热重载可能会出现异常。运行在iOS模拟器需要装brewCocoaPods,有问题运行flutter doctor,它真是如名字一样,就是帮你诊断有没有错误信息,会显示具体信息。效果如下:

TextField输入框
下面改一下样式:

      return TextField(
        controller: controller,
        //最大长度,右下角会显示一个输入数量的字符串
        maxLength: 26,
        //最大行数
        maxLines: 1,
        //是否自动更正
        autocorrect: true,
        //是否自动对焦
        autofocus: true,
        //设置密码 true:是密码 false:不是秘密
        obscureText: true,
        //文本对齐样式
        textAlign: TextAlign.center,

      );

效果如下:

TextField样式

7.Image

Image很好理解就是在界面上区域显示一张图片,而这张图片的来源可以是:本地,网络,资源图片等。下面一一演示一下:

7.1.项目图片资源

首先新建一个资源目录:

配置资源目录
pubspec.yaml中配置图片路径,来识别应用程序所需的assets:

配置资源图片

class MyHomePage extends StatelessWidget {
       //主体
      body: new Center(
        .....
        //图片加载
        child:new ImageWidget()
      ),
}
//图片
class ImageWidget extends StatelessWidget{
    @override
    Widget build(BuildContext context){
      //项目资源图片 方式一
      return Image(
        image: new AssetImage('images/Image_fluttericon.jpeg'),
      );
      //项目资源图片 方式二
//    return Image.asset('images/Image_fluttericon.jpeg');
    }
}

效果如下:

项目本地资源

7.2.网络图片加载

下面进行网络图片加载,也是很简单:

class MyHomePage extends StatelessWidget {
  //图片路径
  String image_url = "https://ws1.sinaimg.cn/large/0065oQSqgy1fze94uew3jj30qo10cdka.jpg";
       //主体
      body: new Center(
        .....
        //图片加载
        child:new ImageWidget(image_url)
      ),
}
//图片
class ImageWidget extends StatelessWidget{
    String image_url;
    ImageWidget(this.image_url);
    @override
    Widget build(BuildContext context){
        return Image.network(image_url);
    }
}

效果如下:

网络加载图片
下面用一个库来加载和缓存网络图像,也可以与占位符和错误小部件一起使用,在pubspec.yaml添加依赖cached_network_image: ^0.4.1+1,在Dart文件导入这个库import 'package:cached_network_image/cached_network_image.dart';

//图片
class ImageWidget extends StatelessWidget{

    String image_url;
    ImageWidget(this.image_url);
    @override
    Widget build(BuildContext context){
        return new CachedNetworkImage(
            imageUrl: image_url,
            //占位符
            placeholder: new CircularProgressIndicator(),
            //加载错误时显示的图片
            errorWidget: new Icon(Icons.error),
            //宽高
            width:200,
            height: 200,
        );
    }
}

当图片还没加载出来的时候会显示占位符,当如果加载出错会显示errorWidget的图片。

7.3.声明分辨率相关的图片

另外Flutter可以为当前设备添加合适其分辨率的图像,其实对于Android原生来说,就是在不同分辨率目录下放置不同分辨率的图片,只不过flutter并不是创建drawable-xxdpi文件,而是创建以下文件夹:

.../logo.png
.../Mx/logo.png
.../Nx/logo.png

其中M和N是数字标识符,对应于其中包含的图像分辨率,它们指定不同素设备像比例的图片,主资源默认对应于1.0倍的分辨率图片。看下面例子:

不同分辨率的图
在设备像素比率为1.8的设备上,images/2.0x/logo.png 将被选择。对于2.7的设备像素比率,images/3.0x/logo.png将被选择。如果未在Image控件上指定渲染图像的宽度和高度,以便它将占用与主资源相同的屏幕空间量(并不是相同的物理像素),只是分辨率更高。 也就是说,如果images/logo.png是72px乘72px,那么images/3.0x/logo.png应该是216px乘216px; 但如果未指定宽度和高度,它们都将渲染为72像素×72像素(以逻辑像素为单位)。pubspec.yaml中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到的顺序去选择,也就是说1.0x中没有的话会在2.0x中找,2.0x中还没有的话就在3.0x中找。

      return Image(
        // 系统会根据分辨率自动选择不同大小的图片
        image: AssetImage('images/logo.png'),
        // ...
      ),

8.FlatButton

Flutter预先定义了一些按钮控件,如FlatButtonRaisedButtonOutlineButtonIconButton

  1. FlatButton:扁平化按钮,继承自MaterialButton
  2. RaisedButton:凸起按钮,继承自MaterialButton
  3. OutlineButton:带边框按钮,继承自MaterialButton
  4. IconButton:图标按钮,继承自StatelessWidget

下面看看FlatButton,其他的只是样式稍微不一样,大致用法一样。

//按钮
class FlatButtonWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
      return FlatButton(
        onPressed: (){
          Fluttertoast.showToast(
            msg:'你点击了FlatButton',
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
          );
        },
        child: Text('FlatButton'),
        color: Colors.blue,//按钮背景色
        textColor: Colors.white,//文字的颜色
        onHighlightChanged: (bool b){//水波纹变化回调

        },
        disabledColor: Colors.black,//按钮禁用时的显示的颜色
        disabledTextColor: Colors.black38,//按钮被禁用的时候文字显示的颜色
        splashColor: Colors.white,//水波纹的颜色
      );

  }
}

上面也设置了一些属性,效果图如下:

FlatButton

四、Flutter布局

Flutter中拥有30多种预定义的布局widget,常用的有ContainerPaddingCenterFlexRowColumListViewGridView。用一个表格列出它们的特性和使用。

Flutter布局
下面一一介绍简单用法:

1.Container

一个拥有绘制、定位、调整大小的widget,示意图如下:

Container示意图
下面直接上例子:

class MyHomePage extends StatelessWidget {
  ....
  body:new ContainWidget(),
  ...
}
//Container布局
class ContainWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
     return Container(
       child:Text("My name is Knight"),
       color: Colors.indigo,
       width:200,//宽
       height:200,//高
       margin:EdgeInsets.fromLTRB(5,5,5,5),//设置外边距
       padding:EdgeInsets.all(30),//内边距
     );
  }
}

下面设置边框,添加圆角:

//Container布局
class ContainWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
     return Container(
       ....
       padding:EdgeInsets.all(30),//内边距
       decoration: BoxDecoration(//设置边框
         //背景色
         color:Colors.redAccent,
         //圆角
         borderRadius: BorderRadius.circular(6),
       ),
     );
  }
}

运行效果如下:

Contianer圆角

2.Padding

一个Widget,会给其子Widget添加指定的填充,示意图如下:

Padding示意图

class MyHomePage extends StatelessWidget {
  ....
  body: new PaddingWidget(),
  ...
}
//Padding布局
class PaddingWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Padding(
      //设置左上右下内边距为4,10,6,8
      padding:EdgeInsets.fromLTRB(4, 10, 6, 8),
      child: Text('My name is Knight'),
    );
  }
}

效果图如下:

Padding效果图
下面实现Container嵌套Padding:

//Container嵌套Padding
class ContainPaddWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
      return Container(
        width:200,//宽
        height:200,//高
        child: Padding(
          padding:EdgeInsets.fromLTRB(4, 10, 6, 8),
          child: Text("My name is Knight"),
        ),
        decoration: BoxDecoration(//设置边框
          //背景色
          color:Colors.redAccent,
          //圆角
          borderRadius: BorderRadius.circular(6),
        ),
      );
  }
}

效果图如下:

Container嵌套Padding

3.Center

将其子widget居中显示在自身内部的widget,示意图:

Center示意图

//Center
class CenterWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Container(
      width:200,//宽
      height:200,//高
      child: Center(
        child: Text("My name is Knight"),
      ),
      decoration: BoxDecoration(//设置边框
        //背景色
        color:Colors.redAccent,
        //圆角
        borderRadius: BorderRadius.circular(6),
      ),
    );
  }
}

运行效果如下:

Center运行效果图
Center作为Container的孩子,Text所以在布局的中间。

4.Stack

可以允许其子Widget简单的堆叠在一起,层叠布局,示意图:

Stack示意图
下面直接上代码,把之前的布局全部用上试试:

class MyHomePage extends StatelessWidget {
  ....
   body:new Center(
              child:new StackWidget()
            ),
  ...
}
//层叠布局
class StackWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Stack(
      children: <Widget>[
          new Image.network('https://ws1.sinaimg.cn/large/0065oQSqgy1fze94uew3jj30qo10cdka.jpg',
            width:300.0,//宽
            height:300.0,//高
            ),
        new Opacity(
          opacity: 0.6,//不透明度
          child:new Container(
            width:100.0,
            height:100.0,
            color:Colors.redAccent,
          ),
        ),
        new Opacity(
          opacity: 0.6,
          child:new Container(
            width: 200.0,
            height:200.0,
            color:Colors.indigo,
          ),
        ),
      ],
    );
  }
}

运行效果:

stack运行效果图
可以看到控件都按Stack左上角对齐,叠在一起,下面改一下显示位置:

//层叠布局
class StackWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Stack(
      //Aliginment的范围是[-1,1],中心是[0,0].注释有写
      //和Android一样,左移的取值是往1取,右移是往-1取
      //这里注意,它是取stack里范围最大的布局为基准,下面是以Container为//基准对齐
      alignment: new Alignment(-0.6, -0.6),
     ...
    );
  }
}

运行效果图:

Stack运行效果图二

5.Colum

在垂直方向上排列子Widget,示意图如下:

Column示意图
直接上代码:

class MyHomePage extends StatelessWidget {
    ...
    body:new ColumnWidget(),
    ....
    
}
//Column布局
class ColumnWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          color:Colors.blue,
          width: 50,
          height: 50,
        ),
        Container(
          color:Colors.black,
          width:50,
          height:50,
        ),
        Container(
          color:Colors.green,
          width:50,
          height:50,
        ),
      ],
    );
  }
}

运行效果:

Column效果图
下面简单设置一下排列方式属性:

return Column(
      //设置垂直方向的对齐方式
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      ...
    );

运行效果如下:

Column运行效果图二
垂直方向(主轴上)属性:

Column垂直属性

  1. MainAxisAlignment.start这是默认值:垂直方向顶部对齐
  2. MainAxisAlignment.end:垂直方向底部对齐
  3. MainAxisAlignment.center:垂直方向居中对齐
  4. MainAxisAlignment.spaceBetween:垂直方向平分剩余空间
  5. MainAxisAlignment.spaceAround:放置控件后,剩余空间平分成n份,n是子widget的数量,然后把其中一份空间分成2份,放在第一个child的前面,和最后一个child的后面,也就是子widget的之前之后之间均匀分割空闲的一半空间
  6. MainAxisAlignment.spaceEvenly:放置控件后,把剩余空间平分n+1份,然后平分所有的空间,在子widget之前之后之间均匀的分割空闲的空间

下面列一下水平方向(交叉轴)的属性:

Column水平方向

  1. CrossAxisAlignment.center这是默认值,水平居中
  2. CrossAxisAlignment.end:水平方向右侧对齐
  3. CrossAxisAlignment.start:水平方向左侧对齐
  4. CrossAxisAlignment.stretch:水平方向拉伸子child填充满布局
  5. CrossAxisAlignment.baseline:和textBaseline一起使用

6.Row

在水平方向上排列子widget的列表,示意图:

Row示意图
直接上代码:

class MyHomePage extends StatelessWidget {
  ....
  body:new RowWidget(),
  ...
}
//Row
class RowWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Row(
      children: <Widget>[
        Container(
          color:Colors.blue,
          width: 50.0,
          height:50.0,
        ),
        Container(
          color:Colors.black,
          width:50.0,
          height:50.0,
        ),
        Container(
          color:Colors.green,
          width:50.0,
          height:50.0,
        ),
      ],
    );
  }
}

效果图:

Row运行效果图
下面简单设置一些属性,和Column没多大差别:

return Row(
    //把剩余空间平分n+1份,然后平分所有的空间
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    ...
 );

效果图:

Row运行效果图
水平方向上(主轴上)属性:

Row上主轴属性

  1. MainAxisAlignment.start这是默认值,水平方向顶部对齐
  2. MainAxisAlignment.center:水平方向居中对齐
  3. MainAxisAlignment.end:水平方向底部对齐
  4. MainAxisAlignment.spaceBetween:水平方向上平分剩余空间
  5. MainAxisAlignment.spaceAround:放置控件后,剩余空间平分成n份,n是子widget的数量,然后把其中一份空间分成2份,放在第一个child的前面,和最后一个child的后面,也就是子widget的之前之后之间均匀分割空闲的一半空间
  6. MainAxisAlignment.spaceEvenly:放置控件后,把剩余空间平分n+1份,然后平分所有的空间,在子widget之前之后之间均匀的分割空闲的空间 而交叉轴(垂直方向)的属性:

Row上垂直方向

  1. CrossAxisAlignment.center这是默认,垂直居中
  2. CrossAxisAlignment.end:垂直方向右侧对齐
  3. CrossAxisAlignment.start:垂直方向左侧对齐
  4. CrossAxisAlignment.stretch:垂直方向拉伸子child填充满布局
  5. CrossAxisAlignment.baseline:和textBaseline一起使用

7.Expanded

Expanded组件可以使RowColumnFiex等子组件在其主轴上方向展开并填充可用的空间,这里注意:Expanded组件必须用在RowColumnFiex内,并且从Expanded到封装它的RowColumnFlex的路径必须只包括StatelessWidgets或者StatefulWidgets(不能是其他类型的组件,像RenderObjectWidget,它是渲染对象,不再改变尺寸,因此Expanded不能放进RenderObjectWidget),示意图如下:

Expanded示意图

class MyHomePage extends StatelessWidget {
  ....
  body:new RowWidget(),
  ...
}
class RowWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Row(
        children: <Widget>[
          new RaisedButton(
              onPressed: (){

              },
              color:Colors.green,
              child:new Text('绿色按钮1')
          ),
          new Expanded(
            child:new RaisedButton(
              onPressed: (){

              },
              color:Colors.yellow,
              child:new Text('黄色按钮2')
            ),
          ),
          new RaisedButton(
              onPressed:(){

              },
              color:Colors.red,
              child:new Text('黑色按钮3')),
      ],
    );
  }
}

运行效果如下:

Expanded效果图

class MyHomePage extends StatelessWidget {
  ....
  body:new RowWidget(),
  ...
}
class RowWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Row(
        children: <Widget>[
         Expanded(
         child:Container(
           color:Colors.green,
           padding:EdgeInsets.all(8),
           height: 40.0,
         ),
         flex:1,
       ),
       Expanded(
         child:Container(
           color:Colors.yellow,
           padding:EdgeInsets.all(8),
           height: 40.0,
         ),
         flex:2,
       ),
       Expanded(
         child:Container(
           color:Colors.red,
           padding:EdgeInsets.all(8),
           height: 40.0,
         ),
       ),
      ],
    );
  }
}

上面代码设置了flex,将一行的宽度分成四等分,第一、三child占1/4的区域,第二个child占1/2区域。 效果如下:

Expanded运行效果图二

8.ListView

我相信这个布局在平时开发会经常用到,这是可滚动的列表控件,ListView是最常用的滚动widget,它在滚动方向上一个接一个地显示它的孩子。在纵轴上,孩子没被要求填充ListView,并且内置ListTitle,示意图如下:

ListView示意图

class MyHomePage extends StatelessWidget {
  ....
  body: new ListViewWidget(
          new List<String>.generate(1000,(i){
            return 'Item &i';
          }),
      ),
  ...
}
//ListView
class ListViewWidget extends StatelessWidget {
  final List<String> items;
  ListViewWidget(this.items);
  @override
  Widget build(BuildContext context) {
    return new ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return new ListTile(
          title: new Text('This is $index'),
        );
      },
    );
  }
}

效果图如下:

垂直的ListView
下面设置水平的ListView:

class MyHomePage extends StatelessWidget {
  ....
 body: new ListViewWidget(                       
  new List<String>.generate(1000, (i) {         
    return 'Item &i';                           
  }),                                            
),                                          
  ...
}
  Widget build(BuildContext context) {      
   return new ListView.builder(            
     itemCount: items.length,              
     //设置水平方向                              
     scrollDirection:Axis.horizontal,      
     //竖直时:确定每一个item的高度                    
     //水平时:确定每一个item的宽度 得要设置 不然不显示         
     itemExtent: 110.0,                    
     itemBuilder: (context, index) {       
       return new ListTile(                
         title: new Text('This is $index'),
       );                                   
     },                                    
   );                                        

效果如下:

ListView水平效果

9.GridView

GridView是一个网格布局的列组件。GridView继承至CustomScrollView,示意图如下:

网格示意图
直接竖直上例子:

//GridView                                           
class GridViewWidget extends StatelessWidget{        
                                                     
  @override                                          
  Widget build(BuildContext context){                
      return new GridView.count(                     
          crossAxisCount: 3, //3列                        
          children: List.generate(40,                
      (i){                                           
           return Card(                              
             child: Center(                          
               child:Text('This is $i'),             
             ),                                       
           );                                         
      })                                              
      );                                              
  }                                                  
}                                                    

垂直的网格
下面上水平例子:

    return new GridView.count(         
      //3行                             
      crossAxisCount: 3,               
      //设置水平                           
      scrollDirection: Axis.horizontal,
      children: List.generate(40, (i) {
        return Card(                   
          child: Center(               
            child: Text('This is $i'), 
          ),                            
        );                              
      }),                               
    );                                  

效果图如下:

水平的GridView

10.TabBar

移动开发中tab切换是一个很常用的功能,那么Flutter有没有提供这个Widget呢?答案是有的,Flutter通过Material库提供了很方便的API来使用tab切换。

10.1.创建TabController

TabBarViewTabBar都有一个TabController的参数,TabbarViewTabBar就是由TabController来控制同步,点击某个Tab后,要同步显示对应的TabBarView,创建TabController有两种方式:

  1. 使用系统自带的DefaultTabController,在Scaffold套一层DefaultTabController,这种方式TabBarView会自动查找这个tabController
  2. 自己定义一个TabController,实现SingleTickerProviderStateMixin

下面就列一下第一种方式:

 @override
  Widget build(BuildContext context) {
    return new DefaultTabController();
 }

10.2.构建Tab数据

final List<Tab> myTabs = <Tab>[
    new Tab(text: 'Android'),
    new Tab(text: 'IOS'),
    new Tab(text: 'Flutter'),
    new Tab(text: 'RN'),
    new Tab(text: 'Java'),
    new Tab(text: 'C'),
    new Tab(text: 'C++'),
    new Tab(text: 'Go'),
  ];

10.3.创建TabBar

TabBar在哪里都可以创建,在AppBar里有一个bottom参数可以接受TabBar,就放在AppBar下:

        //设置appbar
        appBar: new AppBar(
          //底部
          bottom: new TabBar(
            indicatorColor: Colors.red, //指示器颜色 如果和标题栏颜色一样会白色
            tabs: myTabs,//绑定数据
            isScrollable: true, //是否可以滑动
          ),
    ),

10.4.绑定TabBar和TabBarView

class MyHomePage extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
    new Tab(text: 'Android'),
    new Tab(text: 'IOS'),
    new Tab(text: 'Flutter'),
    new Tab(text: 'RN'),
    new Tab(text: 'Java'),
    new Tab(text: 'C'),
    new Tab(text: 'C++'),
    new Tab(text: 'Go'),
  ];
  @override
  Widget build(BuildContext context) {
    return new DefaultTabController(
      length: myTabs.length, //Tab长度
      child: new Scaffold(
        //设置appbar
        appBar: new AppBar(
          //底部
          bottom: new TabBar(
            indicatorColor: Colors.red, //指示器颜色 如果和标题栏颜色一样会白色
            tabs: myTabs,//绑定数据
            isScrollable: true, //是否可以滑动
          ),
         ....
        ),
          body: new TabBarView(
          //选中哪个Tabs,body就会显示
          children: myTabs.map((Tab tab) {
            return new Center(child: new Text(tab.text));
          }).toList(),
        ),
        ....
    );
  }
}

效果如下图:

TabBar效果图

11.BottomNavigationBar

BottomNavigationBar即是底部导航栏控件,显示在页面底部的设计控件,用于在试图切换,底部导航栏包含多个标签、图标或者两者搭配的形式,简而言之提供了顶级视图之间的快速导航。

11.1.构建底部标签

 //底部数据
  final Map bottomMap ={
    "首页":Icon(Icons.home),
    "朋友圈":Icon(Icons.camera),
    "信息":Icon(Icons.message),
    "其他":Icon(Icons.devices_other),
  };

11.2.创建导航栏

因为点击导航栏需要对应的字体显示,所以MyHomePage需要继承StatefulWidget,增加State

//用无状态控件显示
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //主题色
      theme: ThemeData(
          //设置为红色
          primarySwatch: Colors.red),
      //这是一个Widget对象,用来定义当前应用打开的时候,所显示的界面
      home: MyHomePageWidget(),
    );
  }
}

class MyHomePageWidget extends StatefulWidget{
  @override
  State<StatefulWidget> createState(){
     return new MyHomePage();
  }
}
class MyHomePage extends State<MyHomePageWidget> {
 //底部数据
  final Map bottomMap ={
    "首页":Icon(Icons.home),
    "朋友圈":Icon(Icons.camera),
    "信息":Icon(Icons.message),
    "其他":Icon(Icons.devices_other),
  };

  int _index = 0;
 bottomNavigationBar: BottomNavigationBar(
            items: (){
              var items = <BottomNavigationBarItem>[];
              bottomMap.forEach((k,v){
                items.add(BottomNavigationBarItem(
                  title:Text(k),//取map的值
                  icon : v,//取map的图标
                  backgroundColor:Colors.red,//背景红色
                ));
              });
              return items;
            }(),
             currentIndex: _index,//选中第几个
             onTap:(position){
               Fluttertoast.showToast(
                   msg: 'text inputted: $position',
                   toastLength: Toast.LENGTH_SHORT,
                   gravity: ToastGravity.CENTER,
                   timeInSecForIos: 1,
               );
               setState(() {
                 _index = position;//状态更新
               });
             }
            ),
   }

最终效果如下:

第三天最后成品

五、实践

下面实践Flutter中文网的例子:

第三天例子
先上布局分析图:

例子分析图

1.实现图像

再说一下如何配置图像

图像存放步骤

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return MaterialApp(
       home:new MyHomeWidget(),
    );
  }
}


class MyHomeWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
     return new Scaffold(
       //设置标题栏
       appBar: new AppBar(
           title:new Text('Flutter Demo'),
       ),
       //主体用ListView
       body:new ListView(
         children: <Widget>[
           //图片
           new Image.asset(
             'images/lake.jpg',
             width:600.0,
             height:240.0,
             //顺便设置图片属性
             fit:BoxFit.cover,
           )
         ],
       ),
     );
  }
}

2.实现标题栏

    //实现标题栏
     Widget titleWidget = new Container(
       //内边距
       padding:const EdgeInsets.all(30.0),
       //整体是一个水平的布局
       child:new Row(
         //只有一个孩子
         children: <Widget>[
           //用Expanded 会占用icon之外剩余空间
           new Expanded(
               //垂直布局 放置两个文本
               child: new Column(
                 //设置文本一起始端对齐
                 crossAxisAlignment: CrossAxisAlignment.start,
                 //有两个孩子
                 children: <Widget>[
                   new Container(
                     //底部内边距
                     padding:const EdgeInsets.only(bottom:10.0),
                     //孩子 设置字体样式
                     child:new Text(
                       'Oeschinen Lake Campground',
                       style: new TextStyle(fontWeight: FontWeight.bold),
                     ),
                   ),
                   new Text(
                     'Kandersteg, Switzerland',
                     style: new TextStyle(
                       color:Colors.grey[450],//设置颜色透明度
                     ),
                   )
                 ],
               ),
           ),
           new Icon(
             Icons.star,
             color:Colors.red[400],
           ),

           new Text('41'),
         ],
       ),
     );

3.实现按钮行

因为三个按钮样式都是一样的,所以抽取公共部分:

     /**
      * 抽取button行的代码复用
      *
      */
     Column getText(IconData icon,String text){
         return new Column(
           //聚集widgets
           mainAxisSize:MainAxisSize.min,
           //child居中
           mainAxisAlignment: MainAxisAlignment.center,
           children: <Widget>[
             new Icon(icon,color:Colors.blue[500]),
             new Container(
               //上部外边距
               margin: const EdgeInsets.only(top:8.0),
               //Text内容样式设定
               child:new Text(
                 text,
                 style:new TextStyle(
                   color:Colors.blue[500],
                 ),
               ),
             )
           ],

         );

     }

     /**
      * 按钮实现
      */
     Widget buttonWidget = new Container(
       //三列
       child:new Row(
         //用MainAxisAlignment.spaceEvenly平均分配子空间
         mainAxisAlignment: MainAxisAlignment.spaceEvenly,
         //孩子们
         children: <Widget>[
           getText(Icons.call, "CALL"),
           getText(Icons.near_me, "ROUTE"),
           getText(Icons.share, "SHARE"),
         ],
       ),
     );

4.实现文本

     /**
      * 文本实现
      */
     Widget textWidget = new Container(
       alignment: Alignment.center,
       //设置内边距
        padding:const EdgeInsets.all(10.0),
        child:new Text(
           'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, '
               'it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, '
               'followed by a half-hour walk through pastures and pine forest, '
               'leads you to the lake, which warms to 20 degrees Celsius in the summer. '
               'Activities enjoyed here include rowing, and riding the summer toboggan run.',
          // softWrap: true,//属性表示文本是否应在软换行符(例如句点或逗号)之间断开。
          // textAlign: TextAlign.center,
         ),

     );

5.整合

   return new Scaffold(
       //设置标题栏
       appBar: new AppBar(
           title:new Text('Flutter Demo'),
       ),
       //主体用ListView
       body:new ListView(
         children: <Widget>[
           //图片
           new Image.asset(
             'images/lake.jpg',
             width:600.0,
             height:240.0,
             //顺便设置图片属性
             fit:BoxFit.cover,
           ),
           //标题栏
           titleWidget,
           //按钮栏
           buttonWidget,
           //文本栏
           textWidget,
         ],

       ),
     );

运行效果图:

最后成果

六、总结

Flutter还有很多Widget上面没有说到,就只能自己有空再去学习了,下面直接上一张图,今天学到的内容:

Widget总结图

学习链接:flutterchina.club/widgets/

如有不正之处欢迎大家批评指正~