# 基础

# 初始化项目

# 项目目录

# UI 库

Material Design(google):

# APP 结构

flutter 一切都是组件(Widget 类),组件嵌套组件,组件组成页面,页面组成 APP。

  • MaterialApp(顶级组件)

    • title(任务管理器中的标题)
    • home(主内容)
    • debugShowCheckedModeBanner(是否显示右上角的 debug 标签)
  • Scaffold(脚手架组件)

    • appBar(顶部导航栏)
    • body(主内容)
    • floatingActionButton(悬浮按钮)
    • drawer(左侧抽屉菜单)
    • endDrawer(右侧抽屉菜单)
    • ...

# 基础组件

# Text

文本组件,用于显示文本内容。RichText 和 TextSpan,富文本组件,用于显示富文本内容。

  • TextDirection(文字方向)
  • TextStyle(文字样式)
    • colors(文字颜色)
    • fontSize(文字大小)
    • fontWeight(文字粗细)
    • fontStyle(文字样式)
  • TextAlign(文字对齐方式)
  • TextOverflow(文字溢出方式)
  • maxLines(最大行数)
# 设置自定义字体
  1. 下载并导入字体
  1. 在 pubspec.yaml 中配置字体
flutter:
  fonts:
    - family: LiuJianMaoCao
      fonts:
        - asset: assets/fonts/LiuJianMaoCao-Regular.ttf
        - asset: assets/fonts/LiuJianMaoCao-Medium.ttf
          weight: 500
        - asset: assets/fonts/LiuJianMaoCao-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/LiuJianMaoCao-Bold.ttf
          weight: 700
  1. 使用字体
  • 为整个应用设置默认字体
@override
Widget build(BuildContext context) {
  return MaterialApp(
      title: appName, home: Home(), theme: ThemeData(fontFamily: 'LiuJianMaoCao'));
}
  • 为某个组件设置字体
Text('Hello World', style: TextStyle(fontFamily: 'LiuJianMaoCao'));

# Icon

图标库,在线预览 (opens new window)

TODO:如何自定义?

# Color

  • ARGB,十六进制表示:透明度 + 红 + 绿 + 蓝
Color.fromARGB(255, 255, 255, 255)
  • RGBO, 0-255 表示:红 + 绿 + 蓝 + 透明度
Color.fromRGBO(255, 255, 255, 1)

# Container

盒布局

  • child:子组件
  • padding(margin)
    • EdgeInsets(all、fromLTRB、only)
  • decoration
    • BoxDecoration
      • color
      • borderRadius
      • border
      • boxShadow
      • gradient
      • image
  • alignment
  • transform
    • Matrix4
      • translate
      • rotate
      • scale
      • skew

# Column 和 Row

线性布局(跟 flex 很像) Column 、 Row:

  • mainAxisAlignment:主轴对齐方式
  • crossAxisAlignment:交叉轴对齐方式
  • children:子组件

# Flex

弹性布局,相对 Column 和 Row 可以指定主轴放心 Flex

  • direction (声明主轴方向)
  • mainAxisAlignment (声明主轴对产方式)
  • textDirection (声明水平方向的排列顺序)
  • crossAxisAlignment (声明交叉轴对齐方式)
  • verticalDirection (声明垂直方向的排列顺序)
  • children (声明子组件)

Expanded (可伸缩组件,自动充满剩余空间)

  • flex (声明弹性布局所占比例)
  • child (声明子组件)

Spacer (占位组件,自动充满剩余空间)

  • flex (声明弹性布局所占比例)

# Wrap

流式布局,自动换行,解决内容溢出问题

  • spacing (主轴方向间距)
  • runSpacing (交叉轴方向间距)
  • alignment (交叉轴对齐方式)
  • runAlignment (主轴对齐方式)

# Stack

层叠布局,子组件可以重叠(类似 css z-index)

  • alignment (子组件对齐方式)
  • textDirection (子组件水平方向排列顺序)

# Positioned

层叠布局子组件,用于定位

  • child (子组件)
  • left、top、right、bottom (距离父组件边距)
  • width、height (子组件宽高)

# Image

图片

  • Image.asset('图片路径')
  • 在 pubspec.yaml 中配置图片路径
flutter:
  assets:
    - assets/images/

# NetwordImage

网络图片组件

  • Networklmage('图片地址')
  • 配置 Android 权限 (AndroidManifest.xml)
<uses-permission android:name="android.permission.INTERNET"/>
  • 配置允许访问 http
<application
  android:usesCleartextTraffic="true"
  ...
>

# Button

按钮组件 (opens new window)

  • TextButton,text
  • ElevatedButton,fill
  • OutlinedButton,stroke

属性:

# SingleChildScrollView

滚动组件 (opens new window),用于解决内容溢出问题

属性:

  • child
  • scrollDirection (滚动方向)
  • reverse (是否反向滚动)
  • physics (滚动到底部或顶部的行为)
    • BouncingScrollPhysics (允许超出边界,回弹)
    • ClampingScrollPhysics (不允许超出边界)
    • NeverScrollableScrollPhysics (不允许滚动)
    • AlwaysScrollableScrollPhysics (允许滚动)

# ListView

列表组件 (opens new window)

属性:

  • children (子组件)
  • padding (内边距)
  • reverse (是否反向)
  • scrollDirection (滚动方向)

第二种方式:

  • itemCount (子组件数量)
  • itemBuider (子组件构造器)
  • separatorBuilder (分割线构造器)

# GridView

网格组件 (opens new window)

属性:

  • children (子组件)
  • scrollDirection (滚动方向)
  • gridDelegate (网格代理)
    • SliverGridDelegateWithFixedCrossAxisCount (固定列数 - 子组件宽度自适应)
    • SliverGridDelegateWithMaxCrossAxisExtent (固定宽度 - 列数自适应)*
  • GridView.count (固定列数,构造器)
  • GridView.extent (固定宽度,构造器)*
  • GridView.builder (子组件构造器)
  • physics

# Cupertino

  • Material 安卓风格: import 'package:flutter/material.dart';
  • Cupertino iOS 风格: import 'package:flutter/cupertino.dart';
  • 判断环境:dart:io 中的 Platform.isIOS 和 Platform.isAndroid

# Safearea

安全区域组件,用于解决刘海屏、底部虚拟按键等问题

# DataTable

表格组件

# 导航组件

# BottomNavigationBar

底部导航栏组件

# DefaultTabController

TabBar + TabBarView (可滑动)

# 表单组件

# Switch

开关

# Checkbox

Checkbox 和 CheckboxListTile

# Radio

单选

# TextField

文本框

  • autofocus(获取焦点)
  • keyboardType(键盘类型)
  • obscureText(是否隐藏输入内容)
  • decoration(输入框样式)
  • onChanged(输入框内容变化时的回调)
  • labelText(输入框标题)
  • hintText(输入框提示)
  • maxLines(最大行数)

# Calendar

日历

  • CalendarDatePicker(日历选择器)
  • showCalendarMode(显示模式)

# Form

使用步骤:

  1. 创建 GlobalKey<FormState> key 对象
  2. 创建 Form 组件,传入 key
  3. 创建 TextFormField 组件,传入 validator(验证器)
  4. 验证表单:key.currentState.validate()
class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>(); // 1. 创建一个全局的key

  String? _phone;

  @override
  Widget build(BuildContext context) {
    return Container(
        child: Form(
            key: _formKey, // 2. 将 key 绑定到 Form
            child: Column(
              children: [
                TextFormField(
                  keyboardType: TextInputType.phone,
                  decoration: const InputDecoration(
                    hintText: '手机号',
                  ),
                  validator: (value) {
                    // 3. 添加验证规则
                    RegExp reg = RegExp(r'^\d{11}$');
                    if (!reg.hasMatch(value.toString())) {
                      return '请输入正确的手机号';
                    }
                    return null;
                  },
                  onSaved: (value) {
                    _phone = value.toString();// 6. 触发 save,保存数据
                  },
                ),
                Row(
                  children: [
                    Expanded(
                        child: ElevatedButton(
                      child: const Text('提交'),
                      onPressed: () {
                        if (_formKey.currentState!.validate()) {
                          // 4. 调用 validate() 方法
                          print("提交成功");
                          // 5. 调用 save() 方法
                          _formKey.currentState!.save();
                        }
                      },
                    ))
                  ],
                )
              ],
            )));
  }
}

# 第三方组件

# dio

dio (opens new window),类似 axios,用于请求数据

# flutter_swiper

flutter_swiper (opens new window),轮播组件,适配多种平台

# shared_preferences

shared_preferences (opens new window),本地数据缓存库(类似 AsyncStorage)

# 状态管理

  • Flutter 中的组件,按状态划分:

    • statelessWidget (无状态组件)
    • StatefulWidget (状态组件)
  • 按状态作用域划分

    • 组件内私有状态 (StatefulWidget)
    • 跨组件状态共享 (Inheritedwidget、 Provider) 全局状态 (Redux fish-redux、Mobx .....)
  • 状态组件的组成

    • StatefulWidget (组件本身不可变- @immutable)
    • state (将变化的状态放到 state 中维护)

# StatefulWidget (这里讲得很烂)

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'counter', home: Home());
  }
}

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

  @override
  State<Home> createState() => _HomeState();// 必须实现这个方法
}

class _HomeState extends State<Home> {// 泛型可以是 StatefulWidget 或其子类
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(_counter.toString()),
        ElevatedButton(onPressed: _incrementCounter, child: Icon(Icons.add)),
        ElevatedButton(onPressed: _decrementCounter, child: Icon(Icons.remove)),
      ],
    );
  }
}

# InheritedWidget

提供了沿树向下,共享数据的能力。解决了跨组件状态共享的问题。

使用构造函数传参,若层级过深,代码会很冗余。

使用 InheritedWidget 传参,可以解决这个问题。

使用过程:

  1. 创建一个 InheritedWidget 组件,它相当于一个数据容器,用于存放需要共享的数据。
  2. 初始化这个数据容器,它应该包裹在需要共享数据的组件的外层,并同时传入初始数据。(体现沿树向下的特点)
  3. 在子组件中使用 InheritedWidget.of(context) 获取这个数据容器的实例,从而获取到数据。
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'InheritedWidge Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
      ),
      home: const Home(),
    );
  }
}

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

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _count = 0;

  void _updateNum(int num) {
    setState(() {
      _count += num;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ShareDataWidget(// 2. 将需要共享的数据放入数据容器
        num: _count, //
        updateNum: _updateNum,
        child: const Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CounterTv(),
              CounterControl(),
            ]));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
            onPressed: () {// 3. 访问数据容器中的更新方法
              ShareDataWidget.of(context)!.updateNum(1);
            },
            child: const Icon(Icons.add)),
        ElevatedButton(
            onPressed: () {
              ShareDataWidget.of(context)!.updateNum(-1);
            },
            child: const Icon(Icons.remove)),
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Text(// 3. 访问数据容器中的数据
      ShareDataWidget.of(context)!.num.toString(),
      style: const TextStyle(fontSize: 30, color: Colors.yellow),
    );
  }
}

// 1. 声明数据容器,用于存放需要共享的数据
class ShareDataWidget extends InheritedWidget {
  const ShareDataWidget(
      {super.key,
      required this.child,
      required this.num,
      required this.updateNum})
      : super(child: child);

  final int num; // 需要在子树中共享的数据 num
  final Function updateNum;// 更新数据的方法
  final Widget child;

  static ShareDataWidget? of(BuildContext context) {
    // 返回数据容器实例
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  }

  @override
  bool updateShouldNotify(ShareDataWidget oldWidget) {
    // 该回调决定当data发生变化时,是否通知子树中依赖data的Widget
    return true;
  }
}

# 生命周期

  1. 调用构造函数 StatefulWidget
  2. createElement 创建组件元素,并挂载到元素树上
  3. createState 创建组件状态 State
  4. initState 初始化状态
  5. didChangeDependencies 数据被更新,状态依赖改变
  6. 组件被标识为 dirty,为需要构建/更新的组件
  7. build 构建组件
  8. 组件被标识为 clean,为组件构建完成

或重新构建

  • setState 状态被更新

或更新

  • didUpdateWidget 组件被更新

或销毁:

  • deactivate 组件被移除
  • dispose 组件被销毁

didChangeDependencies 的调用时机:

  1. initState 后调用
  2. InheritedWidge 更新时调用

# Provider

Provider (opens new window),是第三方对 InheritedWidget 的封装。

优点:

  • 简化资源的分配与处置
  • 懒加载

原理:

Provider 遵循观察者模式,类似于 vue 的 MVVM:

  1. 被观察者:数据模型,Model
  2. 观察者:ViewModel
  3. 生产者、消费者:View,展示 或 触发数据初始化、更新
  1. ChangeNotifier 为数据仓库
  2. ChangeNotifierProvider 为 ViewModel

使用:

  1. 安装引入 Provider
  2. 创建数据模型(T extends ChangeNotifier)
  3. 创建 Provider(注册数据模型)
  • Provider(),不会被要求随着数据的变化而重新构建
  • ChangeNotifierProvider(),会被要求随着数据的变化而被通知更新
  1. 获取数据模型并更新 UI
  • 通过上下文 BuildContext
  • 通过静态方法 Provider.of<T>(context)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(title: 'Provider demo', home: Home());
  }
}

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

  @override
  Widget build(BuildContext context) {
    // 2. 注册数据模型
    return ChangeNotifierProvider(
      create: (context) => LikesModel(),
      child: Scaffold(
        appBar: AppBar(title: const Text('Provider demo')),
        body: const LikesWidget(),
      ),
    );
  }
}

// 3. 使用数据模型
class LikesWidget extends StatelessWidget {
  const LikesWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Likes: ${context.watch<LikesModel>().like}'), // 访问方式使用 context
        TextButton(
            onPressed: () {
              Provider.of<LikesModel>(context, listen: false)
                  .addLike(); // 访问方式2:使用 Provider.of
            },
            child: Icon(Icons.thumb_up))
      ],
    );
  }
}

// 1.创建数据模型
class LikesModel extends ChangeNotifier {
  int _like = 0;
  int get like => _like;

  void addLike() {
    // 累加
    _like++;

    // 通知 UI 更新
    notifyListeners();
  }
}

# 路由与导航

  • Route
    • 一个路由是一个屏幕或页面的抽象
  • Navigator
    • 管理路由的组件。Navigator 可以通过路由入栈和出栈来实现页面之间的跳转
    • 常用属性
      • initialRoute: 初始路由,即默认页面
      • onGenerateRoute: 动态路由 (根据规则,匹配动态路由)
      • onUnknownRoute:未知路由,也就是 404
      • routes:路由集合

# 匿名路由

  • push
Navigator.push(context, MaterialPageRoute(builder: (context) => 组件名称()));
  • pop
Navigator.pop(context);

具体用法:

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: 'Provider demo', home: PageList());
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Page List')),
        body: Center(
          child: ElevatedButton(
              onPressed: () {
                Navigator.push(
                    context,
                    MaterialPageRoute(
                        builder: (context) => const PageDetail()));
              },
              child: const Text("Route To Detail")),
        ));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page Detail'),
        leading: IconButton(
            onPressed: () {
              Navigator.pop(context);
            },
            icon: const Icon(Icons.arrow_back)),
      ),
      body: const Center(
        child: Text("Page Detial"),
      ),
    );
  }
}

# 命名路由

  1. 在 MaterialApp 中配置路由集合 routes
  2. 使用 initialRoute 指定默认路由
  3. onUnknownRoute 指定未知路由
  4. 使用 Navigator.pushNamed(context, '路由名称') 跳转到指定路由
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由 demo',
      routes: {
        'page_list': (context) => PageList(),
        'page_detail': (context) => PageDetail()
      },
      initialRoute: 'page_list', // 指定首页
      onUnknownRoute: (RouteSettings routeSettings) {
        // 未知路由处理
        return MaterialPageRoute(builder: (context) => Page404());
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Page List')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                  onPressed: () {
                    Navigator.pushNamed(context, 'page_detail');
                  },
                  child: const Text("Route To Detail")),
              ElevatedButton(
                  onPressed: () {// 测试跳转到未知路由
                    Navigator.pushNamed(context, 'hhhhhhhh');
                  },
                  child: const Text("hhhhhhhh"))
            ],
          ),
        ));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page Detail'),
        leading: IconButton(
            onPressed: () {
              Navigator.pop(context);
            },
            icon: const Icon(Icons.arrow_back)),
      ),
      body: const Center(
        child: Text("Page Detial"),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page 404'),
        leading: IconButton(
            onPressed: () {
              Navigator.pop(context);
            },
            icon: const Icon(Icons.arrow_back)),
      ),
      body: const Center(
        child: Text("404"),
      ),
    );
  }
}

# 动态路由

通过 onGenerateRoute 指定的路由

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: '命名路由 demo',
        // 重点看这
        onGenerateRoute: (RouteSettings settings) {
          print("当前路径:$settings.name");

          if (['/', '/page_list'].contains(settings.name)) {
            return MaterialPageRoute(builder: (context) => const PageList());
          }

          // 匹配 /page_detail/:id
          var uri = Uri.parse(settings.name.toString());
          if (uri.pathSegments.length == 2 &&
              uri.pathSegments.first == 'page_detail') {
            var id = uri.pathSegments[1];
            return MaterialPageRoute(builder: (context) => PageDetail(id: id));
          }

          return MaterialPageRoute(builder: (context) => Page404());
        });
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Page List')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                  onPressed: () {
                    Navigator.pushNamed(context, '/page_detail/1');
                  },
                  child: const Text("Route To Detail 1")),
              ElevatedButton(
                  onPressed: () {
                    Navigator.pushNamed(context, '/page_detail/2');
                  },
                  child: const Text("Route To Detail 2")),
              ElevatedButton(
                  onPressed: () {
                    // 测试跳转到未知路由
                    Navigator.pushNamed(context, 'hhhhhhhh');
                  },
                  child: const Text("hhhhhhhh"))
            ],
          ),
        ));
  }
}

class PageDetail extends StatelessWidget {
  const PageDetail({super.key, required this.id});
  final String id;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page Detail'),
        leading: IconButton(
            onPressed: () {
              Navigator.pop(context);
            },
            icon: const Icon(Icons.arrow_back)),
      ),
      body: Center(
        child: Text("Page Detial$id"),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page 404'),
        leading: IconButton(
            onPressed: () {
              Navigator.pop(context);
            },
            icon: const Icon(Icons.arrow_back)),
      ),
      body: const Center(
        child: Text("404"),
      ),
    );
  }
}

# 路由传参

  1. 匿名路传参:主要是通过构造函数传参
  2. 命名路由传参:
  • 跳转时传参 Navigator.pushNamed(context, routename, arguments: {参数A: 'A'})
  • 页面中接收参数 ModalRoute.of(context)!.settings.arguments

# 动画(可以搜下王叔不秃 up)

  • 补间动画(Tween)

    • 定义开始点、结束点、时间线、转换时间、速度曲线。
    • 例如透明度从 0 到 1,时间线为 0-1,转换时间为 1s,速度曲线为匀速。
  • 拟物动画

    • 对真实世界的模拟,例如弹簧、阻尼、重力、抛物线动画等。

# Animation

动画库的核心类 Animation,包含动画的值和状态。有两种 监听函数:

  • 监听值的变化:addListener、removeListener
  • 监听状态的变化:addStatusListener、removeStatusListener

动画的状态AnimationStatus:

  • dismissed:动画在起始点停止
  • forward:动画正在正向执行
  • reverse:动画正在反向执行
  • completed:动画在终点停止

动画的控制器AnimationController:

  • 在指定时间内,将组件初始值演变到终止值,从而形成动画。
  • 属性:
    • duration:动画时长
    • reverseDuration:反向动画的时长,默认是 duration 的值
    • value:动画的初始值,默认是 lowerBound
    • lowerBound:动画的起始值,默认是 0
    • upperBound:动画的终止值,默认是 1
    • vsync:垂直同步信号,用于防止屏幕外动画消耗不必要的资源
  • 方法:
    • forward():正向执行动画
    • reverse():反向执行动画
    • dispose():释放资源(在不需要动画时调用,避免资源泄露)
    • stop():停止动画

# Tween

  • AnimationController 动画生成值的范围是 0-1,而 Tween 可以将其转换为任意值。
  • Tween 的唯一职责就是定义从输入范围到输出范围的映射。例如颜色区间 0 ~ 255。

用法:

  • Tween<double>(begin:起始值, end:终止值);
  • ColorTween(begin:Colors.white, end:Colors.black);

# CurvedAnimation

CurvedAnimation 用于定义动画的速度曲线。 CurvedAnimation(parent: controller, curve: Curves.easeIn, reverseCurve: Curves.easeIn);

  • parent:动画控制器对象
  • curve:正向动画的速度曲线
  • reverseCurve:反向动画的速度曲线

详见,Curves (opens new window)

# 动画实战

  1. 创建动画控制器:controller = AnimationController(duration, vsync, value)

动画是通过 TickerProvider 来驱动的,所以需要传入一个 TickerProvider 对象,一般使用 State 对象作为 TickerProvider。 TickerProvider 通知组件每一帧的时间,从而驱动动画。

  1. 声明动画曲线,声明动画取值范围

  2. 监听动画的值和状态,调用 setState() 更新 UI

  3. 绑定动画值到组件

  4. 执行动画 controller.forward()、controller.reverse()

  5. 释放动画 controller.dispose()

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: '动画', home: PageAnimation());
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('动画')),
      body: const AnimationDemo(),
    );
  }
}

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

  @override
  State<AnimationDemo> createState() => _AnimationDemoState();
}

class _AnimationDemoState extends State<AnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation animation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    // 1. 创建 AnimationController
    controller = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this, // 因为混入了 SingleTickerProviderStateMixin 才能使用 this
    );

    // 2-1. 声明动画曲线
    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);

    // 2-2. 声明动画值的范围
    animation = Tween(begin: 50.0, end: 400.0)
        .animate(controller); // 指定 animation.value 的范围

    // 3. 监听动画
    animation.addListener(() {
      // print(animation.value);
      setState(() {});
    });
  }

  @override
  void dispose() {
    super.dispose();
    controller.dispose(); // 6. 释放动画
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          ElevatedButton(
              onPressed: () {
                controller.forward(); // 5. 执行动画
              },
              child: Text('放大')),
          ElevatedButton(
              onPressed: () {
                controller.reverse();
              },
              child: Text('缩小')),
          ElevatedButton(
              onPressed: () {
                animation.addStatusListener((status) {
                  if (status == AnimationStatus.completed) {
                    controller.reverse();
                  } else if (status == AnimationStatus.dismissed) {
                    controller.forward();
                  }
                });
                controller.forward();
              },
              child: Text('重复')),
          ElevatedButton(
              onPressed: () {
                controller.stop();
              },
              child: Text('暂停')),
          Icon(
            Icons.favorite,
            color: Colors.red,
            size: animation.value, // 4.绑定动画值到组件, 这个值时从 50 到 400 的
          ),
          Opacity(
          opacity: controller.value, // 这个值时从 0 到 1 的
          child: const Text('透明度动画'))
        ],
      ),
    );
  }
}

# 交织动画

交织动画是指多个单一动画叠加或拼接而成的动画。 例如:组件变化可能涉及高度、宽度、颜色、透明度等,这些动画可以同时进行。

  • 需要给每个动画设置时间间隔Interval
  • 需要使用 Transform 对组件进行变换
    • Transform.translate:平移
    • Transform.rotate:旋转
    • Transform.scale:缩放

以下为拼接了三个动画的交织动画:

  1. 0 ~ 0.5:尺寸动画
  2. 0.5 ~ 0.8:颜色动画
  3. 0.8 ~ 1:旋转动画
import 'package:flutter/material.dart';
import 'dart:math';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: '动画', home: PageAnimation());
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('动画')),
      body: const AnimationDemo(),
    );
  }
}

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

  @override
  State<AnimationDemo> createState() => _AnimationDemoState();
}

class _AnimationDemoState extends State<AnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> animation;
  late Animation sizeAnimation;
  late Animation colorAnimation;
  late Animation rotateAnimation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    // 1. 创建 AnimationController
    controller = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this, // 因为混入了 SingleTickerProviderStateMixin 才能使用 this
    );

    // 2. 创建动画
    animation =
        CurvedAnimation(parent: controller, curve: const Interval(0.0, 0.5))
          ..addListener(() {
            setState(() {});
          });

    // 3. 让动画反复执行
    animation.addListener(() {
      animation.addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    });

    // 4.设置其它动画
    sizeAnimation =
        Tween(begin: 50.0, end: 200.0).animate(animation); // 0 ~ 0.5
    colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(
        CurvedAnimation(
            parent: controller,
            curve: const Interval(0.5, 0.8, curve: Curves.bounceIn)))
      ..addListener(() {
        setState(() {});
      });

    rotateAnimation = Tween(begin: 0.0, end: 2 * pi).animate(CurvedAnimation(
        parent: controller,
        curve: const Interval(0.8, 1.0, curve: Curves.bounceIn)))
      ..addListener(() {
        setState(() {});
      });
  }

  @override
  void dispose() {
    super.dispose();
    controller.dispose(); // 6. 释放动画
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          ElevatedButton(
              onPressed: () {
                controller.forward();
              },
              child: const Text('重复')),
          ElevatedButton(
              onPressed: () {
                controller.stop();
              },
              child: const Text('暂停')),
          Opacity(// 5. 绑定动画值到 UI
              opacity: controller.value, // 这个值时从 0 到 1 的
              child: Transform.rotate(
                  angle: rotateAnimation.value,
                  child: Container(
                    width: sizeAnimation.value,
                    height: sizeAnimation.value,
                    color: colorAnimation.value,
                  )))
        ],
      ),
    );
  }
}

# Hero 动画

Hero 动画用来实现跨页面动画效果。

  • 在不同页面中,声明一个共享组件Hero
  • 由于共享组件在不同页面中的位置、外观等不同,路由切换时,形成动画效果

步骤:

  • 在页面 A 中定义起始 Hero 组件(source hero),声明tag(一个共享组件的标记)
  • 在页面 B 中定义目标 Hero 组件(target hero),绑定相同的 tag
  • 页面跳转时,通过 Navigator,传递 tag
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Hero 动画', home: PageAnimation());
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero 动画')),
      body: const HeroAni(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      child: GridView.extent(
        maxCrossAxisExtent: 300,
        mainAxisSpacing: 10,
        children: List.generate(20, (index) {
          String imageUrl = "https://picsum.photos/200/300?random=$index";

          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) {
                    return ImageDetail(imageUrl);
                  },
                ),
              );
            },
            child: Hero(
              tag: imageUrl, // 使用图片地址当作标记
              child: Image.network(imageUrl),
            ),
          );
        }),
      ),
    );
  }
}

class ImageDetail extends StatelessWidget {
  final String imageUrl;

  const ImageDetail(this.imageUrl, {super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.black,
        body: GestureDetector(
            onTap: () {
              Navigator.pop(context);
            },
            child: Hero(
              tag: imageUrl,
              child: Flex(
                  direction: Axis.vertical,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Image.network(
                      imageUrl,
                      width: double.infinity,
                      fit: BoxFit.cover,
                    )
                  ]),
            )));
  }
}

# 多语言

# 组件多语言

  1. 安装国际化组件 flutter_localizations (opens new window)
  2. 在 MaterialApp 中配置国际化组件
import 'package:flutter_localizations/flutter_localizations.dart';

...
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由 demo',
      home: PageLogin(),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate, // Material 组件库国际化
        GlobalCupertinoLocalizations.delegate, // iOS 风格的组件国际化
        GlobalWidgetsLocalizations.delegate // 文本方向和本地化的组件
      ],
      supportedLocales: [
        const Locale('zh', 'CN'), // 中文简体
        const Locale('en', 'US'), // 美国英语
      ],
    );
  }
...

  1. 切换系统语言查看一下,一般日历那些会自动跟随系统语言

# 文本多语言

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由 demo',
      home: PageLogin(),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        CustomLocalizations.delegate // 配置自定义的代理
      ],
      supportedLocales: [
        const Locale('zh', 'CN'), // 中文简体
        const Locale('en', 'US'), // 美国英语
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print(Localizations.of(context, CustomLocalizations));

    return Scaffold(
      appBar: AppBar(title: const Text('Form')),
      body: Center(
          child: Text(
              // "Hello world",
              Localizations.of(context, CustomLocalizations).translate('title'),
              style: TextStyle(fontSize: 50))),
    );
  }
}

// 本地化代理类
class CustomLocalizationsDelegate
    extends LocalizationsDelegate<CustomLocalizations> {
  @override
  bool isSupported(Locale locale) {
    return ['en', 'zh'].contains(locale.languageCode);
  }

  @override
  Future<CustomLocalizations> load(Locale locale) {
    return SynchronousFuture(
        CustomLocalizations(locale)); // 依赖 package:flutter/foundation.dart
  }

  @override
  bool shouldReload(LocalizationsDelegate<CustomLocalizations> old) {
    return false;
  }
}

class CustomLocalizations {
  final Locale locale;

  CustomLocalizations(this.locale);

  // // 声明字典
  static final Map<String, Map<String, String>> _localizedValues = {
    'en': {
      'title': 'Hello 你',
    },
    'zh': {
      'title': '你好',
    },
  };

  // 根据 key 获取 value
  String translate(String key) {
    return _localizedValues[locale.languageCode]![key]!;
  }

  static CustomLocalizationsDelegate delegate = CustomLocalizationsDelegate();
}

# 加载语言包

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:flutter/services.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由 demo',
      home: PageLogin(),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        CustomLocalizations.delegate // 配置自定义的代理
      ],
      supportedLocales: [
        const Locale('zh', 'CN'), // 中文简体
        const Locale('en', 'US'), // 美国英语
      ],
      localeResolutionCallback: (locale, supportedLocales) {
        print("deviceLocale: $locale");
        print("languageCode: ${locale!.languageCode}");
        print("countryCode: ${locale.countryCode}");

        for (var supportedLocale in supportedLocales) {
          if (supportedLocale.languageCode == locale.languageCode &&
              supportedLocale.countryCode == locale.countryCode) {
            return supportedLocale;
          }
        }

        return supportedLocales.first;
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print(Localizations.of(context, CustomLocalizations));

    return Scaffold(
      appBar: AppBar(title: const Text('Form')),
      body: Center(
          child: Text(
              // "Hello world",
              Localizations.of(context, CustomLocalizations).translate('title'),
              style: TextStyle(fontSize: 50))),
    );
  }
}

// 本地化代理类
class CustomLocalizationsDelegate
    extends LocalizationsDelegate<CustomLocalizations> {
  @override
  bool isSupported(Locale locale) {
    return ['en', 'zh'].contains(locale.languageCode);
  }

  @override
  Future<CustomLocalizations> load(Locale locale) async {
    CustomLocalizations localizations = CustomLocalizations(locale);
    await localizations.loadJSON();
    return SynchronousFuture(localizations);
  }

  @override
  bool shouldReload(LocalizationsDelegate<CustomLocalizations> old) {
    return false;
  }
}

class CustomLocalizations {
  final Locale locale;

  CustomLocalizations(this.locale);

  late Map<String, String> _localizedValues;

  Future<bool> loadJSON() async {
    String jsonString = await rootBundle.loadString(
        'lang/${locale.languageCode}.json'); // import 'package:flutter/services.dart';
    Map<String, dynamic> jsonMap =
        json.decode(jsonString); // import 'dart:convert';

    _localizedValues = jsonMap.map((key, value) {
      return MapEntry(key, value.toString());
    });

    return true;
  }

  // 根据 key 获取 value
  String translate(String key) {
    return _localizedValues[key]!;
  }

  static CustomLocalizationsDelegate delegate = CustomLocalizationsDelegate();
}

创建 lang 目录,并新增 zh.json 和 en.json。 在 yaml 中配置语言包资源,然后通过 rootBundle 加载

  assets:
    - lang/en.json
    - lang/zh.json

# 多主题

最简单的适配:

theme: ThemeData.light(),
darkTheme: ThemeData.dark(),

更自定义的方法:

CustomTheme.dart

import 'package:flutter/material.dart';

class CustomTheme{
  // 公共样式
  Static const double _titleFontSize = 20;

  // 亮色主题
  static final ThemeData lightTheme = ThemeData(
    primarySwatch: Colors.blue,
    primaryColor: Colors.white,
    backgroundColor: Colors.grey[100],
    accentColor: Colors.blue,
    accentIconTheme: IconThemeData(color: Colors.white),
    dividerColor: Colors.white54,
    fontFamily: 'Montserrat',
    textTheme: TextTheme(
      headline1: TextStyle(fontSize: _titleFontSize, color: Colors.black),
      headline2: TextStyle(fontSize: _titleFontSize, color: Colors.white),
      bodyText1: TextStyle(fontSize: 14.0, color: Colors.black87),
      bodyText2: TextStyle(fontSize: 14.0, color: Colors.white),
    ),
  );

  // 暗色主题
  static final ThemeData darkTheme = ThemeData(
    primarySwatch: Colors.grey,
    primaryColor: Colors.black,
    backgroundColor: Colors.grey[850],
    accentColor: Colors.white,
    accentIconTheme: IconThemeData(color: Colors.black),
    dividerColor: Colors.black12,
    fontFamily: 'Montserrat',
    textTheme: TextTheme(
      headline1: TextStyle(fontSize: _titleFontSize, color: Colors.white),
      headline2: TextStyle(fontSize: _titleFontSize, color: Colors.black),
      bodyText1: TextStyle(fontSize: 14.0, color: Colors.white70),
      bodyText2: TextStyle(fontSize: 14.0, color: Colors.black),
    ),
  );
}
# 获取主题上下文
Theme.of(context)