Skip to content

常用组件 ​

可使用 getwidget 组件库

一、基础组件 ​

1. 文本组件 ​

1. Text ​

文本组件 Text,文本样式组件 TextStyle

dart
Text(
  "Hello world",
  textAlign: TextAlign.left,
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
  style: TextStyle(
      color: Colors.blue,
      fontSize: 18.0,
      height: 1.2,
      fontFamily: "Courier",
      backgroundColor: Colors.yellow,
      decoration: TextDecoration.underline,
      decorationStyle: TextDecorationStyle.dashed),
)

2. TextSpan ​

如果需要对一个 Text 内容的不同部分按照不同的样式显示,这时可以使用 TextSpan

dart
//RichText 是可以显示多种样式(富文本)的 widget
Text.rich(TextSpan(children: [
  TextSpan(text: "Home: "),
  TextSpan(
    text: "https://flutterchina.club",
    style: TextStyle(color: Colors.blue),
  ),
]));

3. DefaultTextStyle ​

在 Widget 树中,默认样式是可以被继承的。DefaultTextStyle 用于设置默认文本样式

dart
DefaultTextStyle(
  //1.设置文本默认样式
  style: const TextStyle(
    color: Colors.red,
    fontSize: 20.0,
  ),
  textAlign: TextAlign.start,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: const [
      Text("hello world"),
      Text("I am Jack"),
      Text(
        "I am Jack",
        style: TextStyle(
            inherit: false, //2.不继承默认样式
            color: Colors.grey),
      ),
    ],
  ),
);

2. 按钮组件 ​

1. 悬浮按钮(ElevatedButton) ​

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

2. 文本按钮(TextButton) ​

3. 边框按钮(OutlineButton) ​

4. 浮动按钮(FloatingActionButton) ​

5. 图标按钮(IconButton) ​

IconButton 是一个可点击的 Icon,不包括文字

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

6. 带图标的按钮 ​

通过 .icon 调用构造函数,可以创建带图标的按钮

dart
ElevatedButton.icon(
  icon: const Icon(Icons.send),
  label: const Text("发送"),
  onPressed: () {},
);

3. 图片组件 ​

1. 加载本地图片 ​

(1)在根目录新建 images 文件夹,并在其下新建 2.0x、3.0x 文件夹(因为不同分辨率,会读不同文件夹下的图片),在 images 下放本地图片,在 2.0x、3.0x 文件夹一同放置

(2)在 pubspec.yaml 文件中,进行配置

yaml
flutter:
  uses-material-design: true
  assets:
    - images/01.jpg
    - images/2.0x/01.jpg
    - images/3.0x/01.jpg

(3)使用

dart
Image.asset(
  'images/1.png',
  height: 100,
  width: 100,
  fit: BoxFit.cover,
)

2. 加载远程图片 ​

使用 Image.network 添加图片后,重编译下,而不是热加载,否则可能会报错

dart
Image.network(
  "https://....jpg",
  fit: BoxFit.cover,
)

4. 字体图标 ​

(1)在项目根目录新建 fonts 文件夹,并放入下载的 iconfont.ttf(阿里图标)

(2)修改 pubspec.yaml 配置

yaml
uses-material-design: true
fonts:
  - family: Iconfont
    fonts:
      - asset: fonts/iconfont.ttf

(3)新建 lib/myIcon.dart

dart
import 'package:flutter/widgets.dart';
class MyIcon{
  static const String _family = 'Iconfont';
  MyIcon._();
  //编码把&#替换成0
  static const IconData iconManage = IconData(0xe602, fontFamily: _family);
}

(4)使用

dart
import 'myIcon.dart';

Icon(MyIcon.iconManage)

5. Switch、Checkbox、Radio ​

dart
class SwitchAndCheckBoxTest extends StatefulWidget {
  const SwitchAndCheckBoxTest({super.key});

  @override
  State<SwitchAndCheckBoxTest> createState() => _SwitchAndCheckBoxTestState();
}

class _SwitchAndCheckBoxTestState extends State<SwitchAndCheckBoxTest> {
  bool _switchSelected = true; //维护单选开关状态
  bool _checkboxSelected = true; //维护复选框状态
  int sex = 1;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Switch(
          value: _switchSelected, //当前状态
          onChanged: (value) {
            setState(() {
              _switchSelected = value;
            });
          },
        ),
        Checkbox(
          value: _checkboxSelected,
          activeColor: Colors.red, //选中时的颜色
          tristate: true,
          onChanged: (value) {
            setState(() {
              _checkboxSelected = value ?? false;
            });
          },
        ),
        CheckboxListTile(
          value: _checkboxSelected,
          onChanged: (value) {
            setState(() {
              _checkboxSelected = value ?? false;
            });
          },
          title: const Text("标题"),
          subtitle: const Text('这是二级标题'),
          secondary: const Icon(Icons.help),
        ),
        Row(
          children: [
            const Text('男'),
            Radio(
                value: 1,
                groupValue: sex,
                onChanged: (value) {
                  setState(() {
                    sex = value!;
                  });
                }),
            const SizedBox(
              width: 10,
            ),
            const Text('女'),
            Radio(
                value: 2,
                groupValue: sex,
                onChanged: (value) {
                  setState(() {
                    sex = value!;
                  });
                }),
          ],
        ),
        RadioListTile(
          value: 1,
          groupValue: sex,
          onChanged: (value) {
            setState(() {
              sex = value!;
            });
          },
          title: const Text("标题"),
          subtitle: const Text('这是二级标题'),
          selected: sex == 1, //选中时字体颜色也会变
          secondary:
              Image.network("https://www.itying.com/images/flutter/2.png"),
        )
      ],
    );
  }
}

到目前为止,Checkbox 的大小是固定的,无法自定义,而 Switch 只能定义宽度,高度也是固定的

6. 输入框组件(TextField) ​

在 Flutter 中有很多内置的 Controller,大部分内置控件都可以通过 Controller 设置和获取控件参数。比如 TextField 的 TextEditingController,比如 ListView 的 ScrollController

dart
class Login extends StatefulWidget {
  const Login({super.key});

  @override
  State<Login> createState() => _LoginState();
}

class _LoginState extends State<Login> {
  final TextEditingController controller =
      new TextEditingController(text: "输入框默认值");
  String _name = '';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: controller,
          autofocus: true,
          onChanged: (v) {
            setState(() {
              _name = v;
            });
          },
          decoration: const InputDecoration(
            labelText: "用户名",
            hintText: "用户名或邮箱",
            prefixIcon: Icon(Icons.person),
            // 未获得焦点下划线设为灰色
            enabledBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.grey),
            ),
            //获得焦点下划线设为蓝色
            focusedBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.blue),
            ),
          ),
        ),
        const TextField(
          decoration: InputDecoration(
              labelText: "密码",
              hintText: "您的登录密码",
              prefixIcon: Icon(Icons.lock)),
          obscureText: true, //是否隐藏正在编辑的文本
        ),
        Text(_name)
      ],
    );
  }
}

7. 表单组件(Form) ​

dart
class FormTestRoute extends StatefulWidget {
  const FormTestRoute({super.key});
  @override
  State<FormTestRoute> createState() => _FormTestRouteState();
}

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

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

8. 进度指示器(LinearProgressIndicator、CircularProgressIndicator) ​

1. 线性进度条(LinearProgressIndicator) ​

dart
LinearProgressIndicator({
  double value, //当前进度,取值范围[0,1]
  Color backgroundColor, //指示器背景色
  Animation<Color> valueColor, //进度条颜色,
  //如果不需要对进度条颜色执行动画,可通过AlwaysStoppedAnimation来指定
  ...
})

2. 圆形进度条(CircularProgressIndicator) ​

dart
CircularProgressIndicator({
  double value,
  Color backgroundColor,
  Animation<Color> valueColor,
  this.strokeWidth = 4.0, //进度条的粗细
  ...
})

3. 自定义尺寸 ​

如果希望 LinearProgressIndicator 的线细一些,或者希望 CircularProgressIndicator 的圆大一些,可在外层包裹一个 SizedBox 组件

dart
// 线性进度条高度指定为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,
  ),
),

4. 进度条颜色动画 ​

dart
class ProgressRoute extends StatefulWidget {
  const ProgressRoute({super.key});
  @override
  State<ProgressRoute> createState() => _ProgressRouteState();
}

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

  @override
  void initState() {
    //动画执行时间3秒
    _animationController = AnimationController(
      vsync: this, //注意State类需要混入SingleTickerProviderStateMixin(提供动画帧计时/触发器)
      duration: const 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: const EdgeInsets.all(16),
            child: LinearProgressIndicator(
              backgroundColor: Colors.grey[200],
              valueColor: ColorTween(begin: Colors.grey, end: Colors.blue)
                  .animate(_animationController), // 从灰色变成蓝色
              value: _animationController.value,
            ),
          )
        ],
      ),
    );
  }
}

9. 弹窗组件 ​

1. AlertDialog ​

dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    Future<bool?> showDeleteConfirmDialog1() {
      return showDialog<bool>(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: const Text("提示"),
            content: const Text("您确定要删除当前文件吗?"),
            actions: <Widget>[
              TextButton(
                child: const Text("取消"),
                onPressed: () => Navigator.of(context).pop(), // 关闭对话框
              ),
              TextButton(
                child: const Text("删除"),
                onPressed: () {
                  //关闭对话框并返回true
                  Navigator.of(context).pop(true);
                },
              ),
            ],
          );
        },
      );
    }

    return Column(
      children: [
        ElevatedButton(
          child: const Text("对话框1"),
          onPressed: () async {
            //弹出对话框并等待其关闭
            bool? delete = await showDeleteConfirmDialog1();
            if (delete == null) {
              print("取消删除");
            } else {
              print("已确认删除");
              //... 删除文件
            }
          },
        ),
      ],
    );
  }
}

2. SimpleDialog ​

展示一个列表,用于列表选择的场景

dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    Future<void> changeLanguage() async {
      int? i = await showDialog<int>(
          context: context,
          builder: (BuildContext context) {
            return SimpleDialog(
              title: const Text('请选择语言'),
              children: <Widget>[
                SimpleDialogOption(
                  onPressed: () {
                    // 返回1
                    Navigator.pop(context, 1);
                  },
                  child: const Padding(
                    padding: EdgeInsets.symmetric(vertical: 6),
                    child: Text('中文简体'),
                  ),
                ),
                SimpleDialogOption(
                  onPressed: () {
                    // 返回2
                    Navigator.pop(context, 2);
                  },
                  child: const Padding(
                    padding: EdgeInsets.symmetric(vertical: 6),
                    child: Text('美国英语'),
                  ),
                ),
              ],
            );
          });

      if (i != null) {
        print("选择了:${i == 1 ? "中文简体" : "美国英语"}");
      }
    }

    return Column(
      children: [
        ElevatedButton(
          child: const Text("对话框1"),
          onPressed: () {
            changeLanguage();
          },
        ),
      ],
    );
  }
}

3. Dialog ​

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

dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    Future<void> showListDialog() async {
      int? index = await showDialog<int>(
        context: context,
        builder: (BuildContext context) {
          var child = Column(
            children: <Widget>[
              const 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);
        },
      );
      if (index != null) {
        print("点击了:$index");
      }
    }

    return Column(
      children: [
        ElevatedButton(
          child: const Text("对话框1"),
          onPressed: () {
            showListDialog();
          },
        ),
      ],
    );
  }
}

10. ListTile ​

dart
ListTile(
  leading: Icon(Icons.ac_unit), // 左侧组件
  title: Text("title"), // 标题
  subtitle: Text(""), // 副标题
  trailing: Icon(Icons.access_alarm), // 右侧组件
  isThreeLine: true, // 是否三行,设置为 true 时,subTitle 不可以为空
  dense: false, // 是否使用缩小布局
  visualDensity: VisualDensity(horizontal: -4), // 紧凑程度
  contentPadding: EdgeInsets.all(20), // content 内间距
  enabled: true, // 是否可用,仅改变颜色
  // 点击事件回调
  onTap: () {
    print("onTap");
  },
  // 长按事件回调
  onLongPress: () {
    print("onLongPress");
  },
  // 是否选中
  selected: selected,
)

二、容器类组件 ​

1. Container ​

相当于 div,可以上背景色

dart
Container({
  this.alignment, //容器内对齐方式
  this.padding, //容器内补白,属于decoration的装饰范围
  Color color, // 背景色
  Decoration decoration, // 背景装饰 与color互斥
  Decoration foregroundDecoration, //前景装饰
  double width, //容器的宽度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制条件 与width、height互斥
  this.margin, //容器外补白,不属于decoration的装饰范围
  this.transform, //变换
  this.child,
  ...
})

示例:

dart
Container(
  margin: EdgeInsets.only(top: 50.0, left: 120.0),
  constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0), //卡片大小
  decoration: BoxDecoration( //背景装饰
    gradient: RadialGradient( //背景径向渐变
      colors: [Colors.red, Colors.orange],
      center: Alignment.topLeft,
      radius: .98,
    ),
    boxShadow: [
      //卡片阴影
      BoxShadow(
        color: Colors.black54,
        offset: Offset(2.0, 2.0),
        blurRadius: 4.0,
      )
    ],
  ),
  transform: Matrix4.rotationZ(.2), //卡片倾斜变换
  alignment: Alignment.center, //卡片内文字居中
  child: Text(
    //卡片文字
    "5.20",
    style: TextStyle(color: Colors.white, fontSize: 40.0),
  ),
)

2. SizedBox ​

给子元素指定固定的宽高

dart
SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

3. Padding ​

对于没有 padding 属性的组件,可使用 Padding 组件实现内边距

dart
Padding(
  padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
  child: Image.network('1.png', fit: BoxFit.cover),
)
  • EdgeInsets.fromLTRB(10, 10, 0, 0):四个方向的 padding 值
  • EdgeInsets.symmetric(horizontal: 80, vertical: 18.0):水平和垂直方向的 padding 值
  • EdgeInsets.all(double value):所有方向均使用相同数值的填充
  • EdgeInsets.only({left, top, right, bottom}):可以设置具体某个方向的填充

4. 约束盒子(ConstrainedBox) ​

父组件约束子组件宽高

dart
Widget redBox = const DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
);

ConstrainedBox(
  constraints: const BoxConstraints( //描述约束条件
      minWidth: 20.0,
      minHeight: 50.0,
      maxWidth: double.infinity, //尽可能大
      maxHeight: double.infinity),
  child: SizedBox(
    height: 5.0,
    child: redBox,
  ),
)

BoxConstraints.expand():获取最大宽高

有多重限制时,对于 minWidth 和 minHeight 来说,是取父子中相应数值较大的;最终显示效果是宽 90,高 60

dart
ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), //子
    child: redBox,
  ),
)

5. 取消约束盒子(UnconstrainedBox) ​

dart
ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), //父
  child: UnconstrainedBox( //“去除”父级限制
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), //子
      child: redBox,
    ),
  )
)

6. AspectRatio ​

固定宽高比

dart
AspectRatio(
  aspectRatio: 1.5,
  child: Image.asset(
    "images/1.png",
  ),
)

7. Card ​

dart
Card(
  child:...
)

三、布局类组件 ​

1. 线性布局(Row、Column) ​

dart
Row({
  ...
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  List<Widget> children = const <Widget>[],
})

mainAxisAlignment:水平方向对齐方式

crossAxisAlignment:垂直方向对齐方式

MainAxisAlignment:start,center,end,spaceBetween,spaceAround,spaceEvenly

2. 弹性布局(Flex 与 Expanded) ​

dart
Flex(
  direction: Axis.horizontal,
  children: [
    Expanded(
        flex: 1, //默认值,可省略
        child: Container(
          height: 30.0,
          color: Colors.red,
        )),
    Expanded(
        flex: 1,
        child: Container(
          height: 30.0,
          color: Colors.black,
        )),
  ],
);

3. 流式布局(Wrap、Flow) ​

1. Wrap ​

超出显示范围后会折行

dart
Wrap(
  spacing: 8.0, // 主轴(水平)方向间距
  runSpacing: 4.0, // 纵轴(垂直)方向间距
  alignment: WrapAlignment.center, //沿主轴方向居中
  children: <Widget>[
    Chip(
      avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
      label: Text('Hamilton'),
    ),
    Chip(
      avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
      label: Text('Lafayette'),
    ),
    Chip(
      avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
      label: Text('Mulligan'),
    ),
    Chip(
      avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
      label: Text('Laurens'),
    ),
  ],
)

2. Flow ​

Flow 主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景

4. 层叠布局(Stack 与 Positioned) ​

层叠布局:绝对定位。Stack 允许子组件堆叠,Positioned 根据 Stack 的四个角来确定子组件的位置

1. Stack ​

dart
Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.clipBehavior = Clip.hardEdge,
  List<Widget> children = const <Widget>[],
})

alignment:指定未定位或部分定位 widget 的对齐方式

textDirection:和 Row、Wrap 的 textDirection 功能一样,都用于确定 alignment 对齐的参考系

fit:确定没有定位的子组件如何去适应 Stack 的大小。StackFit.loose 表示使用子组件的大小,StackFit.expand 表示扩伸到 Stack 的大小

clipBehavior:决定对超出 Stack 显示空间的部分如何剪裁

2. Positioned ​

dart
ConstrainedBox(
  constraints: BoxConstraints.expand(), //占满屏幕
  child: Stack(
    alignment: Alignment.center,
    children: <Widget>[
      Container(
        child: Text("Hello world", style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18.0,
        child: Text("I am Jack"),
      ),
      Positioned(
        top: 18.0,
        child: Text("Your friend"),
      )
    ],
  ),
);

5. 对齐组件(Align、Center) ​

1. Align ​

dart
Align({
  Key key,
  this.alignment = Alignment.center, //对齐方式
  this.widthFactor,
  this.heightFactor,
  Widget child,
})

widthFactor 和 heightFactor 是两个缩放因子,用于确定 Align 组件本身的宽高。如果值为 null,则组件的宽高将会占用尽可能多的空间

2. Center ​

与 Align 使用相似,只是少了 alignment 属性

6. LayoutBuilder ​

通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局

dart
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, contraints) {
        if (contraints.maxWidth < 600) {
          return const Text('小于600');
        } else {
          return const Text('大于600');
        }
      },
    );
  }
}

使用场景:

  1. 可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局。

  2. LayoutBuilder 可以帮我们高效排查问题。比如我们在遇到布局问题或者想调试组件树中某一个节点布局的约束时 LayoutBuilder 就很有用

7. 页面骨架(Scaffold) ​

Scaffold 是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面

Scaffold 的 drawer 和 endDrawer 属性可以分别接受一个 Widget 来作为页面的左、右抽屉菜单

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Welcome to Flutter',
      home: ScaffoldRoute(),
    );
  }
}

class ScaffoldRoute extends StatefulWidget {
  const ScaffoldRoute({super.key});

  @override
  State<ScaffoldRoute> createState() => _ScaffoldRouteState();
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _selectedIndex = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        //导航栏
        title: const Text("App Name"),
        actions: <Widget>[
          //导航栏右侧菜单
          IconButton(icon: const Icon(Icons.share), onPressed: () {}),
        ],
      ),
      drawer: const MyDrawer(), //抽屉
      bottomNavigationBar: BottomNavigationBar(
        // 底部导航
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(
              icon: Icon(Icons.business), label: 'Business'),
          BottomNavigationBarItem(icon: Icon(Icons.school), label: 'School'),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
      floatingActionButton: FloatingActionButton(
          //悬浮按钮
          onPressed: _onAdd, //悬浮按钮
          child: const Icon(Icons.add)),
    );
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  void _onAdd() {}
}

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

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        //移除抽屉菜单顶部默认留白
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 38.0),
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: ClipOval(
                      child: Image.asset(
                        "imgs/avatar.png",
                        width: 80,
                      ),
                    ),
                  ),
                  const Text(
                    "Wendux",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: const <Widget>[
                  ListTile(
                    leading: Icon(Icons.add),
                    title: Text('Add account'),
                  ),
                  ListTile(
                    leading: Icon(Icons.settings),
                    title: Text('Manage accounts'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

8. SafeArea ​

由于现在屏幕不再方方正正,因此引入了安全区组件,本质是创建边距

dart
SafeArea(
  top: false,
  bottom: false,
  child: ,
),

9. 垫片(Spacer) ​

本身是一个 Expanded 组件,用于撑开空白区域

dart
Column(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: [
    Text("data"), //上
    Spacer(),
    Text("data"), //中
    Spacer(),
    Text("data"), //下
  ],
),

10. 叠加层(OverlayEntry) ​

可做 dialog,toast 等悬浮组件

dart
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late OverlayEntry _overlayEntry;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Overlay Demo'),
      ),
      body: Center(
        child: TextButton(
          onPressed: () {
            // Create an OverlayEntry for the Overlay.
            _overlayEntry = OverlayEntry(
              builder: (context) => Positioned(
                left: 10,
                top: 10,
                child: Text('This is an Overlay'),
              ),
            );

            // 向叠加层中插入一个 OverlayEntry 对象,来显示叠加层内容
            Overlay.of(context)?.insert(_overlayEntry);
          },
          child: Text('Show Overlay'),
        ),
      ),
    );
  }
}

四、装饰类组件 ​

1. 装饰容器(DecoratedBox 与 BoxDecoration) ​

1. DecoratedBox ​

dart
const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget? child
})

decoration:将要绘制的装饰,通常是 BoxDecoration

position:background 在子组件之后绘制,即背景装饰;foreground 在子组件之前绘制,即前景

2. BoxDecoration ​

dart
BoxDecoration({
  Color color, //颜色
  DecorationImage image, //图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})

示例

dart
DecoratedBox(
    decoration: BoxDecoration(
        gradient: LinearGradient(
            colors: [Colors.red, Colors.orange.shade700]), //背景渐变
        borderRadius: BorderRadius.circular(3.0), //3像素圆角
        boxShadow: const [
          BoxShadow(
              color: Colors.black54,
              offset: Offset(2.0, 2.0),
              blurRadius: 4.0)
        ]),
    child: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 80, vertical: 18.0),
      child: Text(
        "Login",
        style: TextStyle(color: Colors.white),
      ),
    )),

2. 装饰图片(DecorationImage) ​

等同于 web 的 fit 模式

dart
Container(
  width: 100,
  height: 100,
  decoration: BoxDecoration(
      color: Colors.red,
      image: DecorationImage(
        fit: BoxFit.cover,
        image: AssetImage("static/gsy_cat.png"),
      ),
      borderRadius: BorderRadius.all(Radius.circular(5.0))),
)
  • BoxFit.none:没有任何填充模式
  • BoxFit.fill:不按宽高比例填充,内容不会超过容器范围
  • BoxFit.contain:按照宽高比等比模式填充,内容不会超过容器范围
  • BoxFit.cover:按照原始尺寸填充整个容器模式。内容可能会超过容器范围
  • BoxFit.scaleDown:会根据情况缩小范围

3. Transform ​

dart
Container(
    color: Colors.black,
    child: Transform(
      alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
      transform: Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
      child: Container(
        padding: const EdgeInsets.all(8.0),
        color: Colors.deepOrange,
        child: const Text('Apartment for rent!'),
      ),
    )),

1. 平移(Transform.translate) ​

接收一个 offset 参数,可以在绘制时沿 x、y 轴对子组件平移指定的距离

dart
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Transform.translate(
    offset: Offset(-20.0, -5.0), //以左上角为原点,左移20像素,向上平移5像素
    child: Text("Hello world"),
  ),
)

2. 旋转(Transform.rotate) ​

dart
import 'dart:math';

DecoratedBox(
  decoration: const BoxDecoration(color: Colors.red),
  //将Transform.rotate换成RotatedBox
  child: Transform.rotate(
    angle: pi / 2,
    child: const Text("Hello world"),
  ),
)

3. 缩放(Transform.scale) ​

dart
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Transform.scale(
    scale: 1.5, //放大到1.5倍
    child: Text("Hello world")
  )
);

4. RotatedBox ​

Transform 是在控件绘制前对其进行矩阵变换,RotatedBox 是在控件 layout 的时候就对其子 widget 进行旋转处理,这意味着 RotatedBox 控件所需要的空间大小跟旋转子 widget 所需要的空间大小一样

dart
const DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  //将Transform.rotate换成RotatedBox
  child: RotatedBox(
    quarterTurns: 1, //旋转90度(1/4圈)
    child: Text("Hello world"),
  ),
)

使用 Transform.rotate 文字会旋转,但背景色不会;使用 RotatedBox 背景色跟着旋转

5. 裁剪组件(Clip) ​

dart
SizedBox(
  height: 200.0,
  width: 200.0,
  child: ClipOval(
      child: Image.network(
          "http://img.redocn.com/sheying/20150213/mulanweichangcaoyuanfengjing_3951976.jpg",
          fit: BoxFit.fill)),
)
  • ClipOval:子组件为正方形时剪裁成内贴圆形;为矩形时,剪裁成内贴椭圆
  • ClipRRect:将子组件剪裁为圆角矩形
  • ClipRect:默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁)
  • ClipPath:按照自定义的路径剪裁

6. 分割线组件(Divider) ​

dart
Divider(
  height: 1,
  color: ,
),

五、可滚动组件 ​

1. SingleChildScrollView ​

适用于内容不会超过屏幕太多时使用(不需按需加载的情况)

dart
SingleChildScrollView({
  this.scrollDirection = Axis.vertical, //滚动方向,默认是垂直方向
  this.reverse = false,
  this.padding,
  bool primary,
  this.physics, //它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画
  this.controller, //控制滚动位置和监听滚动事件
  this.child,
})

physics 释义:

类型释义
BouncingScrollPhysics允许滚动出边界,超过边界时会有回弹效果,会响应滚动事件
ClampingScrollPhysics不允许滚动出边界,会响应滚动事件
AlwaysScrollableScrollPhysics一直响应滚动事件
NeverScrollableScrollPhysics禁止滚动,不响应滚动事件
FixedExtentScrollPhysicsListWheelScrollView 滚轮使用时,item 都会停止在中间位置,不会停在分割线
PageScrollPhysicsPageView 滚轮使用时,item 都会停止在一页,不会停止在分割线位置
RangeMaintainingScrollPhysics当内容突然改变尺寸时,试图将滚动位置保持在范围内的滚动物理

示例:

dart
class SingleChildScrollViewTestRoute extends StatelessWidget {
  const SingleChildScrollViewTestRoute({super.key});

  @override
  Widget build(BuildContext context) {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar(
      // 显示进度条
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        //不用Center组件包裹会不出滚动条,或者使用physics
        physics: const AlwaysScrollableScrollPhysics(), //总是可以滚动
        child: Center(
          child: Column(
            //动态创建一个List<Widget>
            children: str
                .split("")
                //每一个字母都用一个Text显示,字体为原来的两倍
                .map((c) => Text(
                      c,
                      textScaleFactor: 2.0,
                    ))
                .toList(), //map方法返回的需调用toList
          ),
        ),
      ),
    );
  }
}

2. ListView ​

当内容长于自己的渲染盒时,会自动支持滚动,支持列表项按需加载

dart
ListView({
  ...
  //可滚动widget公共参数
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController? controller,
  bool? primary,
  ScrollPhysics? physics,
  EdgeInsetsGeometry? padding,

  //ListView各个构造函数的共同参数
  double? itemExtent,
  Widget? prototypeItem, //列表项原型
  bool shrinkWrap = false, //当ListView没有父组件时,需置为true,自己定空间
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double? cacheExtent, // 预渲染区域长度

  //子widget列表
  List<Widget> children = const <Widget>[],
})

itemExtent:提前约定子组件的长度,能提升性能

prototypeItem:提前定义一个模型子组件,让 flutter 知道子组件的宽高,能提升性能;与 itemExtent 属性不能同时使用

示例:

dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  List<Widget> _getData() {
    return [2, 4, 6, 8]
        .map((o) => Text('我是$o列表'))
        .toList(); //遍历器类型需使用toList转换,flutter才可使用
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
        height: 180.0,
        child: ListView(
          // scrollDirection: Axis.horizontal, 默认垂直
          children: _getData(),
        ));
  }
}

1. ListView.builder ​

除了使用 map 定义动态列表外,还可以使用 ListView.builder

dart
var listData = ['2', ' 4', ' 6', ' 8'];

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  Widget _getData(context, index) {
    return ListTile(title: Text(listData[index]));
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: listData.length,
      itemBuilder: _getData, //这里不调用方法是因为需要的就是一个方法
    );
  }
}

2. ListView.separated ​

在生成的列表项之间添加一个分割组件

dart
class ListView3 extends StatelessWidget {
  const ListView3({super.key});

  @override
  Widget build(BuildContext context) {
    //下划线widget预定义以供复用。
    Widget divider1 = const Divider(
      color: Colors.blue,
    );
    Widget divider2 = const Divider(color: Colors.green);
    return ListView.separated(
      itemCount: 100,
      //列表项构造器
      itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      },
      //分割器构造器
      separatorBuilder: (BuildContext context, int index) {
        return index % 2 == 0 ? divider1 : divider2;
      },
    );
  }
}

3. 固定高度列表 ​

给列表指定 itemExtent 或 prototypeItem 会有更高的性能

dart
class ListView3 extends StatelessWidget {
  const ListView3({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 100,
      prototypeItem: const ListTile(title: Text("1")),
      //列表项构造器
      itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      },
    );
  }
}

3. ScrollController ​

1. 回到顶部功能 ​

dart
class ScrollControllerTestRoute extends StatefulWidget {
  const ScrollControllerTestRoute({super.key});

  @override
  ScrollControllerTestRouteState createState() {
    return ScrollControllerTestRouteState();
  }
}

class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
  final ScrollController _controller = ScrollController();
  bool showToTopBtn = false; //是否显示“返回到顶部”按钮

  @override
  void initState() {
    super.initState();

    //监听滚动事件,打印滚动位置
    _controller.addListener(() {
      print(_controller.offset); //打印滚动位置
      if (_controller.offset < 1000 && showToTopBtn) {
        setState(() {
          showToTopBtn = false;
        });
      } else if (_controller.offset >= 1000 && showToTopBtn == false) {
        setState(() {
          showToTopBtn = true;
        });
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    //为了避免内存泄露,需要调用_controller.dispose
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("滚动控制")),
      body: Scrollbar(
        child: ListView.builder(
          itemCount: 100,
          itemExtent: 50.0, //列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
          controller: _controller,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text("$index"),
            );
          },
        ),
      ),
      floatingActionButton: !showToTopBtn
          ? null
          : FloatingActionButton(
              child: const Icon(Icons.arrow_upward),
              onPressed: () {
                //返回到顶部时执行动画
                _controller.animateTo(
                  .0,
                  duration: const Duration(milliseconds: 200),
                  curve: Curves.ease,
                );
                //返回到顶部没有动画
                _controller.jumpTo(
                  0,
                );
              }),
    );
  }
}

4. AnimatedList ​

AnimatedList 和 ListView 的功能大体相似,不同的是,AnimatedList 可以在列表中插入或删除节点时执行一个动画,在需要添加或删除列表项的场景中会提高用户体验

5. GridView ​

在 GridView 布局中设置高度无效,只有通过设置宽高比来增加高度。最大比例为 1

dart
var listData = ['2', '4', '6', '8'];

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return GridView(
        padding: const EdgeInsets.all(10),
        physics: const BouncingScrollPhysics(), //允许滚动出边界,超过边界时会有回弹效果,会响应滚动事件
        scrollDirection: Axis.vertical,
        children: List.generate(
            listData.length,
            (index) => Container(
                decoration: BoxDecoration(
                    border: Border.all(
                        color: const Color.fromRGBO(255, 10, 25, 1))),
                child: Text(
                  listData[index],
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 20),
                ))),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2, //一行Widget的数量
          mainAxisSpacing: 5.0, //水平Widget之间间距
          crossAxisSpacing: 10.0, //垂直Widget之间间距
          childAspectRatio: 1.0, //子Widget宽高比例
        ));
  }
}

6. PageView ​

如果要实现页面切换和 Tab 布局,可以使用 PageView 组件

dart
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    var children = <Widget>[];
    // 生成 6 个 Tab 页
    for (int i = 0; i < 6; ++i) {
      children.add(Page(text: '$i'));
    }

    return PageView(
      // scrollDirection: Axis.vertical, // 滑动方向为垂直方向
      allowImplicitScrolling: true, //预渲染一个页面
      children: children,
    );
  }
}

class Page extends StatefulWidget {
  const Page({super.key, required this.text});
  final String text;

  @override
  State<Page> createState() => _PageState();
}

class _PageState extends State<Page> {
  @override
  Widget build(BuildContext context) {
    print("build ${widget.text}");
    return Center(child: Text(widget.text, textScaleFactor: 5));
  }
}

页面在切换时,会销毁重建,可通过 allowImplicitScrolling 预渲染一个页面。如果想全部缓存,可封装一个组件

dart
class KeepAliveWrapper extends StatefulWidget {
  const KeepAliveWrapper({
    super.key,
    this.keepAlive = true,
    required this.child,
  });
  final bool keepAlive;
  final Widget child;

  @override
  State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;
  }

  @override
  void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
    if (oldWidget.keepAlive != widget.keepAlive) {
      // keepAlive 状态需要更新,实现在 AutomaticKeepAliveClientMixin 中
      updateKeepAlive();
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  bool get wantKeepAlive => widget.keepAlive;
}

在需要缓存的组件中调用

dart
@override
Widget build(BuildContext context) {
  var children = <Widget>[];
  for (int i = 0; i < 6; ++i) {
    //只需要用 KeepAliveWrapper 包装一下即可
    children.add(KeepAliveWrapper(child: Page(text: '$i')));
  }
  return PageView(children: children);
}

7. TabBarView 与 TabBar ​

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: TabHomePage());
  }
}

class TabHomePage extends StatefulWidget {
  const TabHomePage({super.key});

  @override
  State<TabHomePage> createState() => _TabHomePageState();
}

//有动画效果,需混入SingleTickerProviderStateMixin类
class _TabHomePageState extends State<TabHomePage>
    with SingleTickerProviderStateMixin {
  List tabs = ["分类1", "分类2", "分类3", "分类4"];
  late TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(
      length: tabs.length,
      vsync: this,
    );
    _controller.addListener(() {
      //监听Tab切换事件
      print(_controller.index);
    });
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  Widget _tabBar() {
    return Container(
      height: 56,
      color: Colors.white,
      child: TabBar(
          controller: _controller,
          labelStyle: const TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.bold,
          ),
          labelColor: Colors.green,
          unselectedLabelColor: Colors.black26,
          tabs: tabs
              .map((e) => Tab(
                    text: e,
                  ))
              .toList()),
    );
  }

  Widget _body() {
    return TabBarView(
      controller: _controller,
      children: tabs.map((item) {
        return Container(
          child: Center(
            child: Text(item),
          ),
        );
      }).toList(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Container(
        child: Column(
          children: [
            _tabBar(),
            Expanded(
              child: _body(),
            ),
          ],
        ),
      ),
    );
  }
}

六、功能型组件 ​

1. 局部刷新(ValueListenableBuilder) ​

类似于 React 的 memo

dart
const ValueListenableBuilder({
  Key? key,
  required this.valueListenable, // 数据源,类型为ValueListenable<T>
  required this.builder, // 当valueListenable变化时,重新调用builder
  this.child, //子组件不变的部分
}

示例:

dart
class ValueListenableRoute extends StatefulWidget {
  const ValueListenableRoute({Key? key}) : super(key: key);

  @override
  State<ValueListenableRoute> createState() => _ValueListenableState();
}

class _ValueListenableState extends State<ValueListenableRoute> {
  final _counter = ValueNotifier(0);
  static const double textScaleFactor = 1.5;

  @override
  Widget build(BuildContext context) {
    // 添加 + 按钮不会触发整个 ValueListenableRoute 组件的 build
    print('build');
    return Scaffold(
      appBar: AppBar(title: const Text('ValueListenableBuilder 测试')),
      body: Center(
        child: ValueListenableBuilder(
          builder: (BuildContext context, int value, Widget? child) {
            // builder 方法只会在 _counter 变化时被调用
            return Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                child!,
                Text('$value 次', textScaleFactor: textScaleFactor),
              ],
            );
          },
          valueListenable: _counter,
          // 当子组件不依赖变化的数据,且子组件收件开销比较大时,指定 child 属性来缓存子组件非常有用
          child: const Text('点击了 ', textScaleFactor: textScaleFactor),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        // 点击后值 +1,触发 ValueListenableBuilder 重新构建
        onPressed: () => _counter.value += 1,
      ),
    );
  }
}

2. 异步 UI 更新(FutureBuilder、StreamBuilder) ​

依赖异步数据更新 UI,可以使用 StatefulWidget 实现。flutter 提供了 FutureBuilder 和 StreamBuilder 两个组件快速实现这种功能

1. FutureBuilder ​

FutureBuilder 会依赖一个 Future,它会根据所依赖的 Future 的状态来动态构建自身

dart
FutureBuilder({
  this.future, //FutureBuilder依赖的Future,通常是一个异步耗时任务。
  this.initialData,
  required this.builder,
})

示例

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

Future<String> mockNetworkData() async {
  return Future.delayed(const Duration(seconds: 2), () => "我是从互联网上获取的数据");
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: const Text("App Name"),
      ),
      body: const Home(),
    ));
  }
}

class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FutureBuilder(
        future: mockNetworkData(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          print(snapshot.connectionState);
          // 请求已结束
          if (snapshot.connectionState == ConnectionState.done) {
            if (snapshot.hasError) {
              // 请求失败,显示错误
              return Text("Error: ${snapshot.error}");
            } else {
              // 请求成功,显示数据
              return Text("Contents: ${snapshot.data}");
            }
          } else {
            // 请求未结束,显示loading
            return const CircularProgressIndicator();
          }
        },
      ),
    );
  }
}

2. StreamBuilder ​

常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

Stream<int> counter() {
  return Stream.periodic(const Duration(seconds: 1), (i) {
    return i;
  });
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: const Text("App Name"),
      ),
      body: const Home(),
    ));
  }
}

class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: counter(), //
      //initialData: ,// a Stream<int> or null
      builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return const Text('没有Stream');
          case ConnectionState.waiting:
            return const Text('等待数据...');
          case ConnectionState.active:
            return Text('active: ${snapshot.data}');
          case ConnectionState.done:
            return const Text('Stream 已关闭');
        }
      },
    );
  }
}

3. 显示隐藏(Visibility、Offstage) ​

控制组件的显示与隐藏

dart
Visibility(
  visible: isEnd, //控制是否显示
  child: Text('Offstage'),
)

Offstage(
  offstage: isFlag, //控制是否显示
  child: Text('Offstage'),
)

4. 通知(Notification) ​

七、事件处理 ​

1. 手势识别组件(GestureDetector) ​

GestureDetector 是一个用于手势识别的功能性组件,内部封装了 Listener

1. 点击、双击、长按 ​

dart
class GestureTest extends StatefulWidget {
  const GestureTest({super.key});

  @override
  State<GestureTest> createState() => _GestureTestState();
}

class _GestureTestState extends State<GestureTest> {
  String _operation = "No Gesture detected!"; //保存事件名
  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        child: Container(
          alignment: Alignment.center,
          color: Colors.blue,
          width: 200.0,
          height: 100.0,
          child: Text(
            _operation,
            style: const TextStyle(color: Colors.white),
          ),
        ),
        onTap: () => updateText("Tap"), //点击
        onDoubleTap: () => updateText("DoubleTap"), //双击
        onLongPress: () => updateText("LongPress"), //长按
      ),
    );
  }

  void updateText(String text) {
    //更新显示的事件名
    setState(() {
      _operation = text;
    });
  }
}

当同时监听 onTap 和 onDoubleTap 事件时,当用户触发 tap 事件时,会有 200 毫秒左右的延时,这是因为当用户点击完之后很可能会再次点击以触发双击事件,所以 GestureDetector 会等一段时间来确定是否为双击事件。如果用户只监听了 onTap(没有监听 onDoubleTap)事件时,则没有延时

2. 拖动、滑动 ​

dart
class Drag extends StatefulWidget {
  const Drag({super.key});

  @override
  State<Drag> createState() => _DragState();
}

class _DragState extends State<Drag> with SingleTickerProviderStateMixin {
  double _top = 0.0; //距顶部的偏移
  double _left = 0.0; //距左边的偏移

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: const CircleAvatar(child: Text("A")),
            //手指按下时会触发此回调
            onPanDown: (DragDownDetails e) {
              //打印手指按下的位置(相对于屏幕原点(左上角)的偏移)
              print("用户手指按下:${e.globalPosition}");
            },
            //手指滑动时会触发此回调
            onPanUpdate: (DragUpdateDetails e) {
              //用户手指滑动时,更新偏移,重新构建
              setState(() {
                _left += e.delta.dx;
                _top += e.delta.dy;
              });
            },
            onPanEnd: (DragEndDetails e) {
              //打印滑动结束时在x、y轴上的速度
              print(e.velocity);
            },
          ),
        )
      ],
    );
  }
}

3. 单一方向拖动 ​

GestureDetector 可以只识别特定方向的手势事件

dart
class DragVertical extends StatefulWidget {
  const DragVertical({super.key});

  @override
  State<DragVertical> createState() => _DragVerticalState();
}

class _DragVerticalState extends State<DragVertical> {
  double _top = 0.0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          child: GestureDetector(
            child: const CircleAvatar(child: Text("A")),
            //垂直方向拖动事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
          ),
        )
      ],
    );
  }
}

4. 缩放 ​

dart
class Scale extends StatefulWidget {
  const Scale({super.key});

  @override
  State<Scale> createState() => _ScaleState();
}

class _ScaleState extends State<Scale> {
  double _width = 200.0; //通过修改图片宽度来达到缩放效果

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        //指定宽度,高度自适应
        child: Image.asset("images/1.png", width: _width),
        onScaleUpdate: (ScaleUpdateDetails details) {
          setState(() {
            //缩放倍数在0.8到10倍之间
            _width = 200 * details.scale.clamp(.8, 10.0);
          });
        },
      ),
    );
  }
}

2. 手势识别组件(InkWell) ​

如果没有特殊要求,只相应 onTap 的话,尽可能用 InkWell,开发中 InkWell 反应比较灵敏一点

弹出的软键盘无法关闭,尽可能使用 GestureDetector

dart
InkWell(
  child: const Text('点击'),
  onTap: () {
    print('点击');
  },
)

3. 手势识别监听器(GestureRecognizer) ​

GestureDetector 只能接收一个子 widget,当要监听的不是一个 widget 时,可以使 GestureRecognizer

dart
import 'package:flutter/gestures.dart';

class GestureRecognizer extends StatefulWidget {
  const GestureRecognizer({super.key});

  @override
  State<GestureRecognizer> createState() => _GestureRecognizerState();
}

class _GestureRecognizerState extends State<GestureRecognizer> {
  final TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
  bool _toggle = false; //变色开关

  @override
  void dispose() {
    //用到GestureRecognizer的话一定要调用其dispose方法释放资源
    _tapGestureRecognizer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text.rich(
        TextSpan(
          children: [
            const TextSpan(text: "你好世界"),
            TextSpan(
              text: "点我变色",
              style: TextStyle(
                fontSize: 30.0,
                color: _toggle ? Colors.blue : Colors.red,
              ),
              recognizer: _tapGestureRecognizer
                ..onTap = () {
                  setState(() {
                    _toggle = !_toggle;
                  });
                },
            ),
            const TextSpan(text: "你好世界"),
          ],
        ),
      ),
    );
  }
}

八、动画 ​

1. 基本使用 ​

1. Animation ​

Animation 是一个抽象类,主要功能是保存动画的插值和状态;通常是 Tween 的返回值

dart
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0)...

2. Curve ​

动画曲线

dart
final CurvedAnimation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeIn);

Curves 曲线动画过程:

曲线说明
linear匀速的
decelerate匀减速
ease开始加速,后面减速
easeIn开始慢,后面快
easeOut开始快,后面慢
easeInOut开始慢,然后加速,最后再减速

3. AnimationController ​

AnimationController 用于控制动画,包含动画的启动 forward()、停止 stop()、反向播放 reverse()、重置 reset()、在 dispose 生命周期,调用 dispose 方法释放资源

默认情况下,给定的时间段内线性的生成从 0.0 到 1.0(默认区间)的数字,可通过 Animation.value 取值。生成的数字区间可以通过 lowerBound 和 upperBound 来指定

dart
final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 2000),
  lowerBound: 10.0,
  upperBound: 20.0,
  vsync: this
);

4. Tween ​

默认情况下,AnimationController 对象值的范围是 [0.0,1.0]。如果我们需要构建 UI 的动画值在不同的范围或不同的数据类型,则可以使用 Tween 来添加映射以生成不同的范围或数据类型的值。例如,像下面示例,Tween 生成 [-200.0,0.0] 的值:

dart
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);

示例:

dart
class ScaleAnimationRoute extends StatefulWidget {
  const ScaleAnimationRoute({super.key});

  @override
  State<ScaleAnimationRoute> createState() => _ScaleAnimationRouteState();
}

//如果有多个AnimationController,则应该使用TickerProviderStateMixin
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation; //保存动画
  late AnimationController controller; //保存动画控制器

  @override
  initState() {
    super.initState();
    //动画控制器
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    //图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0)
        .animate(CurvedAnimation(parent: controller, curve: Curves.easeIn))
      ..addListener(() {
        setState(() => {});
      });

    //启动动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Image.asset(
        "images/1.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }

  @override
  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }
}

2. AnimatedBuilder ​

可以无需手动调用 addListener 监听动画然后调用 setState 更新 UI

dart
class ScaleAnimationRoute extends StatefulWidget {
  const ScaleAnimationRoute({super.key});

  @override
  State<ScaleAnimationRoute> createState() => _ScaleAnimationRouteState();
}

//如果有多个AnimationController,则应该使用TickerProviderStateMixin
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    //图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0)
        .animate(CurvedAnimation(parent: controller, curve: Curves.easeIn));

    //启动动画(正向执行)
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
        child: AnimatedBuilder(
      animation: controller,
      builder: ((context, child) {
        return Image.asset(
          "images/1.png",
          width: animation.value,
          height: animation.value,
        );
      }),
    ));
  }

  @override
  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }
}

3. SingleTickerProviderStateMixin 与 TickerProviderStateMixin ​

单个动画(调用一次),使用 SingleTickerProviderStateMixin

多个动画(多次调用),使用 TickerProviderStateMixin

dart
class _TravelPageState extends State<TravelPage> with SingleTickerProviderStateMixin {
  TabController _tabController;
  TravelTabModel travelTabModel;
  List<TravelTab> tabs = [];
  @override
  void initState() {
    _tabController = TabController(length: 0, vsync: this); //调用一次
    TravelTabDao.fetch().then((TravelTabModel model) {
      print(model);
      //又一次被调用
      _tabController = TabController(length: model.tabs.length, vsync: this);
      setState(() {
        tabs = model.tabs;
        travelTabModel = model;
      });
    }).catchError((e) {
      print(e);
    });
    super.initState();
  }
}

4. 透明动画(AnimatedOpacity) ​

dart
AnimatedOpacity(
  onEnd: () {
    Get.offNamed(SpUtil.getUserInfo() == null
        ? Routes.loginPage
        : Routes.homePage);
  },
  opacity: controller.opacityLevel,
  duration: const Duration(milliseconds: 2000),
  child: Container()
)

5. 动态图标(AnimatedIcon) ​

dart
class addView extends StatefulWidget {
  @override
  _addViewViewState createState() => _addViewViewState();
}
// 1,with TickerProviderStateMixin 要继承这样类

class _addViewViewState extends State<addView> with TickerProviderStateMixin {
  //动画效果
  late AnimationController animationController;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 1000),
    );
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return InkWell(
      child: AnimatedIcon(
          icon: AnimatedIcons.menu_arrow, progress: animationController),
      onTap: () => {
        animationController.reset(),
        animationController.forward()
      },
    );
  }
}

6. 平移动画(SlideTransition) ​

dart
class BoxMoveAnimation extends StatefulWidget {
  const BoxMoveAnimation({super.key});

  @override
  _BoxMoveAnimationState createState() => _BoxMoveAnimationState();
}

class _BoxMoveAnimationState extends State<BoxMoveAnimation>
    with TickerProviderStateMixin {
  late AnimationController controller;
  late Animation<Offset> animation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        duration: Duration(milliseconds: 2000), vsync: this);
    controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //AnimationStatus.completed 动画在结束时停止的状态
        debugPrint('完成');
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //AnimationStatus.dismissed 表示动画在开始时就停止的状态
        debugPrint('消失');
        controller.forward();
        //        controller.dispose();
      }
    });
    animation =
        Tween(begin: Offset.zero, end: Offset(2.0, 0.0)).animate(controller);
    //开始执行动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        //SlideTransition 用于执行平移动画
        child: SlideTransition(
          position: animation,
          //将要执行动画的子view
          child: Container(
            width: 200,
            height: 200,
            color: Colors.red,
          ),
        ),
      ),
    );
  }
}