【Flutter 组件】001-关于 Widget 的一切
文章目录
- 【Flutter 组件】001-关于 Widget 的一切
- 一、概述
- 1、Widget 基本概述
- 2、Flutter Framework 里的 Widget
- 架构图
- 说明
- 3、根 Widget
- 二、Widget 类
- 1、Widget 的功能
- 2、Widget 类
- 源码
- 说明
- Widget 的标识符:Key
- Flutter 中如何在 diff 过程中判断哪些 Widget 没有变化
- Key 的分类
- key 的使用
- 三、Flutter中的四棵树
- 1、概述
- 2、举个例子
- 一个 Widget 树示例:
- 说明
- 代码演示
- 三棵树图示
- 四、StatelessWidget
- 1、概述
- 2、一个简单的示例
- 继承 `StatelessElement` 类实现自定义组件
- 使用自定义的类
- 运行结果
- 3、Context
- 概述
- 获取父级 widget 代码示例
- 五、StatefulWidget
- 1、概述
- 概述
- StatefulWidget 类
- 说明
- 2、State 状态
- 简介
- State生命周期
- 3、在 widget 树中获取 State 对象
- 通过 Context 获取
- 通过 GlobalKey 获取
- 六、自定义 Widget 的三种方式
- 1、Flutter 自定义 Widget 的三种方式
- 2、通过继承实现自定义
- 概述
- 一个官方的 Dialog 例子
- 我们继承 Dialog 来实现一个加载中的对话框
- 3、通过组合实现自定义
- 概述
- 自定义 ToolBar 示例
- 4、通过 RenderObject 自定义
- 概述
- 一个示例
- CustomPaint 绘制
- 七、附加内容
- 1、Flutter 方法的封装示例
- N、参考资料
一、概述
1、Widget 基本概述
Flutter 中的 Widget 相当于 Android 里的 View,iOS 里的 UIView。
-
Flutter 中几乎所有的对象都是一个 **widget **;
-
Widget 不仅可以表示 UI 元素,还可以表示一些功能性的组件,如用于手势检测的
GestureDetector
、用于APP主题数据传递的Theme
等等; -
我们将 Widget 统称为组件。
界面刷新机制,类似 React
当 Widget 状态发生变化,需要更新界面时,框架会先计算从上一个状态转换到下一个状态所需的最小更改,然后再去刷新界面。
2、Flutter Framework 里的 Widget
架构图
说明
Framework 里面有一层是 Widgets,在 Widgets 层下面,有:
- Rendering(渲染层)
- Animation、Painting、Gestures(动画、绘制、手势)
- Foundation(基础库层)
Widgets 层下面平常使用较少,也比较复杂,常用 Widgets 层上面的 Material 和 Cupertino。
摘录:Material & Cupertino 指的 Widget 的风格是 Material 或 Cupertino 。Flutter 为了减轻开发人员的工作量,实现了两种不同风格的组件:Material 和 Cupertino 。**Material 用于 Android,Cupertino 用于 iOS。**有了这些组件,开发人员不需要再做额外的工作,就可以让 Flutter 的 UI 风格适应不同的平台,让 Flutter UI 获得和 Native UI 一样的使用体验。
3、根 Widget
我们常用的 MaterialApp 就是根(Root)Widget ,Flutter会默认把 根Widget 充满屏幕。
根 Widget 只能是下面三个:
-
WidgetsApp
WidgetsApp 是可以自定义风格的 根Widget。
-
MaterialApp
MaterialApp 是在 WidgetsApp 上添加了很多 material-design 的功能,是 Material Design 风格的 根Widget。
-
CupertinoApp
CupertinoApp 也是基于 WidgetsApp 实现的 iOS 风格的 根Widget。
MaterialApp 是最常用的,也是功能最完善的,且经常与 Scaffold 一起使用。
二、Widget 类
1、Widget 的功能
描述一个UI元素的配置信息。
就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。
Widget 描述了他们的视图在给定其当前配置和状态时应该看起来像什么。
2、Widget 类
源码
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
Element createElement();
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
bool operator ==(Object other) => super == other;
int get hashCode => super.hashCode;
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
static int _debugConcreteSubtype(Widget widget) {
return widget is StatefulWidget ? 1 :
widget is StatelessWidget ? 2 :
0;
}
}
说明
Widget
类本身是一个抽象类,其中最核心的就是定义了createElement()
接口。我们在开发中一般不直接继承 Widget 而是 Widget 的子类:StatelessWidget
或StatefulWidget
来间接继承widget
类。
-
@immutable
:代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final)。为什么?因为 Flutter 中的属性发生变化会导致重新构建 Widget 树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。 -
widget
类继承自DiagnosticableTree
,DiagnosticableTree
即“诊断树”,主要作用是提供调试信息。 -
Key
: 这个key
属性类似于 React/Vue 中的key
,主要的作用是决定是否在下一次build
时复用旧的 widget ,决定的条件在canUpdate()
方法中。 -
createElement()
:正如前文所述“一个 widget 可以对应多个Element
”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element
对象。 此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。 -
debugFillProperties(...)
复写父类的方法,主要是设置诊断树的一些特性。 -
canUpdate(...)
是一个静态方法,它主要用于在 widget 树重新build
时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧 UI 树上所对应的Element
对象的配置;通过其源码我们可以看到,只要newWidget
与oldWidget
的runtimeType
和key
同时相等时就会用new widget
去更新Element
对象的配置,否则就会创建新的Element
。
Widget 的标识符:Key
diff 简介: 因为 Flutter 采用的是 react-style 的框架,每次刷新 UI 的时候,都会重新构建新的 Widget树,然后和之前的 Widget树 进行对比,计算出变化的部分,这个计算过程叫做 diff 。
标识符: 在 diff 过程中,如果能提前知道哪些 Widget 没有变化,无疑会提高 diff 的性能,这时候就需要使用到标识符。
为了在 diff 过程中,知道 Widget 有没有变化,就需要给 Widget 添加一个唯一的标识符,然后在 Widget树 的 diff 过程中,查看刷新前后的 Widget树 有没有相同标识符的 Widget,如果标识符相同,则说明 Widget 没有变化,否则说明 Widget 有变化。
假设 UI 刷新前,Widget树 是 A,在 A 里有一个标识符为 a 的 Widget,在 UI 刷新后,重建的 Widget树 是 B,如果 B 里还有标识符为 a 的 Widget,则说明这个 Widget 没变,但是如果 B 里没有标识符为 a 的 Widget,那么说明这个 Widget 发生了变化。
这个标识符在 Flutter 中就是 Key,所有 Widget 都有 Key 这一个属性。
Flutter 中如何在 diff 过程中判断哪些 Widget 没有变化
稍微有些复杂,有两种情况:
-
默认情况下( Widget 没有设置 Key)
当没有给 Widget 设置 Key 时,Flutter 会根据 Widget 的 runtimeType 和显示顺序是否相同来判断 Widget 是否有变化。
runtimeType 是 Widget 的类型,例如 Text 和 RaisedButton 就是不同的类型。
-
Widget 有 Key
当给 Widget 设置了 Key 时,Flutter 是根据 Key 和 runtimeType 是否相同来判断 Widget 是否有变化。
Key 的分类
Key 总共分为两类:
- Local Key(局部Key)
- Global Key(全局Key)
1. Local Key(局部Key)
在有相同父级的 Widget 中,Key 必须是唯一的,这样的 Key 叫做 局部 Key。
局部Key 在 Flutter 中对应的抽象类是 LocalKey。LocalKey 有不同的实现,主要的区别就是使用什么值来作为 Key 的值:
-
ObjectKey
将对象作为 Key 的值。
-
ValueKey
使用特定类型的值来作为 Key 的值。
-
UniqueKey
使用 UniqueKey 自己的对象作为 Key 的值,所以只与自身相等,称为 唯一 Key。
2.Global Key(全局Key)
全局 Key 是在整个 APP 中唯一的 Key。
全局 Key 在 Flutter 中对应的抽象类是 GlobalKey。GlobalKey 有不同的实现,主要区别是使用的场景不同:
-
LabeledGlobalKey
LabeledGlobalKey 用于调试,不会用来比较 Widget 是否有变化。
-
GlobalObjectKey
将对象作为 Global Key 的值。
key 的使用
一般不用,页面复杂时用。
一般情况下我们不需要使用 Key,但是当页面比较复杂时,就需要使用 Key 去提升渲染性能。
三、Flutter中的四棵树
1、概述
Flutter 中的 Widget 是用来秒数 UI 元素的配置信息的,那么真正的布局绘制由谁来完成呢?
Flutter 框架的的处理流程:
根据 Widget 树生成 Element 树,根据 Element 树生成 Render 树,根据 Render 树生成 Layer 树。
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自
Element
类。 - 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自
RenderObject
类。 - 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自
Layer
类。
真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。
2、举个例子
一个 Widget 树示例:
Container( // 一个容器 widget
color: Colors.blue, // 设置容器背景色
child: Row( // 可以将子widget沿水平方向排列
children: [
Image.network('https://www.example.com/1.png'), // 显示图片的 widget
const Text('A'),
],
),
);
说明
如上 Container 设置了背景色,Container 内部会创建一个新的 ColoredBox 来填充背景,相关逻辑如下:
// 源代码位置:D:/MySoft/Flutter/SDK/flutter/packages/flutter/lib/src/widgets/container.dart:403
if (color != null) {
current = ColoredBox(color: color!, child: current);
}
代码演示
代码
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// 此处返回的是根组件
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container( // 一个容器 widget
color: Colors.blue, // 设置容器背景色
child: Row( // 可以将子widget沿水平方向排列
children: [
Image.network('https://p3-passport.byteimg.com/img/user-avatar/f755e80a8a455a4d9de36f5f096784e6~100x100.awebp'), // 显示图片的 widget
const Text('訾博', style: TextStyle(color: Colors.white)), // 显示文本的 widget
],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
图示
三棵树图示
注意:
- 三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如
StatelessWidget
和StatefulWidget
都没有对应的 RenderObject。 - 渲染树在上屏前会生成一棵 Layer 树,前期只需要记住以上三棵树就行。
四、StatelessWidget
1、概述
无状态的组件!
StatelessWidget
相对比较简单,它继承自widget
类,重写了createElement()
方法:
StatelessElement createElement() => StatelessElement(this);
StatelessElement
间接继承自 Element
类,与 StatelessWidget
相对应(作为其配置数据)。
StatelessElement 用于不需要维护状态的场景,它通常在 build 方法中通过嵌套其他 widget 来构建 UI,在构建过程中会递归的构建其嵌套的 widget 。
2、一个简单的示例
继承 StatelessElement
类实现自定义组件
import 'package:flutter/material.dart';
class Echo extends StatelessWidget {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey,
}):super(key:key);
final String text;
final Color backgroundColor;
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
使用自定义的类
import 'package:flutter/material.dart';
import 'package:study/echo.dart';
// 省略其他内容......
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const Echo(text: '訾博', backgroundColor: Colors.red,),
);
}
}
运行结果
3、Context
概述
build
方法有一个context
参数,它是 BuildContext
类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。
实际上,context
是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。
获取父级 widget 代码示例
重视这段代码的写法!
class ContextRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context测试"),
),
body: Container(
// 主义这个 Builder ,一般我们返回的是一个 widget 但有时需要逻辑计算,就可以使用这个 Builder
child: Builder(builder: (context) {
// 在 widget 树中向上查找最近的父级 `Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar 的 title, 此处实际上是 Text("Context测试")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
五、StatefulWidget
1、概述
概述
有状态的组件!
和 StatelessWidget
一样,StatefulWidget
也是继承自 widget
类,并重写了 createElement()
方法,不同的是返回的 Element
对象并不相同;另外 StatefulWidget
类中添加了一个新的接口createState()
。
StatefulWidget 类
abstract class StatefulWidget extends Widget {
const StatefulWidget({ super.key });
StatefulElement createElement() => StatefulElement(this);
State createState();
}
说明
-
StatefulElement
间接继承自Element
类,与StatefulWidget
相对应(作为其配置数据)。StatefulElement
中可能会多次调用createState()
来创建状态(State)对象。 -
createState()
用于创建和 StatefulWidget 相关的状态,它在 StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的 State 实例,其实,本质上就是一个
StatefulElement
对应一个 State 实例。
2、State 状态
简介
State 是其对用的 StatefulWidget 要维护的状态:
一个 StatefulWidget 类会对应一个 State 类,State 表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:
- 在 widget 构建时可以被同步读取。
- 在 widget 生命周期中可以被改变,当State被改变时,可以手动调用其
setState()
方法通知 Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build
方法重新构建 widget 树,从而达到更新 UI 的目的。
State 中有两个常用属性:
widget
,它表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI 树上的某一个节点的 widget 实例在重新构建时可能会变化,但 State 实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置 State. widget 为新的 widget 实例。context
,StatefulWidget 对应的 BuildContext,作用同 StatelessWidget 的 BuildContext。
State生命周期
图示
说明
-
initState
:当 widget 第一次插入到 widget 树时会被调用,对于每一个 State 对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType
(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget
),原因是在初始化完成后, widget 树中的InheritFrom widget
也可能会发生变化,所以正确的做法应该在在build()
方法或didChangeDependencies()
中调用它。当 widget 第一次插入到 widget 树时会被调用,只会调用一次,常用于初始化状态。
-
didChangeDependencies()
:当 State 对象的依赖发生变化时会被调用;例如:在之前build()
中包含了一个InheritedWidget
,然后在之后的build()
中Inherited widget
发生了变化,那么此时InheritedWidget
的子 widget 的didChangeDependencies()
回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的didChangeDependencies
也会被调用。 -
build()
:此回调读者现在应该已经相当熟悉了,它主要是用于构建 widget 子树的,会在如下场景被调用:- 在调用
initState()
之后; - 在调用
didUpdateWidget()
之后; - 在调用
setState()
之后; - 在调用
didChangeDependencies()
之后; - 在 State 对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
- 在调用
-
reassemble()
:此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在 Release 模式下永远不会被调用。 -
didUpdateWidget ()
:在 widget 重新构建时,Flutter 框架会调用widget.canUpdate
来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate
返回true
则会调用此回调。正如之前所述,widget.canUpdate
会在新旧 widget 的key
和runtimeType
同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()
就会被调用。 -
deactivate()
:当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()
方法。 -
dispose()
:当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
3、在 widget 树中获取 State 对象
由于 StatefulWidget 的的具体逻辑都在其 State 中,所以很多时候,我们需要获取 StatefulWidget 对应的State 对象来调用一些方法,比如
Scaffold
组件对应的状态类ScaffoldState
中就定义了打开 SnackBar(路由页底部提示条)的方法。我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。
通过 Context 获取
context
对象有一个 findAncestorStateOfType()
方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。
代码示例
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("子树中获取 State 对象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父级【最近的】 Scaffold 对应的 ScaffoldState 对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单'),
);
}),
],
),
),
drawer: Drawer(),
);
}
}
约定
如果希望暴露状态,则提供一个静态的 of 方法来获取其 State 状态,反之则不提供!
一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其 State 对象。但是通过 context.findAncestorStateOfType
获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:**如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of
静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State 不希望暴露,则不提供of
方法。**这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold
也提供了一个of
方法,我们其实是可以直接调用它的:
代码示例 1
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 直接通过 of 静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单2'),
);
}),
代码示例 2
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("我是SnackBar")),
);
},
child: Text('显示SnackBar'),
);
}),
通过 GlobalKey 获取
开销较大,如果有其他可选方案,应尽量避免使用它!
GlobalKey 不能重复!
Flutter还有一种通用的获取 State
对象的方法——通过 GlobalKey 来获取! 步骤分两步:
-
给目标
StatefulWidget
添加GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储 static GlobalKey<ScaffoldState> _globalKey= GlobalKey(); ... Scaffold( key: _globalKey , //设置key ... )
-
通过
GlobalKey
来获取State
对象_globalKey.currentState.openDrawer()
GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了 GlobalKey
,那么我们便可以通过globalKey.currentWidget
获得该 widget 对象、globalKey.currentElement
来获得 widget 对应的 element 对象,如果当前 widget 是 StatefulWidget
,则可以通过 globalKey.currentState
来获得该 widget 对应的 state 对象。
注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。
六、自定义 Widget 的三种方式
1、Flutter 自定义 Widget 的三种方式
-
通过继承 Widget 来修改和扩展它的功能;
-
通过组合 Widget 来扩展功能;
-
使用 CustomPaint 绘制自定义 Widget。
CustomPaint 继承自 SingleChildRenderObjectWidget
这几种方式都有各自的优势和特点,相对来说 CustomPaint 绘制实现自定义是这里面比较复杂的一种自定义 Widget 方式。Flutter 中的很多基础 Widget 也是通过继承 Widget 进行扩展形成新的 Widget 或者是自己绘制 Widget。
2、通过继承实现自定义
概述
首先我们看下通过 Widget 的继承来实现自定义 Widget 组件。这种例子在 Flutter 中不在少数,例如:NetworkImage 和 AssetImage 都是继承 ImageProvider 来实现不同场景功能的、Center 是继承自 Align 来实现的等等。所以我们可以根据具体的使用场景、特点来选择基础 Widget 进行继承,从而实现我们想要的功能。
一个官方的 Dialog 例子
// 继承自StatelessWidget
class Dialog extends StatelessWidget {
// 构造方法,设置传参
const Dialog({
Key key,
this.backgroundColor,
this.elevation,
this.insetAnimationDuration = const Duration(milliseconds: 100),
this.insetAnimationCurve = Curves.decelerate,
this.shape,
this.child,
}) : super(key: key);
// 设置属性
final Color backgroundColor;
final double elevation;
final Duration insetAnimationDuration;
final Curve insetAnimationCurve;
final ShapeBorder shape;
final Widget child;
static const RoundedRectangleBorder _defaultDialogShape =
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0)));
static const double _defaultElevation = 24.0;
// 构建布局样式
Widget build(BuildContext context) {
final DialogTheme dialogTheme = DialogTheme.of(context);
// 具体构建布局样式
return AnimatedPadding(
padding: MediaQuery.of(context).viewInsets + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
duration: insetAnimationDuration,
curve: insetAnimationCurve,
child: MediaQuery.removeViewInsets(
removeLeft: true,
removeTop: true,
removeRight: true,
removeBottom: true,
context: context,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 280.0),
child: Material(
color: backgroundColor ?? dialogTheme.backgroundColor ?? Theme.of(context).dialogBackgroundColor,
elevation: elevation ?? dialogTheme.elevation ?? _defaultElevation,
shape: shape ?? dialogTheme.shape ?? _defaultDialogShape,
type: MaterialType.card,
child: child,
),
),
),
),
);
}
}
我们继承 Dialog 来实现一个加载中的对话框
import 'package:flutter/material.dart';
// 继承我们的Dialog组件,这样它就具有Dialog的一些特性和方法属性
class LoadingDialog extends Dialog {
String text;
// 建立构造方法,传递参数
LoadingDialog({Key key, this.text}) : super(key: key);
Widget build(BuildContext context) {
// 具体构建逻辑
return Material(
type: MaterialType.transparency,
child: Center(
child: SizedBox(
width: 120.0,
height: 120.0,
child: Container(
decoration: ShapeDecoration(
color: Color(0xffffffff),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
Padding(
padding: const EdgeInsets.only(
top: 20.0,
),
child: Text(
text,
style: TextStyle(fontSize: 12.0),
),
),
],
),
),
),
),
);
}
}
// 调用使用的地方
class CustomWidgetSamples extends StatefulWidget {
State<StatefulWidget> createState() {
return CustomWidgetSamplesState();
}
}
class CustomWidgetSamplesState extends State<CustomWidgetSamples> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('CustomWidget'), primary: true),
body: Container(
child: Align(
alignment: Alignment.center,
// 构造并传递参数
child: LoadingDialog(text: '加载中...'),
),
));
}
}
// 我们只需传递我们的text参数即可
LoadingDialog(text: '加载中...'),
3、通过组合实现自定义
概述
Widget 组合,顾名思义,就是将各种 Flutter 的基础 Widget,进行不同的选择、组合拼装,来实现一个可以满足我们需求的、新的 Widget。Flutter 的基础 Widget 中,也有很多是通过组合来实现的。
自定义 ToolBar 示例
// 自定义一个ToolBar
import 'package:flutter/material.dart';
class ToolBar extends StatefulWidget implements PreferredSizeWidget {
// 构造方法,设置传递参数
ToolBar({ this.onTap}) : assert(onTap != null);
// 属性参数,点击回调
final GestureTapCallback onTap;
State createState() {
return ToolBarState();
}
// AppBar需要实现 PreferredSizeWidget
Size get preferredSize {
return Size.fromHeight(56.0);
}
}
class ToolBarState extends State<ToolBar> {
Widget build(BuildContext context) {
// 设置布局
return SafeArea(
top: true,
child: Container(
color: Colors.blue,
child: Row(
children: <Widget>[
Icon(
Icons.menu,
color: Colors.white,
size: 39,
),
Expanded(
child: Container(
color: Colors.white,
padding: EdgeInsets.all(5),
margin: EdgeInsets.all(5),
child: Text(
'搜索...',
style: TextStyle(fontSize: 18),
),
),
),
GestureDetector(
onTap: this.widget.onTap,
child: Icon(
Icons.photo_camera,
color: Colors.white,
size: 39,
),
)
],
),
),
);
}
}
// 调用自定义ToolBar
class CustomWidgetSamples extends StatefulWidget {
State<StatefulWidget> createState() {
return CustomWidgetSamplesState();
}
}
class CustomWidgetSamplesState extends State<CustomWidgetSamples> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
// 设置自定义ToolBar
appBar:ToolBar(
onTap: () {
print('click');
},
),
primary: true,
body: Column(
children: <Widget>[
Container(
child: Align(
alignment: Alignment.center,
child: LoadingDialog(text: '加载中...'),
),
)
],
));
}
}
4、通过 RenderObject 自定义
通过 CustomPaint 绘制 Widget
概述
StatelessWidget
和 StatefulWidget
都是用于组合其他组件的,它们本身没有对应的 RenderObject。Flutter 组件库中的很多基础组件都不是通过 StatelessWidget
和 StatefulWidget
来实现的,比如 Text 、Column、Align等,就好比搭积木,StatelessWidget
和 StatefulWidget
可以将积木搭成不同的样子,但前提是得有积木,而这些积木都是通过自定义 RenderObject 来实现的。实际上Flutter 最原始的定义组件的方式就是通过定义 RenderObject 来实现,而StatelessWidget
和 StatefulWidget
只是提供的两个帮助类。
一个示例
class CustomWidget extends LeafRenderObjectWidget{
RenderObject createRenderObject(BuildContext context) {
// 创建 RenderObject
return RenderCustomObject();
}
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// 更新 RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
void performLayout() {
// 实现布局逻辑
}
void paint(PaintingContext context, Offset offset) {
// 实现绘制
}
}
如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget ,我们可以看一下它的实现:
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
const LeafRenderObjectWidget({ Key? key }) : super(key: key);
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
很简单,就是帮 widget 实现了createElement 方法,它会为组件创建一个 类型为 LeafRenderObjectElement 的 Element 对象。如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承 SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。
然后我们重写了 createRenderObject 方法,它是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用(构建渲染树时)用于生成渲染对象。我们的主要任务就是来实现 createRenderObject 返回的渲染对象类,本例中是 RenderCustomObject 。updateRenderObject 方法是用于在组件树状态发生变化但不需要重新创建 RenderObject 时用于更新组件渲染对象的回调。
RenderCustomObject 类是继承自 RenderBox,而 RenderBox 继承自 RenderObject,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑。
CustomPaint 绘制
CustomPaint 的构造方法
const CustomPaint({
Key key,
// CustomPainter背景
this.painter,
// CustomPainter前景画笔
this.foregroundPainter,
// 画布尺寸,
this.size = Size.zero,
// 是否是复杂的绘制,Flutter会设置一些缓存优化策略
this.isComplex = false,
// 下一帧是否会改变
this.willChange = false,
// 子元素,可以为空
Widget child,
})
绘制的核心就是自定义 CustomPainter,我们简单看下 CustomPainter 里面的方法结构:
class Sky extends CustomPainter {
// 绘制方法
void paint(Canvas canvas, Size size) {
// canvas为画布
// size为画布大小
}
// 刷新布局时是否重绘,可以根据实际情况进行返回值
bool shouldRepaint(Sky oldDelegate) => false;
}
// Canvas和其他平台的Canvas功能和作用基本一样,包含很多绘制方法
void save() native 'Canvas_save';
void saveLayer(Rect bounds, Paint paint);
void _saveLayerWithoutBounds(List<dynamic> paintObjects, ByteData paintData)
native 'Canvas_saveLayerWithoutBounds';
void restore() native 'Canvas_restore';
int getSaveCount() native 'Canvas_getSaveCount';
void translate(double dx, double dy) native 'Canvas_translate';
void scale(double sx, [double sy]) => _scale(sx, sy ?? sx);
void rotate(double radians) native 'Canvas_rotate';
void skew(double sx, double sy) native 'Canvas_skew';
void transform(Float64List matrix4);
void clipRect(Rect rect, { ClipOp clipOp: ClipOp.intersect, bool doAntiAlias = true });
void clipRRect(RRect rrect, {bool doAntiAlias = true});
void clipPath(Path path, {bool doAntiAlias = true});
void drawColor(Color color, BlendMode blendMode);
void drawLine(Offset p1, Offset p2, Paint paint);
void drawPaint(Paint paint);
void drawRect(Rect rect, Paint paint);
void drawRRect(RRect rrect, Paint paint);
void drawDRRect(RRect outer, RRect inner, Paint paint);
void drawOval(Rect rect, Paint paint);
void drawCircle(Offset c, double radius, Paint paint);
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint);
void drawPath(Path path, Paint paint);
void drawImage(Image image, Offset p, Paint paint);
void drawImageRect(Image image, Rect src, Rect dst, Paint paint);
void drawImageNine(Image image, Rect center, Rect dst, Paint paint);
void drawPicture(Picture picture);
void drawParagraph(Paragraph paragraph, Offset offset);
void drawPoints(PointMode pointMode, List<Offset> points, Paint paint);
void drawRawPoints(PointMode pointMode, Float32List points, Paint paint);
void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint);
void drawAtlas(Image atlas,
List<RSTransform> transforms,
List<Rect> rects,
List<Color> colors,
BlendMode blendMode,
Rect cullRect,
Paint paint);
void drawRawAtlas(Image atlas,
Float32List rstTransforms,
Float32List rects,
Int32List colors,
BlendMode blendMode,
Rect cullRect,
Paint paint);
void drawShadow(Path path, Color color, double elevation, bool transparentOccluder);
在进行 Canvas 画布绘制时,我们就需要画笔 Paint,我们需要创建相应的画笔来绘制到 Canvas 上。Paint 画笔也有很多可以设置的属性,常用的有:
color:画笔颜色
style:绘制模式,画线 or 充满
maskFilter:绘制完成,还没有被混合到布局上时,添加的遮罩效果,比如blur效果
strokeWidth:线条宽度
strokeCap:线条结束时的绘制样式
shader:着色器,一般用来绘制渐变效果或ImageShader
... ...
// 可以这样使用
Paint myPaint = Paint()
..color = Colors.blueAccent // 画笔颜色
..strokeCap = StrokeCap.round // 画笔笔触类型
..isAntiAlias = true // 是否启动抗锯齿
..blendMode = BlendMode.exclusion // 颜色混合模式
..style = PaintingStyle.fill // 绘画风格,默认为填充
..colorFilter = ColorFilter.mode(Colors.blueAccent,
BlendMode.exclusion) // 颜色渲染混合模式
..maskFilter = MaskFilter.blur(BlurStyle.inner, 2.0) // 模糊遮罩效果
..filterQuality = FilterQuality.high // 颜色渲染模式的质量
..strokeWidth = 10.0; // 画笔的宽度
通过 CustomPaint 自定义的 Widget
class CustomWidgetSamplesState extends State<CustomWidgetSamples> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('CustomWidget'),
),
body: Column(
children: <Widget>[
CustomPaint(
painter: Sky(),
child: Center(
child: Text(
'文字',
),
),
)
],
));
}
}
class Sky extends CustomPainter {
void paint(Canvas canvas, Size size) {
// 绘制圆角矩形
// 用Rect构建一个边长50,中心点坐标为150,150的矩形
Rect rectCircle =
Rect.fromCircle(center: Offset(150.0, 150.0), radius: 60.0);
// 根据上面的矩形,构建一个圆角矩形
RRect rrect = RRect.fromRectAndRadius(rectCircle, Radius.circular(30.0));
canvas.drawRRect(rrect, Paint()..color = Colors.yellow);
}
bool shouldRepaint(Sky oldDelegate) => false;
bool shouldRebuildSemantics(Sky oldDelegate) => false;
}
七、附加内容
1、Flutter 方法的封装示例
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class Utils {
BuildContext context;
// 可以设置构造方法,传递参数,参数传递时有区别,通过key:value形式
Utils({ this.context}) : assert(context != null);
// 首先指定返回类型,然后定义方法名
/// 获取时间戳毫秒数,13位
int getMilliseconds() {
return DateTime.now().millisecondsSinceEpoch;
}
// 方法名后可以设置传递的参数
/// 复制到剪贴板
void setClipData(String text) {
Clipboard.setData(ClipboardData(text: text));
}
// 以下划线开始的方法名这个类的外部不可以调用,只能内部进行调用使用
/// 获取屏幕宽度
double _getScreenWidth(BuildContext context) {
return MediaQuery.of(context).size.width;
}
/// 获取屏幕高度
double getScreenHeight(BuildContext context) {
return MediaQuery.of(context).size.height;
}
/// 获取屏幕状态栏高度
double getStatusBarTop(BuildContext context) {
return MediaQuery.of(context).padding.top;
}
/// 获取屏幕方向
Orientation getScreenOrientation(BuildContext context) {
return MediaQuery.of(context).orientation;
}
Future<String> getBatteryLevel() async {
var batteryLevel = 'unknown';
MethodChannel methodChannel = MethodChannel('samples.flutter.io/battery');
try {
int result = await methodChannel.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level: $result%';
} on PlatformException {
batteryLevel = 'Failed to get battery level.';
}
return batteryLevel;
}
}
// 使用时,构造方法传参通过key:value形式传递设置
Utils utils = Utils(context: context);
// 调用方法
utils.getScreenHeight(context);
N、参考资料
Flutter Widget详解
https://blog.csdn.net/u011578734/article/details/108781671
Flutter实战
https://book.flutterchina.club/chapter2/flutter_widget_intro.html#_2-2-1-widget-概念
Flutter 完全手册
https://juejin.cn/book/6844733786626719757
Flutter学习记录——24.自定义 Widget 及方法封装
https://blog.51cto.com/u_15781233/5654671