美文网首页跨平台
Flutter了解之常用组件

Flutter了解之常用组件

作者: 平安喜乐698 | 来源:发表于2020-10-25 15:54 被阅读0次
    目录
    
      1. 文本(Text组件、TextSpan组件)
      2. 按钮
      3. 图片、ICON
      4. 表单(TextField、Radio、Switch、CheckboxListTitle、RadioListTitle、SwitchListTitle、Slide、Form)
      5. 进度指示器(LinearProgressIndicator、CircularProgressIndicator)
      6. 日期组件
      7. 轮播图(三方库: flutter_swiper)
      8. 弹框(对话框)
      9. AspectRatio  宽高比组件
    

    1. 文本

    1. Text (用于显示文本)属于基础组件库
    1. textAlign
    文本的对齐方式(左对齐left、右对齐right、居中center、两端对齐justfy)
    对齐的参考系是Text widget本身,因此只有Text宽度大于文本内容长度时才有意义。
    
    
    2. maxLines、overflow:
    文本显示的最大行数,默认情况下文本自动折行。
    如果有多余的文本,可以通过overflow来指定截断方式。TextOverflow.clip直接截断,TextOverflow.ellipsis会将多余文本截断后以省略符“...”表示。
    
    
    3. textScaleFactor:
    文本相对于当前字体大小的缩放因子。
    该属性的默认值可以通过MediaQueryData.textScaleFactor获得,如果没有MediaQuery,那么会默认值将为1.0。
    
    
    4. style(TextStyle):
    用于指定文本显示的样式(如颜色、字体、粗细、背景等)
    说明:
      1. height:指定行高,不是一个绝对值而是一个因子,具体的行高等于fontSize*height。
      2. fontFamily :由于不同平台默认支持的字体集不同,所以在手动指定字体时一定要先在不同平台测试一下。
      3. fontSize:该属性和Text的textScaleFactor都用于控制字体大小。但是有两个主要区别:
        1. fontSize可以精确指定字体大小,而textScaleFactor只能通过缩放比例来控制。
        2. textScaleFactor主要是用于系统字体大小设置改变时对Flutter应用字体进行全局调整,而fontSize通常用于单个文本,字体大小不会跟随系统字体大小变化。
      4. color:字体颜色
      5. fontWeight:字体粗细。FontWeight.w800
      6. fontStyle:字体样式。FontStyle.italic 倾斜
      7. decoration:文本装饰线(none没有线,lineThrough删除线,overline上划线,underline下滑线)
      8. decorationColor:文本装饰线颜色
      9. decorationStyle:文本装饰线样式([dashed,dotted]虚线  double两根线  soild一根实现  wavy波浪线)
      10. wordSpacing: 单词间隙(负值则紧凑)
    
    
    5. textDirection:
    文本方向。ltr从左至右,rtl从右至左。
    

    Text("Hello world! I'm Jack. "*4,
      textAlign: TextAlign.left,
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
      textScaleFactor: 1.5,
    );
    
    Text("Hello world",
      style: TextStyle(
        color: Colors.blue,
        fontSize: 18.0,
        height: 1.2,  
        fontFamily: "Courier",
        background: new Paint()..color=Colors.yellow,
        decoration:TextDecoration.underline,
        decorationStyle: TextDecorationStyle.dashed
      ),
    );
    
    1. TextSpan (富文本)

    对一个Text内容的不同部分按照不同的样式显示

    const TextSpan({
      TextStyle style, 
      Sting text,
      List<TextSpan> children,
      GestureRecognizer recognizer,
    });
    
    说明:
      1. style:样式。
      2. text:内容。 
      3. children:一个TextSpan的数组,也就是说TextSpan可以包括其他TextSpan。
      4. recognizer:用于对该文本片段上用于手势进行识别处理。
    

    通过TextSpan实现了一个基础文本片段和一个链接片段,然后通过Text.rich 方法将TextSpan 添加到Text中,之所以可以这样做,是因为Text其实就是RichText的一个包装,而RichText是可以显示多种样式(富文本)的widget。
    
    Text.rich(TextSpan(
        children: [
         TextSpan(
           text: "Home: "
         ),
         TextSpan(
           text: "https://flutterchina.club",
           style: TextStyle(
             color: Colors.blue
           ),  
           recognizer: _tapRecognizer
         ),
        ]
    ))
    

    DefaultTextStyle

    在Widget树中,文本的样式默认是可以被继承的(子孙类文本类组件未指定具体样式时可以使用Widget树中父级设置的默认样式)。
    因此,如果在Widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式,而DefaultTextStyle正是用于设置默认文本样式的。
    
    例
    
    首先设置了一个默认的文本样式,即字体为20像素(逻辑像素)、颜色为红色。然后通过DefaultTextStyle 设置给了子树Column节点处,这样一来Column的所有子孙Text默认都会继承该样式,除非Text显示指定不继承样式,如代码中注释2。
    
    DefaultTextStyle(
      //1.设置文本默认样式  
      style: TextStyle(
        color:Colors.red,
        fontSize: 20.0,
      ),
      textAlign: TextAlign.start,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text("hello world"),
          Text("I am Jack"),
          Text("I am Jack",
            style: TextStyle(
              inherit: false, //2.不继承默认样式
              color: Colors.grey
            ),
          ),
        ],
      ),
    );
    

    字体

    可以在Flutter应用程序中使用不同的字体 (自定义字体、第三方字体)。
    
    步骤:
    1. 在pubspec.yaml中声明(以确保字体会打包到应用程序中)。
    flutter:
      fonts:
        - family: Raleway  // family 是字体的名称, 用于 TextStyle的 fontFamily 属性中使用.
          fonts:
            - asset: assets/fonts/Raleway-Regular.ttf  // asset 是相对于 pubspec.yaml 文件的字体路径
            - asset: assets/fonts/Raleway-Medium.ttf
              weight: 500  // 指定字体的粗细,取值范围是100到900之间的整百数(100的倍数). 对应TextStyle的FontWeight属性
              style: italic  // 指定字体是倾斜还是正常,对应的值为italic和 normal. 对应TextStyle的 fontStyle TextStyle 属性
            - asset: assets/fonts/Raleway-SemiBold.ttf
              weight: 600
        - family: AbrilFatface
          fonts:
            - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
    2. 通过style(TextStyle)使用字体。
    // 声明文本样式
    const textStyle = const TextStyle(
      fontFamily: 'Raleway',
    );
    // 使用文本样式
    var buttonText = const Text(
      "Use the font for this text",
      style: textStyle,
    );
    
    Package中的字体
    
    
    要使用Package中定义的字体,必须提供package参数。如果在package包内部使用它自己定义的字体,也应该在创建文本样式时指定package参数。
    const textStyle = const TextStyle(
      fontFamily: 'Raleway',
      package: 'my_package', //指定包名
    );
    
    
    一个包也可以只提供字体文件而不需要在pubspec.yaml中声明。 这些文件应该存放在包的lib/文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件:lib/fonts/Raleway-Medium.ttf
    然后,应用程序可以声明一个字体,如下面的示例所示(lib/是隐含的,所以它不应该包含在asset路径中。):
     flutter:
       fonts:
         - family: Raleway
           fonts:
             - asset: assets/fonts/Raleway-Regular.ttf
             - asset: packages/my_package/fonts/Raleway-Medium.ttf
               weight: 500
    在这种情况下,由于应用程序本地定义了字体,所以在创建TextStyle时可以不指定package参数:
    const textStyle = const TextStyle(
      fontFamily: 'Raleway',
    );
    

    1. 在pubsec.yaml中声明字体
    
    name: my_application
    description: A new Flutter project.
    dependencies:
      flutter:
        sdk: flutter
    flutter:
      # Include the Material Design fonts.
      uses-material-design: true
      fonts:
        - family: Rock Salt
          fonts:
            # https://fonts.google.com/specimen/Rock+Salt
            - asset: fonts/RockSalt-Regular.ttf
        - family: VT323
          fonts:
            # https://fonts.google.com/specimen/VT323
            - asset: fonts/VT323-Regular.ttf
        - family: Ewert
          fonts:
            # https://fonts.google.com/specimen/Ewert
            - asset: fonts/Ewert-Regular.ttf
    
    2. 
    import 'package:flutter/material.dart';
    
    const String words1 = "Almost before we knew it, we had left the ground.";
    const String words2 = "A shining crescent far beneath the flying vessel.";
    const String words3 = "A red flair silhouetted the jagged edge of a wing.";
    const String words4 = "Mist enveloped the ship three hours out from port.";
    
    void main() {
      runApp(new MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return new MaterialApp(
          title: 'Flutter Fonts',
          theme: new ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: new FontsPage(),
        );
      }
    }
    
    class FontsPage extends StatefulWidget {
      @override
      _FontsPageState createState() => new _FontsPageState();
    }
    
    class _FontsPageState extends State<FontsPage> {
      @override
      Widget build(BuildContext context) {
        // Rock Salt - https://fonts.google.com/specimen/Rock+Salt
        var rockSaltContainer = new Container(
          child: new Column(
            children: <Widget>[
              new Text(
                "Rock Salt",
              ),
              new Text(
                words2,
                textAlign: TextAlign.center,
                style: new TextStyle(
                  fontFamily: "Rock Salt",
                  fontSize: 17.0,
                ),
              ),
            ],
          ),
          margin: const EdgeInsets.all(10.0),
          padding: const EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
          ),
        );
    
        // VT323 - https://fonts.google.com/specimen/VT323
        var v2t323Container = new Container(
          child: new Column(
            children: <Widget>[
              new Text(
                "VT323",
              ),
              new Text(
                words3,
                textAlign: TextAlign.center,
                style: new TextStyle(
                  fontFamily: "VT323",
                  fontSize: 25.0,
                ),
              ),
            ],
          ),
          margin: const EdgeInsets.all(10.0),
          padding: const EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
          ),
        );
    
        // https://fonts.google.com/specimen/Ewert
        var ewertContainer = new Container(
          child: new Column(
            children: <Widget>[
              new Text(
                "Ewert",
              ),
              new Text(
                words4,
                textAlign: TextAlign.center,
                style: new TextStyle(
                  fontFamily: "Ewert",
                  fontSize: 25.0,
                ),
              ),
            ],
          ),
          margin: const EdgeInsets.all(10.0),
          padding: const EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
          ),
        );
    
        // Material Icons font - included with Material Design
        String icons = "";
    
        // https://material.io/icons/#ic_accessible
        // accessible: &#xE914; or 0xE914 or E914
        icons += "\u{E914}";
    
        // https://material.io/icons/#ic_error
        // error: &#xE000; or 0xE000 or E000
        icons += "\u{E000}";
    
        // https://material.io/icons/#ic_fingerprint
        // fingerprint: &#xE90D; or 0xE90D or E90D
        icons += "\u{E90D}";
    
        // https://material.io/icons/#ic_camera
        // camera: &#xE3AF; or 0xE3AF or E3AF
        icons += "\u{E3AF}";
    
        // https://material.io/icons/#ic_palette
        // palette: &#xE40A; or 0xE40A or E40A
        icons += "\u{E40A}";
    
        // https://material.io/icons/#ic_tag_faces
        // tag faces: &#xE420; or 0xE420 or E420
        icons += "\u{E420}";
    
        // https://material.io/icons/#ic_directions_bike
        // directions bike: &#xE52F; or 0xE52F or E52F
        icons += "\u{E52F}";
    
        // https://material.io/icons/#ic_airline_seat_recline_extra
        // airline seat recline extra: &#xE636; or 0xE636 or E636
        icons += "\u{E636}";
    
        // https://material.io/icons/#ic_beach_access
        // beach access: &#xEB3E; or 0xEB3E or EB3E
        icons += "\u{EB3E}";
    
        // https://material.io/icons/#ic_public
        // public: &#xE80B; or 0xE80B or E80B
        icons += "\u{E80B}";
    
        // https://material.io/icons/#ic_star
        // star: &#xE838; or 0xE838 or E838
        icons += "\u{E838}";
    
        var materialIconsContainer = new Container(
          child: new Column(
            children: <Widget>[
              new Text(
                "Material Icons",
              ),
              new Text(
                icons,
                textAlign: TextAlign.center,
                style: new TextStyle(
                  inherit: false,
                  fontFamily: "MaterialIcons",
                  color: Colors.black,
                  fontStyle: FontStyle.normal,
                  fontSize: 25.0,
                ),
              ),
            ],
          ),
          margin: const EdgeInsets.all(10.0),
          padding: const EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
          ),
        );
    
        return new Scaffold(
          appBar: new AppBar(
            title: new Text("Fonts"),
          ),
          body: new ListView(
            children: <Widget>[
              rockSaltContainer,
              v2t323Container,
              ewertContainer,
              materialIconsContainer,
            ],
          ),
        );
      }
    }
    

    2. 按钮

    常见按钮组件
      RaisedButton  凸起的按钮
      FlatButton    扁平化按钮
      OutlineButton  线框按钮
      IconButton  图标按钮
      ButtonBar  按钮组
      Inkwell  没有样式的带水波纹的按钮
      
    它们都是直接或间接对RawMaterialButton组件的包装定制。
    所有Material 库中的按钮都有如下相同点:
        1. 按下时都会有“水波动画”(又称“涟漪动画”,就是点击时按钮上会出现水波荡漾的动画)。
        2. 有一个onPressed属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。
    
    1. RaisedButton

    默认带有阴影和灰色背景。按下后,阴影会变大

    RaisedButton(
      child: Text("按钮"),
      color: Colors.blue,    // 背景色, Color(0x000000)透明
      textColor: Colors.white,  // 文本色
      disabledColor: Colors.gray,  // 禁用时的背景色
      disabledTextColor: Colors.gray,  // 禁用时的文本色
      splashColor: Colors.yellow,  // 点击按钮时的水波纹的颜色
      highligthColor: Colors.blue,    // 点击时的背景色
      elevation: 2.0,  // 正常状态下的阴影,值越大越明显
      highlightElevation: 8.0,  // 按下时的阴影
      disabledElevation:0.0, // 禁用时的阴影
      shape: RoundedRectangleBorder(  
        borderRadius: BorderRadius.circular(10.0)    // 圆角
      ),    // 设置shape为CircleBorder(side:BorderSide(color:Colors.white))  圆形按钮
      onPressed: () {
      },
    );
    
    
    通过在外层加Container:设置宽高。
    通过在外层加Container和Expanded:自适应宽或高。
    
    1. FlatButton

    默认背景透明并不带阴影。按下后,会有背景色

    FlatButton(
      // color、textColor、disabledColor、disabledTextColor、splashColor、highligthColor、shape同上
      child: Text("normal"),
      colorBrightness: Brightness.dark, // 按钮主题,默认是浅色主题 
      padding: 10,  // 内边距
      onPressed: () {},
    )
    
    1. OutlineButton

    默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)

    OutlineButton(
      // textColor、disabledTextColor、splashColor、shape同上
      child: Text("normal"),
      onPressed: () {},
    )
    
    1. IconButton

    一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景

    IconButton(
      icon: Icon(Icons.thumb_up),
      onPressed: () {},
    )
    
    1. ButtonBar
    ButtonBar(
      children:<Weight>[
        // 放入RaisedButton等类型按钮
      ],
    )
    
    1. 带图标的按钮

    前面图片后面文本的按钮

    RaisedButton.icon(
      icon: Icon(Icons.send),
      label: Text("发送"),
      onPressed: _onPressed,
    ),
    
    OutlineButton.icon(
      icon: Icon(Icons.add),
      label: Text("添加"),
      onPressed: _onPressed,
    ),
    
    FlatButton.icon(
      icon: Icon(Icons.info),
      label: Text("详情"),
      onPressed: _onPressed,
    ),
    

    3. 图片、ICON

    1. 图片(通过Image组件加载并显示)
    数据源可以是asset、文件、内存以及网络。
    Image组件支持gif
    Flutter框架对加载过的图片是有缓存的(内存),默认最大缓存数量是1000,最大缓存空间为100M。
    
    1. image: 必选参数。
    对应一个ImageProvider,ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider 。如AssetImage是实现了从Asset中加载图片的ImageProvider,而NetworkImage实现了从网络加载图片的ImageProvider。
    
    
    2. width、height:
    图片的宽、高
    当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置width、height的其中一个,那么另一个属性默认会按比例缩放,但可以通过fit属性来指定适应规则。
    
    
    3. fit:
    缩放模式
    用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。缩放模式是在BoxFit中定义,它是一个枚举类型,有如下值:
        fill:不按比例填充。
        cover:按比例填充,超出显示空间部分会被剪裁。
        contain:默认,按比例填充,会有留白。
        fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
        fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
        none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。
    
    
    4. color和 colorBlendMode:
    在图片绘制时可以对每一个像素进行颜色混合处理
    color指定混合色,而colorBlendMode指定混合模式(如:Blend.screen)
    
    
    5. repeat:
    重复方式
    当图片本身大小小于显示空间时,指定图片的重复规则。
    ImageRepeat.repeat 横纵向重复
    ImageRepeat.repeatX 横向重复
    ImageRepeat.repeatY 纵向重复
    
    
    6. alignment
    对齐方式 Alignment.center居中
    

    Image(
      image: AssetImage("images/avatar.png"),
      width: 100.0,
      color: Colors.blue,
      colorBlendMode: BlendMode.difference,
    );
    
    Image(
      image: AssetImage("images/avatar.png"),
      width: 100.0,
      height: 200.0,
      repeat: ImageRepeat.repeatY ,
    )
    

    例(从asset中加载图片)

    1. 在工程根目录下创建一个images目录,并将图片avatar.png拷贝到该目录。
    
    2. 在pubspec.yaml中的flutter部分添加如下内容:(由于 yaml 文件对缩进严格,所以必须严格按照每一层两个空格的方式进行缩进,此处assets前面应有两个空格。)
    flutter:
      assets:
        - images/avatar.png
    
    3. 加载该图片
    Image(
      image: AssetImage("images/avatar.png"),
      width: 100.0
    );
    或(常用)
    Image.asset("images/avatar.png",
      width: 100.0,
    )
    
    常见类型的asset包括静态数据(如JSON文件),配置文件,图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)
    构建期间,Flutter将asset放置到称为 asset bundle 的特殊存档中。asset会打包到程序安装包中的,可在运行时访问。
    Flutter使用pubspec.yaml文件(位于项目根目录),来识别应用程序所需的asset。
    asset的实际目录可以是任意文件夹,这里是images。
    

    例(从网络加载图片)

    Image(
      image: NetworkImage(
          "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
      width: 100.0,
    )
    
    或
    
    Image.network(
      "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
      width: 100.0,
    )
    

    import 'package:flutter/material.dart';
    
    class ImageAndIconRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        var img=AssetImage("imgs/avatar.png");
        return SingleChildScrollView(
          child: Column(
            children: <Image>[
              Image(
                image: img,
                height: 50.0,
                width: 100.0,
                fit: BoxFit.fill,
              ),
              Image(
                image: img,
                height: 50,
                width: 50.0,
                fit: BoxFit.contain,
              ),
              Image(
                image: img,
                width: 100.0,
                height: 50.0,
                fit: BoxFit.cover,
              ),
              Image(
                image: img,
                width: 100.0,
                height: 50.0,
                fit: BoxFit.fitWidth,
              ),
              Image(
                image: img,
                width: 100.0,
                height: 50.0,
                fit: BoxFit.fitHeight,
              ),
              Image(
                image: img,
                width: 100.0,
                height: 50.0,
                fit: BoxFit.scaleDown,
              ),
              Image(
                image: img,
                height: 50.0,
                width: 100.0,
                fit: BoxFit.none,
              ),
              Image(
                image: img,
                width: 100.0,
                color: Colors.blue,
                colorBlendMode: BlendMode.difference,
                fit: BoxFit.fill,
              ),
              Image(
                image: img,
                width: 100.0,
                height: 200.0,
                repeat: ImageRepeat.repeatY ,
              )
            ].map((e){
              return Row(
                children: <Widget>[
                  Padding(
                    padding: EdgeInsets.all(16.0),
                    child: SizedBox(
                      width: 100,
                      child: e,
                    ),
                  ),
                  Text(e.fit.toString())
                ],
              );
            }).toList()
          ),
        );
      }
    }
    

    例(占位符,淡入图片)依赖transparent_image包

    import 'package:flutter/material.dart';
    import 'package:transparent_image/transparent_image.dart';
    
    void main() {
      runApp(new MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final title = 'Fade in images';
    
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new Stack(
              children: <Widget>[
                new Center(child: new CircularProgressIndicator()),
                new Center(
                  child: new FadeInImage.memoryNetwork(
                    placeholder: kTransparentImage,
                    image:
                        'https://github.com/flutter/website/blob/master/_includes/code/layout/lakes/images/lake.jpg?raw=true',
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    例(缓存图片)依赖cached_network_image包

    除了缓存之外,cached_image_network包在加载时还支持占位符和淡入淡出图片
    
    import 'package:flutter/material.dart';
    import 'package:cached_network_image/cached_network_image.dart';
    
    void main() {
      runApp(new MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final title = 'Cached Images';
    
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new Center(
              child: new CachedNetworkImage(
                placeholder: new CircularProgressIndicator(),
                imageUrl:
                    'https://github.com/flutter/website/blob/master/_includes/code/layout/lakes/images/lake.jpg?raw=true',
              ),
            ),
          ),
        );
      }
    }
    
    1. ICON

    Flutter中,可以像Web开发一样使用iconfont,iconfont即“字体图标”,它是将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。

    在字体文件中,每一个字符都对应一个位码,而每一个位码对应一个显示字形,不同的字体就是指字形不同,即字符对应的字形是不同的。
    而在iconfont中,只是将位码对应的字形做成了图标,所以不同的字符最终就会渲染成不同的图标。

    iconfont和图片相比有如下优势:
    
        1. 体积小:可以减小安装包大小。
        2. 矢量:iconfont都是矢量图标,放大不会影响其清晰度。
        3. 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
        4. 可以通过TextSpan和文本混用。
    
    1. 使用Material Design字体图标。图标官网
    Flutter默认包含了一套Material Design的字体图标,在pubspec.yaml文件中的配置如下
      flutter:
        uses-material-design: true
    Icons类中包含了所有Material Design图标的IconData静态变量定义。
    
    例:
    String icons = "";
    // accessible: &#xE914; or 0xE914 or E914
    icons += "\uE914";
    // error: &#xE000; or 0xE000 or E000
    icons += " \uE000";
    // fingerprint: &#xE90D; or 0xE90D or E90D
    icons += " \uE90D";
    Text(icons,
      style: TextStyle(
          fontFamily: "MaterialIcons",
          fontSize: 24.0,
          color: Colors.green
      ),
    );
    
    使用图标就像使用文本一样,但是这种方式需要提供每个图标的码点,这并对开发者不友好,所以,Flutter封装了IconData和Icon来专门显示字体图标,上面的例子也可以用如下方式实现:
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.accessible,color: Colors.green,),
        Icon(Icons.error,color: Colors.green,),
        Icon(Icons.fingerprint,color: Colors.green,),
      ],
    )
    
    1. 使用自定义字体图标
    iconfont.cn上有很多字体图标素材,我们可以选择自己需要的图标打包下载后,会生成一些不同格式的字体文件,在Flutter中使用ttf格式即可。
    
    
    例
    1. 导入字体图标文件;这一步和导入字体文件相同,假设我们的字体图标文件保存在项目根目录下,路径为"fonts/iconfont.ttf":
      fonts:
        - family: myIcon  #指定一个字体名
          fonts:
            - asset: fonts/iconfont.ttf
    
    2. 为了使用方便,定义一个MyIcons类,功能和Icons类一样:将字体文件中的所有图标都定义成静态变量:
    class MyIcons{
      // book 图标
      static const IconData book = const IconData(
          0xe614, 
          fontFamily: 'myIcon', 
          matchTextDirection: true
      );
      // 微信图标
      static const IconData wechat = const IconData(
          0xec7d,  
          fontFamily: 'myIcon', 
          matchTextDirection: true
      );
    }
    
    3.使用
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(MyIcons.book,color: Colors.purple,),
        Icon(MyIcons.wechat,color: Colors.green,),
      ],
    )
    

    4.Form表单

    属于 Material组件库

    常见表单
      1. TextField
      2. Radio
      3. Switch
      4. CheckboxListTitle
      5. RadioListTitle
      6. SwitchListTitle
      7. Slide
    
    1. TextField (用于文本输入)
    1.
    style
    正在编辑的文本样式
    
    textAlign
    输入框内编辑文本在水平方向的对齐方式。默认TextAlign.start。
    
    
    
    2. 
    readOnly
    是否只读。默认false。
    
    enabled
    是否禁用。
    如果为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式。
    
    obscureText 
    是否用“•”替换正在编辑的文本。默认false。
    
    
    
    
    3. 
    maxLines
    输入框的最大行数。
    默认为1。如果为null,则无行数限制。
    
    minLines
    输入框的最小行数。
    
    maxLength
    输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数。
    
    maxLengthEnforced 
    当输入文本长度超过maxLength时是否阻止输入,为true时会阻止输入,为false时不会阻止输入但输入框会变红。
    默认true。
    
    
    
    4. 光标
    showCursor
    是否显示光标
    
    cursorColor
    自定义输入框光标颜色。
    
    cursorRadius
    自定义输入框光标圆角。
    
    cursorWidth
    自定义输入框光标宽度。默认2.0
    
    
    
    5. 键盘
    keyboardType
    用于设置该输入框默认的键盘输入类型(TextInputType)
    
    textInputAction
    键盘动作按钮图标(即回车键位图标)
    它是一个枚举值,有多个可选值。为TextInputAction.search则是个搜索图标。
    
    
    
    6. 焦点
    autofocus
    是否自动获取焦点。默认false
    
    focusNode
    用于控制TextField是否占有当前键盘的输入焦点。它是我们和键盘交互的一个句柄(handle)。
    
    
    
    7. decoration
    用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
    默认情况下,TextField有一个下划线装饰。设置为空会完全删除装饰(包括下划线和为标签保留的空间)。
      hintText (placeholder文本)
      border(边框,如OutlineInputBorder())
      labelText(placeholder文本,输入文本后还显示且位置跑到上边)
      labelStyle(labelText的样式)
      icon(图标,位于文本框前面)
    
    
    
    8. 回调
    onChanged
    输入框内容改变时的回调函数。内容改变事件也可以通过controller来监听
    
    onEditingComplete
    输入框输入完成时的回调函数,比如按了键盘的完成键(对号图标)或搜索键(🔍图标)。
    回调是ValueChanged<String>类型,它接收当前输入内容做为参数。
    
    onSubmitted
    输入框输入完成时的回调函数。不接收参数
    
    
    
    9. controller
    编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。
    大多数情况下都需要显式提供一个controller来与文本框交互。如果没有提供controller,则TextField内部会自动创建一个。
    
    
    
    10. inputFormatters
    用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验
    
    TextFormField包裹一个TextField并将其集成在Form中。如下情况使用:
      1. 要提供一个验证函数来检查用户的输入是否满足一定的约束(例如,一个电话号码)
      2. 想将TextField与其他FormField集成时。
    
    TextInputType枚举值

    Column(
            children: <Widget>[
              TextField(
                autofocus: true,
                decoration: InputDecoration(
                    labelText: "用户名",
                    hintText: "用户名或邮箱",
                    prefixIcon: Icon(Icons.person)
                ),
              ),
              TextField(
                decoration: InputDecoration(
                    labelText: "密码",
                    hintText: "您的登录密码",
                    prefixIcon: Icon(Icons.lock)
                ),
                obscureText: true,
              ),
            ],
    );
    

    获取输入内容

    两种方式:
       1. 定义变量,在onChange触发时保存一下输入内容。
       2. 通过controller直接获取。
    
    例(通过controller直接获取)
    
    // initState方法中:
    TextEditingController _unameController = TextEditingController();
    _unameController.text='初始文本';
    
    // 
    TextField(
        autofocus: true,
        controller: _unameController, // 设置controller
    )
    
    //
    print(_unameController.text)
    
    // 
    _unameController.text='hello';
    

    监听文本变化(两种方式)

    1. 设置onChange回调
      专门用于监听文本变化。输入文本发生变化时会调用。
    2. 通过controller监听
      除了能监听文本变化外,还可以设置默认值、选择文本。
      提供一个TextEditingController作为TextField的controller属性,使用controller的addListener来监听
      注意:需要在State对象的dispose方法中删除监听器
    
    
    例(方法1)
    TextField(
        autofocus: true,
        onChanged: (v) {
          print("onChange: $v");
        }
    )
    
    例(方法2)
    @override
    void initState() {
      //监听输入改变  
      _unameController.addListener((){
        print(_unameController.text);
      });
    }
    
    例(方法2)
    TextField(
      controller: _selectionController,
    )
    TextEditingController _selectionController =  TextEditingController();
    _selectionController.text="hello world!";
    _selectionController.selection=TextSelection(
        baseOffset: 2,
        extentOffset: _selectionController.text.length
    );
    
    例(完整代码)
    
    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.yellow,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      @override
      createState() => new MyHomePageState();
    }
    
    class MyHomePageState extends State<MyHomePage> {
      final TextEditingController _controller = new TextEditingController();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new TextField(
              controller: _controller,
              decoration: new InputDecoration(
                hintText: '输入点什么呗',
              ),
            ),
            new RaisedButton(
              onPressed: () {
                showDialog(
                  context: context,
                  child: new AlertDialog(
                    title: new Text('你输入了:'),
                    content: new Text(_controller.text),
                  ),
                );
              },
              child: new Text('确认'),
            ),
          ],
        ));
      }
    }
    

    控制焦点

    焦点可以通过FocusNode和FocusScopeNode来控制。
    默认情况下,焦点由FocusScope来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode在输入框之间移动焦点、设置默认焦点等。
    可以通过FocusScope.of(context) 来获取Widget树中默认的FocusScopeNode。
    
    例
    
    创建两个TextField,第一个自动获取焦点,然后创建两个按钮:
        点击第一个按钮可以将焦点从第一个TextField挪到第二个TextField。
        点击第二个按钮可以关闭键盘。
    
    
    class FocusTestRoute extends StatefulWidget {
      @override
      _FocusTestRouteState createState() => new _FocusTestRouteState();
    }
    
    class _FocusTestRouteState extends State<FocusTestRoute> {
      FocusNode focusNode1 = new FocusNode();
      FocusNode focusNode2 = new FocusNode();
      FocusScopeNode focusScopeNode;
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: <Widget>[
              TextField(
                autofocus: true, 
                focusNode: focusNode1,//关联focusNode1
                decoration: InputDecoration(
                    labelText: "input1"
                ),
              ),
              TextField(
                focusNode: focusNode2,//关联focusNode2
                decoration: InputDecoration(
                    labelText: "input2"
                ),
              ),
              Builder(builder: (ctx) {
                return Column(
                  children: <Widget>[
                    RaisedButton(
                      child: Text("移动焦点"),
                      onPressed: () {
                        //将焦点从第一个TextField移到第二个TextField
                        // 这是一种写法 FocusScope.of(context).requestFocus(focusNode2);
                        // 这是第二种写法
                        if(null == focusScopeNode){
                          focusScopeNode = FocusScope.of(context);
                        }
                        focusScopeNode.requestFocus(focusNode2);
                      },
                    ),
                    RaisedButton(
                      child: Text("隐藏键盘"),
                      onPressed: () {
                        // 当所有编辑框都失去焦点时键盘就会收起  
                        focusNode1.unfocus();
                        focusNode2.unfocus();
                      },
                    ),
                  ],
                );
              },
              ),
            ],
          ),
        );
      }
    
    }
    

    监听焦点状态改变事件

    FocusNode继承自ChangeNotifier,通过FocusNode可以监听焦点的改变事件
    获得焦点时focusNode.hasFocus值为true,失去焦点时为false。
    
    ...
    // 创建 focusNode   
    FocusNode focusNode = new FocusNode();
    ...
    // focusNode绑定输入框   
    TextField(focusNode: focusNode);
    ...
    // 监听焦点变化    
    focusNode.addListener((){
       print(focusNode.hasFocus);
    });
    

    自定义样式

    可以通过decoration属性来定义输入框样式
    也可以通过主题来自定义输入框的样式。
    
    一般来说,优先通过decoration来自定义样式,如果decoration实现不了,再用widget组合的方式。
    
    例(自定义输入框下划线颜色)
    
    TextField(
      decoration: InputDecoration(
        labelText: "请输入用户名",
        prefixIcon: Icon(Icons.person),
        // 未获得焦点下划线设为灰色
        enabledBorder: UnderlineInputBorder(
          borderSide: BorderSide(color: Colors.grey),
        ),
        //获得焦点下划线设为蓝色
        focusedBorder: UnderlineInputBorder(
          borderSide: BorderSide(color: Colors.blue),
        ),
      ),
    ),
    
    例 (自定义下滑线颜色,方式2)
    
    由于TextField在绘制下划线时使用的颜色是主题色里面的hintColor,但提示文本颜色也是用的hintColor, 如果我们直接修改hintColor,那么下划线和提示文本的颜色都会变。值得高兴的是decoration中可以设置hintStyle,它可以覆盖hintColor,并且主题中可以通过inputDecorationTheme来设置输入框默认的decoration。
    通过这种方式自定义后,输入框在获取焦点时,labelText不会高亮显示了,并且我们还是无法定义下划线宽度
    
    Theme(
      data: Theme.of(context).copyWith(
          hintColor: Colors.grey[200], //定义下划线颜色
          inputDecorationTheme: InputDecorationTheme(
              labelStyle: TextStyle(color: Colors.grey),//定义label字体样式
              hintStyle: TextStyle(color: Colors.grey, fontSize: 14.0)//定义提示文本样式
          )
      ),
      child: Column(
        children: <Widget>[
          TextField(
            decoration: InputDecoration(
                labelText: "用户名",
                hintText: "用户名或邮箱",
                prefixIcon: Icon(Icons.person)
            ),
          ),
          TextField(
            decoration: InputDecoration(
                prefixIcon: Icon(Icons.lock),
                labelText: "密码",
                hintText: "您的登录密码",
                hintStyle: TextStyle(color: Colors.grey, fontSize: 13.0)
            ),
            obscureText: true,
          )
        ],
      )
    )
    
    例(隐藏下划线)
    
    Container(
      child: TextField(
        keyboardType: TextInputType.emailAddress,
        decoration: InputDecoration(
            labelText: "Email",
            hintText: "电子邮件地址",
            prefixIcon: Icon(Icons.email),
            border: InputBorder.none //隐藏下划线
        )
      ),
      decoration: BoxDecoration(
          // 下滑线浅灰色,宽度1像素
          border: Border(bottom: BorderSide(color: Colors.grey[200], width: 1.0))
      ),
    )
    
    通过这种组件组合的方式,也可以定义背景圆角等。一般来说,优先通过decoration来自定义样式,如果decoration实现不了,再用widget组合的方式。
    
    1. 单选开关(Switch)、复选框(Checkbox、CheckboxListTitle)、 单选框(Radio、RadioListTitle)

    都继承自StatefulWidget,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。
    被点击时,会触发它们的onChanged回调。

    例(Switch、Checkbox)

    class SwitchAndCheckBoxTestRoute extends StatefulWidget {
      @override
      _SwitchAndCheckBoxTestRouteState createState() => new _SwitchAndCheckBoxTestRouteState();
    }
    
    class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
      bool _switchSelected=true; //维护单选开关状态
      bool _checkboxSelected=true;//维护复选框状态
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            Switch(
              // activeColor: Colors.red, // 选中时的颜色
              value: _switchSelected,  // 是否选中
              onChanged:(value){
                // 重新构建页面  
                setState(() {
                  _switchSelected=value;
                });
              },
            ),
            // Divider(), 分割线
            Checkbox(
              value: _checkboxSelected,  // 是否选中
              activeColor: Colors.red, // 选中时的颜色
              // checkColor:Colors.white,  // 选中时,对号的颜色
              onChanged:(value){    // 状态改变时调用
                setState(() {
                  _checkboxSelected=value;
                });
              } ,
            )
          ],
        );
      }
    }
    
    到目前为止,Checkbox的大小是固定的,无法自定义;而Switch只能定义宽度,高度也是固定的。
    Checkbox还有一个属性tristate ,表示是否为三态,其默认值为false ,这时Checkbox有两种状态即“选中”和“不选中”,对应的value值为true和false 。如果tristate值为true时,value的值会增加一个状态null。
    
    // 可以设置List,循环创建Checkbox,value为List中对应的值,onChanged中修改List中对应的值
    List checkList=[
      {
        "checked":true,
        "title":"姓名"
      },
    ];
    

    CheckboxListTitle

    左边是图片,然后是标题、副标题(从上到下),右边是选择框
    
    1. value
    是否选中
    
    2. onChanged
    状态改变后的回调
    
    3. activeColor
    选中时的颜色
    
    4. title
    标题
    
    5. subtitle
    副标题
    
    6. secondary
    图片
    
    7. selected
    选中时文本色是否跟随变化
    

    Radio (单选框)

    1. value
    该项的值
    
    2. groupValue
    一组Radio的当前选中的值
    
    3. onChanged
    状态改变后的回调
    
    
    通常最外层为Row
    

    RadioListTitle

    左边是图片,然后是标题、副标题(从上到下),右边是单选框
    
    1. value
    该项的值
    
    2. groupValue
    一组Radio的当前选中的值
    
    3. onChanged
    状态改变后的回调
    
    4. title
    标题
    
    5. subtitle
    副标题
    
    6. secondary
    图片
    
    7. selected
    选中时文本色是否跟随变化
    
    1. 表单Form

    实际业务中,在正式向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。
    为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行统一操作,如输入内容校验、输入框重置以及输入内容保存。

    Form继承自StatefulWidget对象,它对应的状态类为FormState

    0. child
    
    1. autovalidate
    是否自动校验输入内容。默认false。
    当为true时,每一个子FormField内容发生变化时都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate()来手动校验。
    
    2. onWillPop
    决定Form所在的路由是否可以直接返回(如点击返回按钮)。
    该回调返回一个Future对象,如果Future的最终结果是false,则当前路由不会返回;如果为true,则会返回到上一个路由。此属性通常用于拦截返回按钮。
    
    3. onChanged
    Form的任意一个子FormField内容发生变化时会触发此回调。
    

    FormField

    Form的子孙元素必须是FormField类型
    FormField是一个抽象类。为了方便使用,Flutter提供了一个TextFormField组件,它继承自FormField类,也是TextField的一个包装类。
    
    
      const FormField({
        Key key,
        @required this.builder,
        this.onSaved,  // 保存回调
        this.validator,  // 验证回调
        this.initialValue,  // 初始值
        this.autovalidate = false,  // 是否自动校验
        this.enabled = true,
      })
    

    FormState

    FormState为Form的State类,可以通过Form.of()或GlobalKey获得。
    可以通过它来对Form的子孙FormField进行统一操作。
    
    常用的三个方法:
        1. FormState.validate():会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
        2. FormState.save():会调用Form子孙FormField的save回调,用于保存表单内容
        3. FormState.reset():会将子孙FormField的内容清空。
    

    在提交之前校验:
        用户名不能为空,如果为空则提示“用户名不能为空”。
        密码不能小于6位,如果小于6为则提示“密码不能少于6位”。
    
    class FormTestRoute extends StatefulWidget {
      @override
      _FormTestRouteState createState() => new _FormTestRouteState();
    }
    
    class _FormTestRouteState extends State<FormTestRoute> {
      TextEditingController _unameController = new TextEditingController();
      TextEditingController _pwdController = new TextEditingController();
      GlobalKey _formKey= new GlobalKey<FormState>();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title:Text("Form Test"),
          ),
          body: Padding(
            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
            child: Form(
              key: _formKey, //设置globalKey,用于后面获取FormState
              autovalidate: true, //开启自动校验
              child: Column(
                children: <Widget>[
                  TextFormField(
                      autofocus: true,
                      controller: _unameController,
                      decoration: InputDecoration(
                          labelText: "用户名",
                          hintText: "用户名或邮箱",
                          icon: Icon(Icons.person)
                      ),
                      // 校验用户名
                      validator: (v) {
                        return v
                            .trim()
                            .length > 0 ? null : "用户名不能为空";
                      }
    
                  ),
                  TextFormField(
                      controller: _pwdController,
                      decoration: InputDecoration(
                          labelText: "密码",
                          hintText: "您的登录密码",
                          icon: Icon(Icons.lock)
                      ),
                      obscureText: true,
                      //校验密码
                      validator: (v) {
                        return v
                            .trim()
                            .length > 5 ? null : "密码不能少于6位";
                      }
                  ),
                  // 登录按钮
                  Padding(
                    padding: const EdgeInsets.only(top: 28.0),
                    child: Row(
                      children: <Widget>[
                        Expanded(
                          child: RaisedButton(
                            padding: EdgeInsets.all(15.0),
                            child: Text("登录"),
                            color: Theme
                                .of(context)
                                .primaryColor,
                            textColor: Colors.white,
                            onPressed: () {
                              //在这里不能通过此方式获取FormState,此处的context为FormTestRoute的context,向根去查找不会找到(FormState是在FormTestRoute的子树中)
                              //print(Form.of(context));
    
                              // 通过_formKey.currentState 获取FormState后,
                              // 调用validate()方法校验用户名密码是否合法,校验
                              // 通过后再提交数据。 
                              if((_formKey.currentState as FormState).validate()){
                                //验证通过提交数据
                              }
                            },
                          ),
                        ),
                      ],
                    ),
                  )
                ],
              ),
            ),
          ),
        );
      }
    }
    
    登录按钮的onPressed方法中,过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:
    Expanded(
     // 通过Builder来获取RaisedButton所在widget树的真正context(Element) 
      child:Builder(builder: (context){
        return RaisedButton(
          ...
          onPressed: () {
            //由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState  
            if(Form.of(context).validate()){
              //验证通过提交数据
            }
          },
        );
      })
    )
    

    5. 进度指示器

    Material组件库中提供了两种进度指示器:LinearProgressIndicator和CircularProgressIndicator

    它们都可以同时用于精确的进度指示和模糊的进度指示。
    精确进度通常用于任务进度可以计算和预估的情况,比如文件下载;而模糊进度则用户任务进度无法准确获得的情况,如下拉刷新,数据提交等。
    
    可以通过CustomPainter Widget 来自定义绘制
    LinearProgressIndicator和CircularProgressIndicator通过CustomPainter来实现外观绘制的。
    
    flutter_spinkit 包提供了多种风格的模糊进度指示器
    
    1. LinearProgressIndicator

    一个线性、条状的进度条

      const LinearProgressIndicator({
        Key key,
        double value,  // 当前的进度,取值范围为[0,1];如果value为null时则指示器会执行一个循环动画(模糊进度);当value不为null时,指示器为一个具体进度的进度条。
        Color backgroundColor,  // 背景色
        Animation<Color> valueColor,  // 进度条颜色,可以指定动画,也可以AlwaysStoppedAnimation指定固定颜色
        String semanticsLabel,
        String semanticsValue,
      })
    

    // 模糊进度条(会执行一个动画)
    LinearProgressIndicator(
      backgroundColor: Colors.grey[200],
      valueColor: AlwaysStoppedAnimation(Colors.blue),
    ),
    //进度条显示50%
    LinearProgressIndicator(
      backgroundColor: Colors.grey[200],
      valueColor: AlwaysStoppedAnimation(Colors.blue),
      value: .5, 
    )
    
    第一个进度条在执行循环动画:蓝色条一直在移动,
    第二个进度条是静止的,停在50%的位置。
    

    例(一个进度条在3秒内从灰色变成蓝色的动画)

    import 'package:flutter/material.dart';
    
    class ProgressRoute extends StatefulWidget {
      @override
      _ProgressRouteState createState() => _ProgressRouteState();
    }
    
    class _ProgressRouteState extends State<ProgressRoute>
        with SingleTickerProviderStateMixin {
      AnimationController _animationController;
    
      @override
      void initState() {
        //动画执行时间3秒  
        _animationController =
            new AnimationController(vsync: this, duration: Duration(seconds: 3));
        _animationController.forward();
        _animationController.addListener(() => setState(() => {}));
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return SingleChildScrollView(
          child: Column(
            children: <Widget>[
                Padding(
                padding: EdgeInsets.all(16),
                child: LinearProgressIndicator(
                  backgroundColor: Colors.grey[200],
                  valueColor: ColorTween(begin: Colors.grey, end: Colors.blue)
                    .animate(_animationController), // 从灰色变成蓝色
                  value: _animationController.value,
                ),
              );
            ],
          ),
        );
      }
    }
    
    1. CircularProgressIndicator

    一个圆形进度条

      const CircularProgressIndicator({
        Key key,
        double value,
        Color backgroundColor,
        Animation<Color> valueColor,
        this.strokeWidth = 4.0,  // 圆形进度条的粗细
        String semanticsLabel,
        String semanticsValue,
      })
    
    

    // 模糊进度条(会执行一个旋转动画)
    CircularProgressIndicator(
      backgroundColor: Colors.grey[200],
      valueColor: AlwaysStoppedAnimation(Colors.blue),
    ),
    //进度条显示50%,会显示一个半圆
    CircularProgressIndicator(
      backgroundColor: Colors.grey[200],
      valueColor: AlwaysStoppedAnimation(Colors.blue),
      value: .5,
    ),
    
    第一个进度条会执行旋转动画,
    第二个进度条是静止的,它停在50%的位置。
    

    自定义尺寸

    LinearProgressIndicator和CircularProgressIndicator,并没有提供设置圆形进度条尺寸的参数,但他们都是取父容器的尺寸作为绘制的边界的。可以通过尺寸限制类Widget,如ConstrainedBox、SizedBox 来指定尺寸

    // 线性进度条高度指定为3
    SizedBox(
      height: 3,
      child: LinearProgressIndicator(
        backgroundColor: Colors.grey[200],
        valueColor: AlwaysStoppedAnimation(Colors.blue),
        value: .5,
      ),
    ),
    
    // 圆形进度条直径指定为100
    // 如果CircularProgressIndicator显示空间的宽高不同,则会显示为椭圆。
    SizedBox(
      height: 100,
      width: 100,
      child: CircularProgressIndicator(
        backgroundColor: Colors.grey[200],
        valueColor: AlwaysStoppedAnimation(Colors.blue),
        value: .7,
      ),
    ),
    

    日历选择

    6. 日期组件

    var now=new DateTime.now();  // 当前时间。2050-01-01 00:00:01.001
    var time=now.millisecondsSinceEpoch;  // 13位时间戳,ms。日期转时间戳
    var date=DateTime.fromMillisecondsSinceEpoch(time);  // 时间戳转日期,2050-01-01 00:00:01.001
    
    date_format 三方库(日期格式化)
    
    
    // 2050年01月01
    formatDate(DateTime.now(),[yyyy,'年',mm,'月',dd]);  
    
    Android iOS

    日历组件、时间组件

    日历组件
      var _datetime=DateTime.now();
      _showDatePicker() async{
        var date=await showDatePicker(
          context: context,
          initialDate: _datetime, // 当前日期(初始日期)
          firstDate: DateTime(1900), // 最早日期
          lastDate: DateTime(2100), // 最晚日期。_datetime.add(Duration(days: 30),), // 未来30天可选
          // locale: Locale('zh'),    // 当前环境不是中文时,强制使用中文。需要首先国家化Material组件。
        );  // 选择完后才会继续向下执行
        if(!date)return;
        print(date);
        setState(() {
          _datetime=date;
        });
      }
    
    
    时间组件
      var _nowTIme=TimeOfDay(hour: 8,minute: 0);
      _showTimePicker() async{
        var time=await showTimePicker(
          context: context,
          initialTime: _nowTIme, // 当前日期(初始日期)
        );
        if(!time)return;
        print(time);
        setState(() {
          _nowTIme=time;
        });
      }
      // _nowTIme.format(context) 
    
    iOS风格的日历选择器需要使用showCupertinoModalPopup方法和CupertinoDatePicker组件来实现:
    Future<DateTime> _showDatePicker2() {
      var date = DateTime.now();
      return showCupertinoModalPopup(
        context: context,
        builder: (ctx) {
          return SizedBox(
            height: 200,
            child: CupertinoDatePicker(
              mode: CupertinoDatePickerMode.dateAndTime,
              minimumDate: date,
              maximumDate: date.add(
                Duration(days: 30),
              ),
              maximumYear: date.year + 1,
              onDateTimeChanged: (DateTime value) {
                print(value);
              },
            ),
          );
        },
      );
    }
    

    三方组件(flutter_cupertino_date_picker)

    需要先依赖包

      var _datetime=DateTime.now();
      _showDatePicker(){
        DatePicker.showDatePicker(
          context,
          picherTheme: DateTimePickerTheme(
            showTitle: true, // 是否显示标题
            confirm: Text('确定',style:TextStyle(color: Colors.red)),
            cancel: Text('取消',style:TextStyle(color: Colors.blue)),
          ),
          minDateTime: DateTime.parse('1900-01-01') , // 最早日期
          maxDateTime: DateTime.parse('2100-01-01') , // 最晚日期
          initialDateTime: _datetime, // 当前日期(初始日期)
          dateFormat:"yyyy-MM-dd",  // 年月日
          // dateFormat:"yyyy-MM-dd EEE,H时:m分,",  // 年月日周时分秒
          // pickerMode: DateTimePickerMode.datetime,  //  年月日周时分秒
          locale:DateTimePickerLocale.zh_cn,
          onCancel:(){
    
          },
          onChange:(dateTime,List<int> index){
            setDate((){
              _datetime=dateTime;
            });
          },
          onConfirm:(dateTime,List<int> index){
            setDate((){
              _datetime=dateTime;
            });
          },
        );  
      }
    

    7. 轮播图(三方库: flutter_swiper)

    1. 添加依赖包,并下载(pubspec.yaml文件中dependencies下)
    flutter_swiper: #lastversion
    
    2. 导入包
    import 'package:flutter_swiper/flutter_swiper.dart';
    
    3. 使用
    var itemList=[
      {
        "url":"http://..."
      },
      {
        "url":""http://..."
      },
      {
        "url":"http://..."
      },
    ];
    // 通常会在外层使用Container、AspectRatio
    Container(
      child: AspectRatio(
        aspectRatio: 16 / 9,  // 宽高比例
        child: Swiper(
          itemBuilder: (BuildContext context, int index) {
            // item项
            return Image.network(
              this.itemList[index]["url"],
              fit: BoxFit.fill,
            );
          },
          itemCount: 3, // item个数
          pagination: new SwiperPagination(), // 分页器
          control: new SwiperControl(), // 左右箭头
        ),
      ),
    ),
    

    8. 弹框(对话框)

    通常一个对话框会包含标题、内容,以及一些操作按钮。

    showDialog() 
    
    Future<T> showDialog<T>({
      @required BuildContext context,
      bool barrierDismissible = true, // 点击对话框barrier(遮罩)时是否关闭它
      WidgetBuilder builder, // 对话框UI的builder
    })
    弹出Material风格对话框。该方法返回一个Future,用于接收对话框的返回值:如果我们是通过点击对话框遮罩关闭的,则Future的值为null,否则为我们通过Navigator.of(context).pop(result)返回的result值。
    
    1. AlertDialog 对话框

    属于Material库

    AlertDialog定义
    const AlertDialog({
      Key key,
      this.title, // 标题组件
      this.titlePadding, // 标题内边距
      this.titleTextStyle, // 标题文本样式
      this.content, // 对话框内容组件。如果内容过长,内容将会溢出,可以用SingleChildScrollView将内容包裹起来。
      this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), // 内容内边距
      this.contentTextStyle, // 内容文本样式
      this.actions, // 对话框操作按钮组
      this.backgroundColor, // 对话框背景色
      this.elevation,// 对话框的阴影
      this.semanticLabel, // 对话框语义化标签(用于读屏软件)
      this.shape, // 对话框外形
    })
    

    //点击该按钮后弹出对话框
    RaisedButton(
      child: Text("对话框1"),
      onPressed: () async {
        // 弹出对话框并等待其关闭
        bool delete = await showDeleteConfirmDialog1();
        if (delete == null) {
          print("取消删除");
        } else {
          print("已确认删除");
          //... 删除文件
        }
      },
    ),
    
    // 弹出对话框
    Future<bool> showDeleteConfirmDialog1() {
      return showDialog<bool>(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text("提示"),
            content: Text("您确定要删除当前文件吗?"),
            actions: <Widget>[
              FlatButton(
                child: Text("取消"),
                onPressed: () => Navigator.of(context).pop(), // 关闭对话框
              ),
              FlatButton(
                child: Text("删除"),
                onPressed: () {
                  //关闭对话框并返回true
                  Navigator.of(context).pop(true);
                },
              ),
            ],
          );
        },
      );
    }
    
    1. SimpleDialog

    属于Material组件库。会展示一个列表。

    Future<void> changeLanguage() async {
      int i = await showDialog<int>(
          context: context,
          builder: (BuildContext context) {
            return SimpleDialog(
              title: const Text('请选择语言'),    // 标题组件
              children: <Widget>[  // children
                SimpleDialogOption(  // item
                  onPressed: () {
                    // 返回1
                    Navigator.pop(context, 1);
                  },
                  child: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 6),
                    child: const Text('中文简体'),
                  ),
                ),
                SimpleDialogOption(
                  onPressed: () {
                    // 返回2
                    Navigator.pop(context, 2);
                  },
                  child: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 6),
                    child: const Text('美国英语'),
                  ),
                ),
              ],
            );
          });
    
      if (i != null) {
        print("选择了:${i == 1 ? "中文简体" : "美国英语"}");
      }
    }
    
    1. Dialog(自定义对话框)

    AlertDialog和SimpleDialog都使用了Dialog类。由于AlertDialog和SimpleDialog中使用了IntrinsicWidth来尝试通过子组件的实际尺寸来调整自身尺寸,这就导致他们的子组件不能是延迟加载模型的组件(如ListView、GridView 、 CustomScrollView等)。

    如下代码运行后会报错:
    AlertDialog(
      content: ListView(
        children: ...//省略
      ),
    );
    
    这时可以使用Dialog
    Dialog(
      child: ListView(
        children: ...//省略
      ),
    );
    

    Future<void> showListDialog() async {
      int index = await showDialog<int>(
        context: context,
        builder: (BuildContext context) {
          var child = Column(
            children: <Widget>[
              ListTile(title: Text("请选择")),
              Expanded(
                  child: ListView.builder(
                itemCount: 30,
                itemBuilder: (BuildContext context, int index) {
                  return ListTile(
                    title: Text("$index"),
                    onTap: () => Navigator.of(context).pop(index),
                  );
                },
              )),
            ],
          );
          //使用AlertDialog会报错
          //return AlertDialog(content: child);
          return Dialog(child: child);
    /*
    可以返回其他widget来自定义对话框样式
    return UnconstrainedBox(
      constrainedAxis: Axis.vertical,
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: 280),
        child: Material(
          child: child,
          type: MaterialType.card,
        ),
      ),
    );
    */
        },
      );
      if (index != null) {
        print("点击了:$index");
      }
    }
    

    自定义Dialog

    默认的Dialog是满屏的。
    可以继承Dialog,重写build函数来自定义Dialog。
    
    showDialog(
        context: context,
        builder: (context){
          return MyDialog();
        }
    )
    class MyDialog extends Dialog{
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Material(
          type: MaterialType.transparency,  // 透明
          child: Column(  // 自定义UI
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.all(10),
                child: Container(
                  width: 320,
                  height: 280,
                  color: Colors.white,
                ),
              )
            ],
          ),
        );
      }
    }
    

    计时器

    build中调用
    
    _showTimer(context){
      var timer;
      timer = Timer.periodic(
          Duration(microseconds: 3000),(t) {  // 3s后执行
            Navigator.pop(context);
            t.cancel();// 取消定时器,timer.cancel() 也一样
          },
      );
    }
    

    对话框打开动画及遮罩

    Flutter 提供了一个showGeneralDialog方法来打开一个普通风格的对话框(非Material风格)
    
    Future<T> showGeneralDialog<T>({
      @required BuildContext context,
      @required RoutePageBuilder pageBuilder, //构建对话框内部UI
      bool barrierDismissible, //点击遮罩是否关闭对话框
      String barrierLabel, // 语义化标签(用于读屏软件)
      Color barrierColor, // 遮罩颜色
      Duration transitionDuration, // 对话框打开/关闭的动画时长
      RouteTransitionsBuilder transitionBuilder, // 对话框打开/关闭的动画
    })
    showDialog方法正是showGeneralDialog的一个封装,定制了Material风格对话框的遮罩颜色和动画。Material风格对话框打开/关闭动画是一个Fade(渐隐渐显)动画,如果我们想使用一个缩放动画就可以通过transitionBuilder来自定义。
    

    封装一个showCustomDialog方法,它定制的对话框动画为缩放动画,并同时制定遮罩颜色为Colors.black87
    
    Future<T> showCustomDialog<T>({
      @required BuildContext context,
      bool barrierDismissible = true,
      WidgetBuilder builder,
    }) {
      final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
      return showGeneralDialog(
        context: context,
        pageBuilder: (BuildContext buildContext, Animation<double> animation,
            Animation<double> secondaryAnimation) {
          final Widget pageChild = Builder(builder: builder);
          return SafeArea(
            child: Builder(builder: (BuildContext context) {
              return theme != null
                  ? Theme(data: theme, child: pageChild)
                  : pageChild;
            }),
          );
        },
        barrierDismissible: barrierDismissible,
        barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
        barrierColor: Colors.black87, // 自定义遮罩颜色
        transitionDuration: const Duration(milliseconds: 150),
        transitionBuilder: _buildMaterialDialogTransitions,
      );
    }
    
    Widget _buildMaterialDialogTransitions(
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
        Widget child) {
      // 使用缩放动画
      return ScaleTransition(
        scale: CurvedAnimation(
          parent: animation,
          curve: Curves.easeOut,
        ),
        child: child,
      );
    }
    
    
    ... //省略无关代码
    showCustomDialog<bool>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示"),
          content: Text("您确定要删除当前文件吗?"),
          actions: <Widget>[
            FlatButton(
              child: Text("取消"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            FlatButton(
              child: Text("删除"),
              onPressed: () {
                // 执行删除操作
                Navigator.of(context).pop(true);
              },
            ),
          ],
        );
      },
    );
    

    对话框实现原理

    对话框最终都是由showGeneralDialog方法打开的
    
    Future<T> showGeneralDialog<T>({
      @required BuildContext context,
      @required RoutePageBuilder pageBuilder,
      bool barrierDismissible,
      String barrierLabel,
      Color barrierColor,
      Duration transitionDuration,
      RouteTransitionsBuilder transitionBuilder,
    }) {
      return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
        pageBuilder: pageBuilder,
        barrierDismissible: barrierDismissible,
        barrierLabel: barrierLabel,
        barrierColor: barrierColor,
        transitionDuration: transitionDuration,
        transitionBuilder: transitionBuilder,
      ));
    }
    
    实现很简单,直接调用Navigator的push方法打开了一个新的对话框路由_DialogRoute,然后返回了push的返回值。可见对话框实际上正是通过路由的形式实现的,这也是为什么我们可以使用Navigator的pop 方法来退出对话框的原因。
    

    对话框状态管理

    我们在用户选择删除一个文件时,会询问是否删除此文件;在用户选择一个文件夹是,应该再让用户确认是否删除子文件夹。为了在用户选择了文件夹时避免二次弹窗确认是否删除子目录,我们在确认对话框底部添加一个“同时删除子目录?”的复选框,
    

    错误代码

    class _DialogRouteState extends State<DialogRoute> {
      bool withTree = false; // 复选框选中状态
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            RaisedButton(
              child: Text("对话框2"),
              onPressed: () async {
                bool delete = await showDeleteConfirmDialog2();
                if (delete == null) {
                  print("取消删除");
                } else {
                  print("同时删除子目录: $delete");
                }
              },
            ),
          ],
        );
      }
    
      Future<bool> showDeleteConfirmDialog2() {
        withTree = false; // 默认复选框不选中
        return showDialog<bool>(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: Text("提示"),
              content: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text("您确定要删除当前文件吗?"),
                  Row(
                    children: <Widget>[
                      Text("同时删除子目录?"),
                      Checkbox(
                        value: withTree,
                        onChanged: (bool value) {
                          //复选框选中状态发生变化时重新构建UI
                          setState(() {
                            //更新复选框状态
                            withTree = !withTree;
                          });
                        },
                      ),
                    ],
                  ),
                ],
              ),
              actions: <Widget>[
                FlatButton(
                  child: Text("取消"),
                  onPressed: () => Navigator.of(context).pop(),
                ),
                FlatButton(
                  child: Text("删除"),
                  onPressed: () {
                    //执行删除操作
                    Navigator.of(context).pop(withTree);
                  },
                ),
              ],
            );
          },
        );
      }
    }
    
    当我们运行上面的代码时我们会发现复选框根本选不中!为什么会这样呢?其实原因很简单,我们知道setState方法只会针对当前context的子树重新build,但是我们的对话框并不是在_DialogRouteState的build 方法中构建的,而是通过showDialog单独构建的,所以在_DialogRouteState的context中调用setState是无法影响通过showDialog构建的UI的。另外,我们可以从另外一个角度来理解这个现象,前面说过对话框也是通过路由的方式来实现的,那么上面的代码实际上就等同于企图在父路由中调用setState来让子路由更新,这显然是不行的!简尔言之,根本原因就是context不对。
    

    方法1: 单独抽离出StatefulWidget

    既然是context不对,那么直接的思路就是将复选框的选中逻辑单独封装成一个StatefulWidget,然后在其内部管理复选状态。
    
    // 单独封装一个内部管理选中状态的复选框组件
    class DialogCheckbox extends StatefulWidget {
      DialogCheckbox({
        Key key,
        this.value,
        @required this.onChanged,
      });
    
      final ValueChanged<bool> onChanged;
      final bool value;
    
      @override
      _DialogCheckboxState createState() => _DialogCheckboxState();
    }
    
    class _DialogCheckboxState extends State<DialogCheckbox> {
      bool value;
    
      @override
      void initState() {
        value = widget.value;
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Checkbox(
          value: value,
          onChanged: (v) {
            //将选中状态通过事件的形式抛出
            widget.onChanged(v);
            setState(() {
              //更新自身选中状态
              value = v;
            });
          },
        );
      }
    }
    
    Future<bool> showDeleteConfirmDialog3() {
      bool _withTree = false; //记录复选框是否选中
      return showDialog<bool>(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text("提示"),
            content: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Text("您确定要删除当前文件吗?"),
                Row(
                  children: <Widget>[
                    Text("同时删除子目录?"),
                    DialogCheckbox(
                      value: _withTree, //默认不选中
                      onChanged: (bool value) {
                        //更新选中状态
                        _withTree = !_withTree;
                      },
                    ),
                  ],
                ),
              ],
            ),
            actions: <Widget>[
              FlatButton(
                child: Text("取消"),
                onPressed: () => Navigator.of(context).pop(),
              ),
              FlatButton(
                child: Text("删除"),
                onPressed: () {
                  // 将选中状态返回
                  Navigator.of(context).pop(_withTree);
                },
              ),
            ],
          );
        },
      );
    }
    
    RaisedButton(
      child: Text("话框3(复选框可点击)"),
      onPressed: () async {
        //弹出删除确认对话框,等待用户确认
        bool deleteTree = await showDeleteConfirmDialog3();
        if (deleteTree == null) {
          print("取消删除");
        } else {
          print("同时删除子目录: $deleteTree");
        }
      },
    ),
    
    

    方法2:使用StatefulBuilder方法

    方法1有一个明显的缺点——对话框上所有可能会改变状态的组件都得单独封装在一个在内部管理状态的StatefulWidget中,这样不仅麻烦,而且复用性不大。
    
    上面的方法本质上就是将对话框的状态置于一个StatefulWidget的上下文中,由StatefulWidget在内部管理,有没有办法在不需要单独抽离组件的情况下创建一个StatefulWidget的上下文呢?
    Builder组件可以获得组件所在位置的真正的Context
    class Builder extends StatelessWidget {
      const Builder({
        Key key,
        @required this.builder,
      }) : assert(builder != null),
           super(key: key);
      final WidgetBuilder builder;
    
      @override
      Widget build(BuildContext context) => builder(context);
    }
    可以看到,Builder实际上只是继承了StatelessWidget,然后在build方法中获取当前context后将构建方法代理到了builder回调,可见,Builder实际上是获取了StatelessWidget 的上下文(context)。那么我们能否用相同的方法获取StatefulWidget 的上下文,并代理其build方法
    
    class StatefulBuilder extends StatefulWidget {
      const StatefulBuilder({
        Key key,
        @required this.builder,
      }) : assert(builder != null),
           super(key: key);
    
      final StatefulWidgetBuilder builder;
    
      @override
      _StatefulBuilderState createState() => _StatefulBuilderState();
    }
    
    class _StatefulBuilderState extends State<StatefulBuilder> {
      @override
      Widget build(BuildContext context) => widget.builder(context, setState);
    }
    
    ... //省略无关代码
    Row(
      children: <Widget>[
        Text("同时删除子目录?"),
        //使用StatefulBuilder来构建StatefulWidget上下文
        StatefulBuilder(
          builder: (context, _setState) {
            return Checkbox(
              value: _withTree, //默认不选中
              onChanged: (bool value) {
                //_setState方法实际就是该StatefulWidget的setState方法,
                //调用后builder方法会重新被调用
                _setState(() {
                  //更新选中状态
                  _withTree = !_withTree;
                });
              },
            );
          },
        ),
      ],
    ),
    
    方法本质上就是子组件通知父组件(StatefulWidget)重新build子组件本身来实现UI更新的
    

    方法3: 更好的办法

    在调用setState方法后StatefulWidget就会重新build
    void setState(VoidCallback fn) {
      ... //省略无关代码
      _element.markNeedsBuild();
    }
    可以发现,setState中调用了Element的markNeedsBuild()方法,我们前面说过,Flutter是一个响应式框架,要更新UI只需改变状态后通知框架页面需要重构即可,而Element的markNeedsBuild()方法正是来实现这个功能的!markNeedsBuild()方法会将当前的Element对象标记为“dirty”(脏的),在每一个Frame,Flutter都会重新构建被标记为“dirty”Element对象
    
    只要获取到对话框内部UI的Element对象,然后将其标示为为“dirty”。
    
    Future<bool> showDeleteConfirmDialog4() {
      bool _withTree = false;
      return showDialog<bool>(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text("提示"),
            content: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Text("您确定要删除当前文件吗?"),
                Row(
                  children: <Widget>[
                    Text("同时删除子目录?"),
                    Checkbox( // 依然使用Checkbox组件
                      value: _withTree,
                      onChanged: (bool value) {
                        // 此时context为对话框UI的根Element,我们 
                        // 直接将对话框UI对应的Element标记为dirty
                        (context as Element).markNeedsBuild();
                        _withTree = !_withTree;
                      },
                    ),
                  ],
                ),
              ],
            ),
            actions: <Widget>[
              FlatButton(
                child: Text("取消"),
                onPressed: () => Navigator.of(context).pop(),
              ),
              FlatButton(
                child: Text("删除"),
                onPressed: () {
                  // 执行删除操作
                  Navigator.of(context).pop(_withTree);
                },
              ),
            ],
          );
        },
      );
    }
    
    
    优化(只将Checkbox的Element标记为dirty):
    
    ... //省略无关代码
    Row(
      children: <Widget>[
        Text("同时删除子目录?"),
        // 通过Builder来获得构建Checkbox的`context`,
        // 这是一种常用的缩小`context`范围的方式
        Builder(
          builder: (BuildContext context) {
            return Checkbox(
              value: _withTree,
              onChanged: (bool value) {
                (context as Element).markNeedsBuild();
                _withTree = !_withTree;
              },
            );
          },
        ),
      ],
    ),
    

    底部菜单列表 (showModalBottomSheet方法、showBottomSheet方法)

    showModalBottomSheet方法

    从设备底部向上弹出一个Material风格的全屏菜单列表对话框
    
    showModalBottomSheet的实现原理和showGeneralDialog实现原理相同,都是通过路由的方式来实现的。
    
    showBottomSheet是调用widget树顶部的Scaffold组件的ScaffoldState的showBottomSheet同名方法实现,也就是说要调用showBottomSheet方法就必须得保证父级组件中有Scaffold。
    
    RaisedButton(
      child: Text("显示底部菜单列表"),
      onPressed: () async {
        int type = await _showModalBottomSheet();
        print(type);
      },
    ),
    
    // 弹出底部菜单列表模态对话框
    Future<int> _showModalBottomSheet() {
      return showModalBottomSheet<int>(
        context: context,
        builder: (BuildContext context) {
          return ListView.builder(
            itemCount: 30,  // 个数
            itemBuilder: (BuildContext context, int index) {  // itemBuilder
              return ListTile(
                title: Text("$index"),
                onTap: () => Navigator.of(context).pop(index),
              );
            },
          );
        },
      );
    }
    

    showBottomSheet方法

    // 返回的是一个controller,包含了一些控制对话框的方法比如close方法可以关闭该对话框
    PersistentBottomSheetController<int> _showBottomSheet() {
      return showBottomSheet<int>(
        context: context,
        builder: (BuildContext context) {
          return ListView.builder(
            itemCount: 30,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(
                title: Text("$index"),
                onTap: (){
                  // do something
                  print("$index");
                  Navigator.of(context).pop();
                },
              );
            },
          );
        },
      );
    }
    

    Loading框

    Loading框可以直接通过showDialog+AlertDialog来自定义
    
    showLoadingDialog() {
      showDialog(
        context: context,
        barrierDismissible: false, //点击遮罩不关闭对话框
        builder: (context) {
          return AlertDialog(
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                CircularProgressIndicator(),
                Padding(
                  padding: const EdgeInsets.only(top: 26.0),
                  child: Text("正在加载,请稍后..."),
                )
              ],
            ),
          );
        },
      );
    }
    
    
    如果我们嫌Loading框太宽,想自定义对话框宽度,这时只使用SizedBox或ConstrainedBox是不行的,原因是showDialog中已经给对话框设置了宽度限制,我们可以使用UnconstrainedBox先抵消showDialog对宽度的限制,然后再使用SizedBox指定宽度
    ... //省略无关代码
    UnconstrainedBox(
      constrainedAxis: Axis.vertical,
      child: SizedBox(
        width: 280,
        child: AlertDialog(
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              CircularProgressIndicator(value: .8,),
              Padding(
                padding: const EdgeInsets.only(top: 26.0),
                child: Text("正在加载,请稍后..."),
              )
            ],
          ),
        ),
      ),
    );
    

    弹框三方库:fluttertoast

    1. 添加依赖包,并下载(pubspec.yaml文件中dependencies下)
    fluttertoast: #lastversion
    
    2. 导入包
    import 'package:fluttertoast/fluttertoast.dart';
    
    3. 使用
    Fluttertoast.showToast(
        msg: "加载中",  // 提示文本
        toastLength: Toast.LENGTH_SHORT,  // 类型:短、长
        gravity: ToastGravity.CENTER, // 位置
        timeInSecForIosWeb: 1,    // 持续时间
        backgroundColor: Colors.red,  // 背景色
        textColor: Colors.white,  // 文本色
        fontSize: 16.0, // 文本字体
    ),
    

    9. AspectRatio 宽高比组件

    设置子元素宽高比(宽度尽可能扩展,高度由宽高比决定)。

      AspectRatio(
        aspectRatio: 2.0/1.0,   // 宽高比
        child: Container( // 子组件
          color: Colors.blue,
        ),
      )
    

    Card 卡片组件(Meterial组件库)

      Card(
        margin: EdgeInsets.all(10.0), // 外边距
        child: Column( // 子组件
          children: <Widget>[
            ListTile(
              title: Text("张三"),
              subtitle: Text("男"),
            )
          ],
        ),
        // shape: ,  // 默认圆角阴影
      )
    

    相关文章

      网友评论

        本文标题:Flutter了解之常用组件

        本文链接:https://www.haomeiwen.com/subject/loelektx.html