Flutter 笔记 | Flutter 基础组件

Text

Text 用于显示简单样式文本,它包含一些控制文本显示样式的一些属性,一个简单的例子如下:

Text("Hello world",
  textAlign: TextAlign.left,
);

Text("Hello world! I'm Jack. "*4,
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
);

Text("Hello world",
  textScaleFactor: 1.5,
);

下面是Text的常用属性:

属性功能
textAlign文本对齐方式(center居中,left左对齐,right右对齐,justfy两端对齐)
textDirection文本方向(ltr从左至右,rtl从右至左)
overflow文字超出屏幕之后的处理方式(clip裁剪,fade渐隐,ellipsis省略号)
textScaleFactor字体显示大小的缩放因子
maxLines文字显示最大行数
style字体的样式设置

注意, textAlign 对齐的参考系是Text widget 本身。如果 Text 文本内容宽度不足一行,Text 的宽度和文本内容长度相等,那么这时指定对齐方式是没有意义的,只有 Text 宽度大于文本内容长度时指定此属性才有意义。下面我们指定一个较长的字符串:

Text("Hello world "*6,  //字符串重复六次
  textAlign: TextAlign.center,
)

运行效果:
在这里插入图片描述

​ 字符串内容超过一行,Text 宽度等于屏幕宽度,第二行文本便会居中显示。

TextStyle

TextStyle用于指定文本显示的样式如颜色、字体、粗细、背景等。下面是TextStyle的常用属性:

属性功能
decoration文字装饰线(none没有线,lineThrough删除线,overline上划线, underline 下划线
decorationColor文字装饰线颜色
decorationStyle文字装饰线风格([dashed,dotted]虚线,double两根线,solid一根实线,wavy波浪线)
wordSpacing单词间隙(如果是负值,会让单词变得更紧凑
letterSpacing字母间隙(如果是负值,会让字母变得更紧凑)
fontFamily指定字体
fontStyle文字样式(italic斜体,normal正常体)
fontSize文字大小
color文字颜色
fontWeight字体粗细(bold粗体,normal正常体)
height指定行高,但它不是绝对值只是一个因子,具体的行高等于fontSize*height

简单示例:

Text("Hello world",
  style: TextStyle(
    color: Colors.blue,
    fontSize: 18.0,
    height: 1.2,  
    fontFamily: "Courier",
    background: Paint()..color=Colors.yellow,
    decoration:TextDecoration.underline,
    decorationStyle: TextDecorationStyle.dashed
  ),
);

效果:

在这里插入图片描述

fontStyletextScaleFactor都用于控制字体大小,但是有两个主要区别:

  • fontSize可以精确指定字体大小,而textScaleFactor只能通过缩放比例来控制。
  • textScaleFactor主要是用于系统字体大小设置改变时对 Flutter 应用字体进行全局调整,而fontSize通常用于单个文本,字体大小不会跟随系统字体大小变化。

TextSpan

如果我们需要对一个 Text 内容的不同部分按照不同的样式显示,这时就可以使用TextSpan,它代表文本的一个“片段”。我们看看 TextSpan 的定义:

const TextSpan({
  TextStyle style, 
  Sting text,
  List<TextSpan> children,
  GestureRecognizer recognizer,
});

其中styletext属性代表该文本片段的样式和内容。 children是一个TextSpan的数组,也就是说TextSpan可以包括其他TextSpan。而recognizer用于对该文本片段上用于手势进行识别处理。下面我们看一个效果,然后用TextSpan实现它:

在这里插入图片描述
源码:

Text.rich(TextSpan(
    children: [
     TextSpan(
       text: "Home: "
     ),
     TextSpan(
       text: "https://flutterchina.club",
       style: TextStyle(
         color: Colors.blue
       ),  
       recognizer: TapGestureRecognizer()..onTap = () { },
     ),
    ]
))

上面代码中,我们通过 TextSpan 实现了一个基础文本片段和一个链接片段,然后通过Text.rich 方法将TextSpan 添加到 Text 中,之所以可以这样做,是因为 Text 其实就是 RichText 的一个包装,而RichText 是可以显示多种样式(富文本)的 widgetrecognizer,它是点击链接后的一个处理器。

RichText 使用示例:

RichText(
  text: TextSpan(
      style: DefaultTextStyle.of(context).style,
      children: const <InlineSpan>[
        TextSpan(text: '你好', style:
        TextStyle(color: Colors.red)),
        TextSpan(text: ','),
        TextSpan(text: 'Flutter'),
      ]
),

注意:RichText 组件的 text 属性是 TextSpanTextSpan 中的 style 样式需要设置属性,不设置无法显示文字,一般设置应用程序的默认字体样式 DefaultTextStyle.of(context).style,在子组件的 TextSpan 可以设置不同的样式。

DefaultTextStyle

Widget 树中,文本的样式默认是可以被继承的(子类文本类组件未指定具体样式时可以使用 Widget 树中父级设置的默认样式),因此,如果在 Widget 树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式,而DefaultTextStyle正是用于设置默认文本样式的。下面我们看一个例子:

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
        ),
      ),
    ],
  ),
);

上面代码中,我们首先设置了一个默认的文本样式,即字体为20像素(逻辑像素)、颜色为红色。然后通过DefaultTextStyle 设置给了子树 Column 节点处,这样一来 Column 的所有子孙 Text 默认都会继承该样式,除非 Text 显示指定不继承样式,如代码中注释2。示例运行效果如图:
在这里插入图片描述

添加字体文件

可以在 Flutter 应用程序中使用不同的字体。例如,我们可能会使用设计人员创建的自定义字体,或者其他第三方的字体,如 iconfont 中的字体。

在 Flutter 中使用字体分两步完成。首先在pubspec.yaml中声明它们,以确保它们会打包到应用程序中。然后通过TextStyle 属性使用字体。

1. 在asset中声明

要将字体文件打包到应用中,和使用其他资源一样,要先在pubspec.yaml中声明它。然后将字体文件复制到在pubspec.yaml中指定的位置,如:

flutter:
  fonts:
    - family: Raleway
      fonts:
        - asset: assets/fonts/Raleway-Regular.ttf
        - asset: assets/fonts/Raleway-Medium.ttf
          weight: 500
        - asset: assets/fonts/Raleway-SemiBold.ttf
          weight: 600
    - family: AbrilFatface
      fonts:
        - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf

2. 使用字体

// 声明文本样式
const textStyle = const TextStyle(
  fontFamily: 'Raleway',
);

// 使用文本样式
var buttonText = const Text(
  "Use the font for this text",
  style: textStyle,
);

3. Package中的字体

要使用 Package 中定义的字体,必须提供package参数。例如,假设上面的字体声明位于my_package包中。然后创建 TextStyle 的过程如下:

const textStyle = const TextStyle(
  fontFamily: 'Raleway',
  package: 'my_package', //指定包名
);

如果在 package 包内部使用它自己定义的字体,也应该在创建文本样式时指定package参数,如上例所示。

一个包也可以只提供字体文件而不需要在 pubspec.yaml 中声明。 这些文件应该存放在包的lib文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件:

lib/fonts/Raleway-Medium.ttf

然后,应用程序可以声明一个字体,如下面的示例所示:

 flutter:
   fonts:
     - family: Raleway
       fonts:
         - asset: assets/fonts/Raleway-Regular.ttf
         - asset: packages/my_package/fonts/Raleway-Medium.ttf
           weight: 500

lib/是隐含的,所以它不应该包含在 asset 路径中。

在这种情况下,由于应用程序本地定义了字体,所以在创建TextStyle时可以不指定package参数:

const textStyle = const TextStyle(
  fontFamily: 'Raleway',
);

SelectableText

SelectableText显示的文本可以被选中,并可以复制、剪切等。

属性说明
style文本样式,同Text组件
textAlign文本对齐方式,同Text组件
textDirection文本方向,同Text组件
showCursor是否显示光标
cursorRadius光标半径
cursorColor光标颜色
cursorWidth光标宽度
autofocus是否自动聚焦
contextMenuBuilder上下文菜单
onTap单击事件

示例:

import 'package:flutter/material.dart';

class SelectableTextWidget extends StatelessWidget {
  const SelectableTextWidget({Key? key}) : super(key: key);

  final text = "        始臣之解牛之时,所见无非牛者。三年之后,未尝见全牛也。方今之时,"
      "臣以神遇而不以目视,官知止而神欲行。依乎天理,批大郤,导大窾,因其固然,"
      "技经肯綮之未尝,而况大軱乎!良庖岁更刀,割也;族庖月更刀,折也。"
      "今臣之刀十九年矣,所解数千牛矣,而刀刃若新发于硎。彼节者有间,而刀刃者无厚;"
      "以无厚入有间,恢恢乎其于游刃必有余地矣,是以十九年而刀刃若新发于硎。"
      "虽然,每至于族,吾见其难为,怵然为戒,视为止,行为迟。动刀甚微,謋然已解,如土委地。"
      "提刀而立,为之四顾,为之踌躇满志,善刀而藏之.";

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SelectableText"),
      ),
      body: SelectableText(
        text,
        style: const TextStyle(fontSize: 18, color: Colors.orange),
        cursorColor: Colors.green,
        cursorRadius: const Radius.circular(3),
        cursorWidth: 5,
        showCursor: true,
        autofocus: false,
        contextMenuBuilder: (context, editableTextState) {
          return AdaptiveTextSelectionToolbar(
            anchors: editableTextState.contextMenuAnchors,
            children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
              context,
              editableTextState.contextMenuButtonItems,
            ).toList(),
          );
        },
      ),
    );
  }
}

效果:

在这里插入图片描述

当文字超过文本框的时候,可向下滚动显示更多的文本,用法如下:

Container(
	height: 100, 
	width: 250, 
	child: SelectableText(
		'Flutter 基础组件Flutter 基础组件Flutter 基础组件'
		'Flutter 基础组件Flutter 基础组件Flutter 基础组件'
		'Flutter 基础组件Flutter 基础组件Flutter 基础组件'
		'Flutter 基础组件Flutter 基础组件Flutter 基础组件', 
		scrollPhysics: ClampingScrollPhysics(), 
	), 
)

通过 SelectableText.rich 可以支持多种样式的富文本,和 Text.rich 或者 RichText 用法一样:

 const SelectableText.rich(
   TextSpan(
     text: 'Hello', // default text style
     children: <TextSpan>[
       TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
       TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
     ],
   ),
 )

Button

Material 组件库中提供了多种按钮组件如ElevatedButtonTextButtonOutlineButton等,它们都是直接或间接对RawMaterialButton组件的包装和定制,所以他们大多数属性都和RawMaterialButton一样。

所有 Material 库中的按钮都有如下相同点:

  • 按下时都会有“水波动画”(又称“涟漪动画”,就是点击时按钮上会出现水波扩散的动画)。
  • 有一个onPressed属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。

Button通用属性:

属性说明
onPressed必填参数,按下按钮时触发的回调,接收一个方法,传null表示按钮禁用,会显示禁用相关样式
child子组件
style通过ButtonStyle装饰

ButtonStyle里面的常用的参数:

属性说明
foregroundColor文本颜色
backgroundColor按钮的颜色
shadowColor阴影颜色
elevation阴影的范围,值越大阴影范围越大
padding内边距
shape设置按钮的形状
side设置边框

ElevatedButton

"漂浮"按钮,它默认带有阴影和灰色背景。按下后,阴影会变大。

在这里插入图片描述

ElevatedButton(
  child: Text("normal"),
  onPressed: () {},
);

TextButton

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

在这里插入图片描述

TextButton(
  child: Text("normal"),
  onPressed: () {},
)

OutlineButton

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

在这里插入图片描述

OutlineButton(
  child: Text("normal"),
  onPressed: () {},
)

IconButton

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

在这里插入图片描述

IconButton(
  icon: Icon(Icons.thumb_up),
  onPressed: () {},
)

带图标的按钮

ElevatedButtonTextButtonOutlineButton都有一个icon 构造函数,通过它可以轻松创建带图标的按钮:

在这里插入图片描述

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

修改按钮的宽度高度

SizedBox(
  height: 80,
  width: 200,
  child: ElevatedButton(
    style:ButtonStyle(
        backgroundColor:MaterialStateProperty.all(Colors.red),
        foregroundColor: MaterialStateProperty.all(Colors.black)
    ) ,
    onPressed: () {
    },
    child: const Text('宽度高度'),
  ),
)

在这里插入图片描述

自适应按钮

Row(
  mainAxisAlignment: MainAxisAlignment.center,
   children: <Widget>[
     Expanded(
       child: Container(
         height: 60,
         margin: const EdgeInsets.all(10),
         child: ElevatedButton(
           child: const Text('自适应按钮'),
           onPressed: () {
             print("自适应按钮");
           },
         ),
       ),
     )
   ],
 ),

在这里插入图片描述

圆形和圆角按钮

圆角按钮:

ElevatedButton(
    style: ButtonStyle(
      backgroundColor:MaterialStateProperty.all(Colors.blue),
      foregroundColor: MaterialStateProperty.all(Colors.white),
      elevation: MaterialStateProperty.all(20),
      shape: MaterialStateProperty.all(
          RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10))
      ),
    ),
    onPressed: () {
      print("圆角按钮");
    },
    child: const Text('圆角')
)

在这里插入图片描述

圆形按钮:

Container(
   height: 80,
    child: ElevatedButton(
        style: ButtonStyle(
            style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all(Colors.blue),
                 foregroundColor:MaterialStateProperty.all(Colors.white),
                 elevation: MaterialStateProperty.all(20),
                 shape: MaterialStateProperty.all(
                   const CircleBorder(side: BorderSide(color: Colors.white)),
             )),
	    onPressed: () {
	      print("圆形按钮");
	    },
	    child: const Text('圆形按钮')),
)

在这里插入图片描述

修改OutlinedButton边框

Row(
  mainAxisAlignment: MainAxisAlignment.center,
   children: <Widget>[
     Expanded(
       child: Container(
         margin: const EdgeInsets.all(20),
         height: 50,
         child: OutlinedButton(
             style: ButtonStyle(
                 foregroundColor:
                 MaterialStateProperty.all(Colors.black),
                 side: MaterialStateProperty.all(
                     const BorderSide(width: 1, color: Colors.red))),
             onPressed: () {},
             child: const Text("注册")),
       ),
     )
   ],
 )

在这里插入图片描述

Image

Flutter 中,我们可以通过Image组件来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络。

ImageProvider

ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的 ImageProvider,而NetworkImage 实现了从网络加载图片的 ImageProvider

Image 的使用

Image widget 有一个必选的image参数,它对应一个 ImageProvider。下面我们分别演示一下如何从 asset 和网络加载图片。

从asset中加载图片
  1. 在工程根目录下创建一个images目录,并将图片 avatar.png 拷贝到该目录。

  2. pubspec.yaml中的flutter部分添加如下内容:

  assets:
    - images/avatar.png

注意: 由于 yaml 文件对缩进严格,所以必须严格按照每一层两个空格的方式进行缩进,此处 assets 前面应有两个空格。

  1. 加载该图片
Image(image: AssetImage("images/avatar.png"), width: 100.0);

Image也提供了一个快捷的构造函数Image.asset用于从asset中加载、显示图片:

Image.asset("images/avatar.png", width: 100.0,)

加载分辨率相关的本地图片

  1. 在工程根目录下创建一个images目录,并在images中新建2.0x3.0x对应的目录
    在这里插入图片描述

  2. pubspec.yaml 声明添加的图片文件, 注意空格
    在这里插入图片描述

  3. 使用

Image.asset(
	"images/a.jpeg",
	width: 150.0,
	height: 150.0,
	fit: BoxFit.cover,
),
从网络加载图片

NetworkImage 可以加载网络图片,例如:

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

Image也提供了一个快捷的构造函数Image.network用于从网络加载、显示图片:

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

运行上面两个示例,图片加载成功后如图3-11所示:

Image 的参数

Image在显示图片时定义了一系列参数,通过这些参数我们可以控制图片的显示外观、大小、混合效果等。

Image常用属性:

名称类型说明
alignmentAlignment图片的对齐方式
colorcolorBlendMode在图片绘制时可以对每一个像素进行颜色混合处理,color指定混合色,而colorBlendMode指定混合模式
fitBoxFitfit 属性用来控制图片的拉伸和挤压,这都是根据父容器来的。
repeatImageRepeat当图片本身大小小于显示空间时,指定图片的重复规则
width宽度 一般结合ClipOval才能看到效果
height高度 一般结合ClipOval才能看到效果
  • width、height:用于设置图片的宽、高,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置width、height的其中一个,那么另一个属性默认会按比例缩放,但可以通过fit属性来指定适应规则。

  • fit:该属性用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在BoxFit中定义,它是一个枚举类型,有如下值:

    • BoxFit.fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。
    • BoxFit.cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。
    • BoxFit.contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。
    • BoxFit.fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    • BoxFit.fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    • BoxFit.none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。

一图胜万言! 我们对一个宽高相同的头像图片应用不同的fit值,效果如图所示:

在这里插入图片描述

colorcolorBlendMode的使用示例:

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

运行效果如图:

在这里插入图片描述

  • repeat:当图片本身大小小于显示空间时,指定图片的重复规则。取值为ImageRepeat枚举值:

    • ImageRepeat.repeat :横向和纵向都进行重复,直到铺满整个画布。
    • ImageRepeat.repeatX:横向重复,纵向不重复。
    • ImageRepeat.repeatY:纵向重复,横向不重复。
    • ImageRepeat.noRepeat:不重复。

简单示例如下:

Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0,
  height: 200.0,
  repeat: ImageRepeat.repeatY ,
)

运行后效果如图所示:
在这里插入图片描述

完整的示例代码如下:

import 'package:flutter/material.dart';

class ImageAndIconRoute extends StatelessWidget {
  
  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()
      ),
    );
  }
}

Container实现圆形图片

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 150,
        height: 150,
        decoration: BoxDecoration(
            color: Colors.yellow,
            borderRadius: BorderRadius.circular(75),
            image: const DecorationImage(
                image: NetworkImage(
                  "https://www.itying.com/themes/itying/images/ionic4.png",
                ),
                fit: BoxFit.cover)
        ),
      ),
    );
  }
}

ClipOval实现圆形图片

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Center(
      child: ClipOval(
        child: Image.network(
            "https://www.itying.com/themes/itying/images/ionic4.png",
            width: 150.0,
            height: 150.0,
            fit: BoxFit.cover),
      ),
    );
  }
}

CircleAvatar实现圆形图片

const CircleAvatar(
  radius: 200,
  backgroundImage: NetworkImage("https://www.itying.com/images/flutter/3.png"),
)

基本上,CircleAvatar 不提供设置边框的属性。但是,可以将其包裹在具有更大半径和不同背景颜色的不同 CircleAvatar 中,以创建类似于边框的内容。

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return const CircleAvatar(
        radius: 110,
        backgroundColor: Color(0xffFDCF09),
        child: CircleAvatar(
          radius: 100,
          backgroundImage:
          NetworkImage("https://www.itying.com/images/flutter/3.png"),
        )
    );
  }
}

Icon

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

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

在Flutter开发中,iconfont和图片相比有如下优势:

  • 体积小:可以减小安装包大小。
  • 矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
  • 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
  • 可以通过TextSpan和文本混用。

使用Material Design字体图标

Flutter默认包含了一套Material Design的字体图标,在pubspec.yaml文件中的配置如下

flutter:
  uses-material-design: true

Material Design所有图标可以在其官网查看:https://fonts.google.com/icons/

我们看一个简单的例子:

String icons = "";
// accessible: 0xe03e
icons += "\uE03e";
// error:  0xe237
icons += " \uE237";
// fingerprint: 0xe287
icons += " \uE287";

Text(
  icons,
  style: TextStyle(
    fontFamily: "MaterialIcons",
    fontSize: 24.0,
    color: Colors.green,
  ),
);

运行效果如图所示:

图3-15

通过这个示例可以看到,使用图标就像使用文本一样,但是这种方式需要我们提供每个图标的码点,这并对开发者不友好,所以,Flutter封装了IconDataIcon来专门显示字体图标,上面的例子也可以用如下方式实现:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(Icons.accessible,color: Colors.green),
    Icon(Icons.error,color: Colors.green),
    Icon(Icons.fingerprint,color: Colors.green),
  ],
)

Icons类中包含了所有Material Design图标的IconData静态变量定义。

使用自定义字体图标

我们也可以使用自定义字体图标。iconfont.cn上有很多字体图标素材,我们可以选择自己需要的图标打包下载后,会生成一些不同格式的字体文件,在Flutter中,我们使用ttf格式即可。

假设我们项目中需要使用一个书籍图标和微信图标,我们打包下载后导入:

1. 导入字体图标文件;这一步和导入字体文件相同,假设我们的字体图标文件保存在项目根目录下,路径为"fonts/iconfont.ttf":

fonts:
  - family: myIcon  #指定一个字体名
    fonts:
      - asset: fonts/iconfont.ttf

也可以配置多个字体文件:

fonts:
  - family: myIcon #指定一个字体名
    fonts:
      - asset: fonts/iconfont.ttf
  - family: alipayIcon #指定一个字体名
    fonts:
      - asset: fonts/iconfont2.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),
  ],
)

运行后效果如图所示:

图3-16

CheckBox 和 Switch

Material 组件库中提供了 Material 风格的单选开关Switch和复选框Checkbox,虽然它们都是继承自StatefulWidget,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。当SwitchCheckbox被点击时,会触发它们的onChanged回调,我们可以在此回调中处理选中状态改变逻辑。

下面是一个简单的例子:

class SwitchAndCheckBoxTestRoute extends StatefulWidget {
  
  _SwitchAndCheckBoxTestRouteState createState() => _SwitchAndCheckBoxTestRouteState();
}

class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
  bool _switchSelected=true; //维护单选开关状态
  bool? _checkboxSelected=true;//维护复选框状态
  
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Switch(
          value: _switchSelected,//当前状态
          onChanged:(value){
            //重新构建页面  
            setState(() {
              _switchSelected=value;
            });
          },
        ),
        Checkbox(
          value: _checkboxSelected,
          activeColor: Colors.red, //选中时的颜色
          onChanged:(value){
            setState(() {
              _checkboxSelected=value;
            });
          } ,
        )
      ],
    );
  }
}

在这里插入图片描述

SwitchCheckbox 属性比较简单,可自行查阅API文档,它们都有一个activeColor属性,用于设置激活态的颜色。至于大小,到目前为止,Checkbox的大小是固定的,无法自定义,而Switch只能定义宽度,高度也是固定的。

值得一提的是Checkbox有一个属性tristate ,表示是否为三态,其默认值为false ,这时 Checkbox 有两种状态即“选中”和“不选中”;如果tristate值为true时,Checkboxvalue 的值会有三种:truefalsenull

通过Switch和Checkbox我们可以看到,虽然它们本身是与状态(是否选中)关联的,但它们却不是自己来维护状态,而是需要父组件来管理状态,然后当用户点击时,再通过事件通知给父组件,这样是合理的,因为Switch和Checkbox是否选中本就和用户数据关联,而这些用户数据也不可能是它们的私有状态。我们在自定义组件时也应该思考一下哪种状态的管理方式最为合理。

CheckboxListTile

CheckboxListTile主要用于方便的实现一组多选按钮组,例如:

class CheckboxListTilePage extends StatefulWidget {
  const CheckboxListTilePage({super.key});
  
  State<CheckboxListTilePage> createState() => _CheckboxListTilePageState();
}
class _CheckboxListTilePageState extends State<CheckboxListTilePage> {
  final List _hobby = [
    {"checked": true, "title": "吃饭"},
    {"checked": false, "title": "睡觉"},
    {"checked": true, "title": "写代码"}
  ];
  List<Widget> _initHobby() {
    List<Widget> tempList = [];

    for (var i = 0; i < _hobby.length; i++) {
      tempList.add(
          CheckboxListTile(
            title: Text(_hobby[i]["title"]),
            value: _hobby[i]["checked"],
            onChanged: (value) {setState(() => _hobby[i]["checked"] = value);}
          )
      );
    }
    return tempList;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Title'),
      ),
      body: ListView(
        children: [
          Text(
            "爱好:",
            style: Theme.of(context).textTheme.headline6,
          ),
          Column(
            children: _initHobby(),
          ),
          const SizedBox(height: 40),
          ElevatedButton(
              onPressed: () {
                print(_hobby);
              },
              child: const Text("获取值"))
        ],
      ),
    );
  }
}

在这里插入图片描述

Radio

Radio可以用来实现单选按钮组

import 'package:flutter/material.dart';

class RadioPage extends StatefulWidget {
  const RadioPage({super.key});
  
  State<RadioPage> createState() => _RadioPageState();
}

class _RadioPageState extends State<RadioPage> {
  int sex = 1;
  int flag=1;
  _onChanged(value) {
    setState(() {
      sex = value;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Radio"),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
             mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text("男:"),
              Radio(value: 1, groupValue: sex, onChanged: _onChanged),
              const SizedBox(width: 20),
              const Text("女:"),
              Radio(value: 2, groupValue: sex, onChanged: _onChanged)
            ],
          ),
          const SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(sex==1?"男":"女")
            ],
          ), 
        ],
      ),
    );
  }
}

在这里插入图片描述

RadioListTile

CheckboxListTile类似,RadioListTile可以方便的实现一组单选按钮组:

RadioListTile(
 	title: Text("男"),
    value: 1,
    groupValue: sex,
    onChanged: _radioChange),
RadioListTile(
    title: Text("女"),
    value: 2,
    groupValue: sex,
    onChanged: _radioChange),

TextField

TextField表单常见属性:

属性描述
maxLines设置输入框的最大行数,默认为1;如果为null,则无行数限制
onChanged输入框内容改变时的回调函数;注:内容改变事件也可以通过controller来监听
decoration用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
hintText 类似html中的placeholder
border 配置文本框边框 OutlineInputBorder配合使用
labelText lable的名称
labelStyle 配置lable的样式
obscureText把文本框框改为密码框
controller结合TextEditingController() 可以配置表单默认显示的内容,
通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。
大多数情况下我们都需要显式提供一个controller来与文本框交互。
如果没有提供controller,则TextField内部会自动创建一个。
focusNode用于控制TextField是否占有当前键盘的输入焦点。
它是我们和键盘交互的一个句柄(handle)
keyboardType用于设置该输入框默认的键盘输入类型
textInputAction键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值,
TextInputAction.search会在键盘右下角显示一个放大镜图标
style正在编辑的文本样式
textAlign输入框内编辑文本在水平方向的对齐方式
autofocus是否自动获取焦点
obscureText是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“•”替换
maxLength输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数
maxLengthEnforcement决定当输入文本长度超过maxLength时如何处理,如截断、超出等
toolbarOptions长按或鼠标右击时出现的菜单,包括 copy、cut、paste 以及 selectAll
onEditingCompleteonSubmitted这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键(对号图标)或搜索键(🔍图标)。
不同的是两个回调签名不同,onSubmitted回调是ValueChanged<String>类型,它接收当前输入内容做为参数,
onEditingComplete不接收参数。
inputFormatters用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验
enable设置输入框是否禁用,禁用状态不能响应输入和事件,同时显示禁用态样式
cursorWidthcursorRadiuscursorColor这三个属性是用于自定义输入框光标宽度、圆角和颜色的

其中,keyboardType的取值如下:

TextInputType枚举值含义
text文本输入键盘
multiline多行文本,需和maxLines配合使用(设为null或大于1)
number数字;会弹出数字键盘
phone优化后的电话号码输入键盘;会弹出数字键盘并显示“* #
datetime优化后的日期输入键盘;Android上会显示“: -”
emailAddress优化后的电子邮件地址;会显示“@ .”
url优化后的url输入键盘; 会显示“/ .”

示例:实现一个登录表单

1)布局

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,
    ),
  ],
);

运行效果:

在这里插入图片描述

2)获取输入内容

获取输入内容有两种方式:

  1. 定义两个变量,用于保存用户名和密码,然后在onChange触发时,各自保存一下输入内容。
  2. 通过controller直接获取。

第一种方式比较简单,不在举例,我们来重点看一下第二种方式,我们以用户名输入框举例,首先定义一个controller

TextEditingController _unameController = TextEditingController();

然后设置输入框controller

TextField(
    autofocus: true,
    controller: _unameController, //设置controller
    ...
)

通过controller获取输入框内容:

print(_unameController.text)

3)监听文本变化

监听文本变化也有两种方式:

  1. 通过设置onChange回调监听,如:
TextField(
    autofocus: true,
    onChanged: (v) {
      print("onChange: $v");
    }
)
  1. 通过controller监听,如:

void initState() {
  //监听输入改变  
  _unameController.addListener((){
    print(_unameController.text);
  });
}

两种方式相比,onChanged是专门用于监听文本变化,而controller的功能却多一些,除了能监听文本变化外,它还可以设置默认值、选择文本,下面我们看一个例子:

创建一个controller:

TextEditingController _selectionController =  TextEditingController();

设置默认值,并从第三个字符开始选中后面的字符:

_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
    baseOffset: 2,
    extentOffset: _selectionController.text.length
);

设置controller:

TextField(
  controller: _selectionController,
)

运行效果:

图3-20
4)控制焦点

焦点可以通过FocusNodeFocusScopeNode来控制,默认情况下,焦点由FocusScope来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode在输入框之间移动焦点、设置默认焦点等。

我们可以通过FocusScope.of(context) 来获取Widget树中默认FocusScopeNode

下面看一个示例,在此示例中创建两个TextField,第一个自动获取焦点,然后创建两个按钮:

  • 点击第一个按钮可以将焦点从第一个TextField挪到第二个TextField
  • 点击第二个按钮可以关闭键盘。

要实现的效果如图所示:

在这里插入图片描述

代码如下:

class FocusTestRoute extends StatefulWidget {
  
  _FocusTestRouteState createState() => _FocusTestRouteState();
}

class _FocusTestRouteState extends State<FocusTestRoute> {
  FocusNode focusNode1 = FocusNode();
  FocusNode focusNode2 = FocusNode();
  FocusScopeNode? focusScopeNode;

  
  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>[
                ElevatedButton(
                  child: Text("移动焦点"),
                  onPressed: () {
                    //将焦点从第一个TextField移到第二个TextField
                    // 这是一种写法 FocusScope.of(context).requestFocus(focusNode2);
                    // 这是第二种写法
                    if(null == focusScopeNode){
                      focusScopeNode = FocusScope.of(context);
                    }
                    focusScopeNode.requestFocus(focusNode2);
                  },
                ),
                ElevatedButton(
                  child: Text("隐藏键盘"),
                  onPressed: () {
                    // 当所有编辑框都失去焦点时键盘就会收起  
                    focusNode1.unfocus();
                    focusNode2.unfocus();
                  },
                ),
              ],
            );
          },
          ),
        ],
      ),
    );
  }
}

FocusNodeFocusScopeNode还有一些其他的方法,详情可以查看API文档。

5)监听焦点状态改变事件

FocusNode继承自ChangeNotifier,通过FocusNode可以监听焦点的改变事件,如:

...
// 创建 focusNode   
FocusNode focusNode = FocusNode();
...
// focusNode绑定输入框   
TextField(focusNode: focusNode);
...
// 监听焦点变化    
focusNode.addListener((){
   print(focusNode.hasFocus);
});

获得焦点时focusNode.hasFocus值为true,失去焦点时为false

6)自定义样式

我们可以通过decoration属性来定义输入框样式,下面以自定义输入框下划线颜色为例来介绍一下:

TextField(
  decoration: InputDecoration(
    labelText: "请输入用户名",
    prefixIcon: Icon(Icons.person),
    // 未获得焦点下划线设为灰色
    enabledBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.grey),
    ),
    //获得焦点下划线设为蓝色
    focusedBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.blue),
    ),
  ),
),

上面代码我们直接通过InputDecorationenabledBorderfocusedBorder来分别设置了输入框在未获取焦点和获得焦点后的下划线颜色。

另外,我们也可以通过主题来自定义输入框的样式,下面我们探索一下如何在不使用enabledBorderfocusedBorder的情况下来自定义下滑线颜色。

由于TextField在绘制下划线时使用的颜色是主题色里面的hintColor,但提示文本颜色也是用的hintColor, 如果我们直接修改hintColor,那么下划线和提示文本的颜色都会变。值得高兴的是decoration中可以设置hintStyle,它可以覆盖hintColor,并且主题中可以通过inputDecorationTheme来设置输入框默认的decoration。所以我们可以通过主题来自定义,代码如下:

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,
      )
    ],
  )
)

运行效果:

在这里插入图片描述

我们成功的自定义了下划线颜色和提示文字样式,但是通过这种方式自定义后,输入框在获取焦点时,labelText不会高亮显示了,正如上图中的"用户名"本应该显示蓝色,但现在却显示为灰色,并且我们还是无法定义下划线宽度。

另一种灵活的方式是直接隐藏掉TextField本身的下划线,然后通过Container去嵌套定义样式,如:

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组合的方式。

在这个示例中,下划线颜色是固定的,所以获得焦点后颜色仍然为灰色,可以结合前面的FocusNode通过监听焦点的改变事件,即可轻松实现点击后下滑线也变色,可自行尝试。

Form

Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。

Form继承自StatefulWidget对象,它对应的状态类为FormState。我们先看看Form类的定义:

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

FormField

Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作,FormField部分定义如下:

const FormField({
  ...
  FormFieldSetter<T> onSaved, //保存回调
  FormFieldValidator<T>  validator, //验证回调
  T initialValue, //初始值
  bool autovalidate = false, //是否自动校验。
})

为了方便使用,Flutter 提供了一个TextFormField组件,它继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性。

FormState

FormStateFormState类,可以通过Form.of()GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:

  • FormState.validate():调用此方法后,会调用Form子孙FormFieldvalidate 回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。

  • FormState.save():调用此方法后,会调用Form子孙FormFieldsave 回调,用于保存表单内容

  • FormState.reset():调用此方法后,会将子孙FormField的内容清空。

示例:修改上面用户登录的示例,在提交之前进行校验

  1. 用户名不能为空,如果为空则提示“用户名不能为空”。
  2. 密码不能小于 6 位,如果小于 6 为则提示“密码不能少于 6 位”。

完整代码:

import 'package:flutter/material.dart';

class FormTestRoute extends StatefulWidget {
  
  _FormTestRouteState createState() => _FormTestRouteState();
}

class _FormTestRouteState extends State<FormTestRoute> {
  TextEditingController _unameController = TextEditingController();
  TextEditingController _pwdController = TextEditingController();
  GlobalKey _formKey = GlobalKey<FormState>();

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey, //设置globalKey,用于后面获取FormState
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Column(
        children: <Widget>[
          TextFormField(
            autofocus: true,
            controller: _unameController,
            decoration: InputDecoration(
              labelText: "用户名",
              hintText: "用户名或邮箱",
              icon: Icon(Icons.person),
            ),
            // 校验用户名
            validator: (v) {
              return v!.trim().isNotEmpty ? 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: ElevatedButton(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Text("登录"),
                    ),
                    onPressed: () {
                      // 通过_formKey.currentState 获取FormState后,
                      // 调用validate()方法校验用户名密码是否合法,校验
                      // 通过后再提交数据。
                      if ((_formKey.currentState as FormState).validate()) {
                        //验证通过提交数据
                      }
                    },
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

注意,登录按钮的onPressed方法中不能通过Form.of(context)来获取FormState,原因是,此处的contextFormTestRoutecontext,而Form.of(context)是根据所指定context向根去查找,而FormState是在FormTestRoute的子树中,所以不行。正确的做法是通过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:

Expanded(
 // 通过Builder来获取ElevatedButton所在widget树的真正context(Element) 
  child:Builder(builder: (context){
    return ElevatedButton(
      ...
      onPressed: () {
        //由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState  
        if(Form.of(context).validate()){
          //验证通过提交数据
        }
      },
    );
  })
)

其实context正是操作Widget所对应的Element的一个接口,由于Widget树对应的Element都是不同的,所以context也都是不同的。Flutter中有很多“of(context)”这种方法,在使用时一定要注意context是否正确。

ProgressIndicator

Material 组件库中提供了两种进度指示器:LinearProgressIndicatorCircularProgressIndicator,它们都可以同时用于精确的进度指示和模糊的进度指示。精确进度通常用于任务进度可以计算和预估的情况,比如文件下载;而模糊进度则用户任务进度无法准确获得的情况,如下拉刷新,数据提交等。

LinearProgressIndicator

LinearProgressIndicator是一个线性、条状的进度条,定义如下:

LinearProgressIndicator({
  double value,
  Color backgroundColor,
  Animation<Color> valueColor,
  ...
})
属性含义
value表示当前的进度,取值范围为[0,1]
如果valuenull,则指示器会执行一个循环动画(模糊进度);
value不为null时,指示器为一个具体进度的进度条
backgroundColor指示器的背景色
valueColor指示器的进度条颜色;值得注意的是,该值类型是Animation<Color>,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation来指定

示例代码:

// 模糊进度条(会执行一个动画)
LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%
LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
  value: .5, 
)

效果:

在这里插入图片描述
第一个进度条在执行循环动画:蓝色条一直在移动,而第二个进度条是静止的,停在50%的位置。

CircularProgressIndicator

CircularProgressIndicator是一个圆形进度条,定义如下:

 CircularProgressIndicator({
  double value,
  Color backgroundColor,
  Animation<Color> valueColor,
  this.strokeWidth = 4.0,
  ...   
}) 

前三个参数和LinearProgressIndicator相同,不再赘述。strokeWidth 表示圆形进度条的粗细。示例如下:

// 模糊进度条(会执行一个旋转动画)
CircularProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%,会显示一个半圆
CircularProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
  value: .5,
),

效果:
在这里插入图片描述
第一个进度条会执行旋转动画,而第二个进度条是静止的,它停在50%的位置。

自定义尺寸

我们可以发现LinearProgressIndicatorCircularProgressIndicator,并没有提供设置圆形进度条尺寸的参数;如果我们希望LinearProgressIndicator的线细一些,或者希望CircularProgressIndicator的圆大一些该怎么做?

其实LinearProgressIndicatorCircularProgressIndicator都是取父容器的尺寸作为绘制的边界的。知道了这点,我们便可以通过尺寸限制类Widget,如ConstrainedBoxSizedBox 来指定尺寸,如:

// 线性进度条高度指定为3
SizedBox(
  height: 3,
  child: LinearProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .5,
  ),
),
// 圆形进度条直径指定为100
SizedBox(
  height: 100,
  width: 100,
  child: CircularProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .7,
  ),
),

效果:
在这里插入图片描述
注意,如果CircularProgressIndicator显示空间的宽高不同,则会显示为椭圆。如:

// 宽高不等
SizedBox(
  height: 100,
  width: 130,
  child: CircularProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .7,
  ),
),

效果:
在这里插入图片描述

进度色动画

下面代码实现一个进度条在3秒内从灰色变成蓝色的动画:

import 'package:flutter/material.dart';

class ProgressRoute extends StatefulWidget {
  
  _ProgressRouteState createState() => _ProgressRouteState();
}

class _ProgressRouteState extends State<ProgressRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  
  void initState() {
    //动画执行时间3秒  
    _animationController = AnimationController(
        vsync: this, //注意State类需要混入SingleTickerProviderStateMixin(提供动画帧计时/触发器)
        duration: Duration(seconds: 3),
      );
    _animationController.forward();
    _animationController.addListener(() => setState(() => {}));
    super.initState();
  }

  
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  
  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,
            ),
          );
        ],
      ),
    );
  }
}

自定义进度指示器样式

定制进度指示器风格样式,可以通过CustomPainter Widget 来自定义绘制逻辑,实际上LinearProgressIndicatorCircularProgressIndicator也正是通过CustomPainter来实现外观绘制的。

flutter_spinkit 包提供了多种风格的模糊进度指示器,可以参考。


参考:《Flutter实战·第二版》

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值