前言
Android和Flutter分别采用命令式(主动设置)和声明式(被动变化)。
class _TestPageState extends State<MyHomePage> {
var visible = false;
void _switchVisible() {
setState(() {
visible = !visible;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Visibility(visible: visible, child: const Text("文本")),
ElevatedButton(
onPressed: _switchVisible, child: Text(visible ? "隐藏文本" : "显示文本"))
],
);
}
}
声明式UI只需要配置好状态(数据)和界面(控件)关系,Flutter会根据状态变化自动更新UI。命令式->你让它动;声明式->它自己动。
三棵树
Flutter三棵树渲染机制。
- Widget: 对视图的结构化描述,存储视图渲染相关的配置信息:布局、渲染属性、事件响应等信息
- Element: Widget实例化对象,承载视图构建的上下文数据,链接Widget到完成最终渲染的桥梁
- RenderObject: 负责实现视图渲染对象。
flutter渲染三步:
- 通过Widget树生成对应的Element
- 创建相应的RenderObject并关联到Element.renderObject属性上。
- 构建成RenderObject树,深度优先遍历,确定树中各个对象的位置和尺寸(布局),把他们绘制到不同图层上。Skia在Vsync信号同步时直接从渲染树合成Bitmap,最后交给GPU渲染。
增加Element中间层的好处(提高渲染效率):
将Widget树的变化(diff)做抽象,只将真正需要修改的部分同步到RenderObject树中,最大程度降低对真实渲染视图的修改,提高渲染效率,不是销毁整个渲染视图重建。Element是可复用的,Widget触发重建,Flutter会根据重新前后Widget树的渲染类型及属性变化情况决定后续的复用或新建。如:只是调整了渲染样式,Flutter会通知Element复用现有节点。
- 没有状态改变,只是用作展示用 StatelessWidget
- 需要保存(绑定)状态,可能出现状态变化,用StatefulWidget
Widget是不变的,变化的是与之绑定的State,State任何更改都会强制Widget的重新构建。
StatefulWdiget发生状态改变流程: setState->build()->didUpdateWidget(处理Widget更新)->dispose(释放资源)->initState(初始化状态)->didChangeDependencies(处理依赖关系的变化)->build(根据新的状态构建Widget)
根据标记来更改不同的Widget:
class _SampleAppPageState extends State<MyHomePage>{
bool toggle = true;
void _toggle(){
setState(() {
toggle = !toggle;
});
}
// 根据标记返回不同的Widget
Widget _getToggleChild(){
if(toggle){
return const Text("Toggle One");
}else{
return ElevatedButton(onPressed: (){}, child: const Text("Toggle Two"));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Sample App"),),
body: Center(child: _getToggleChild(),),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: "update Text",
child: const Icon(Icons.update),
),
);
}
}
Widget淡出动画
class _MyFadeTest extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController controller;
@override
void initState() {
super.initState();
// 初始化,控制动画的执行过程:开始、暂停、停止、反向播放
controller = AnimationController(
/// 垂直同步信号,用于同步屏幕刷新和动画更新,避免出现屏幕闪烁、撕裂等问题
/// this->TickerProviderStateMixin
/// 这个Ticker除了在垂直同步时发出信号,还在运行时创建一个介于0-1间的线性差值。
vsync: this,
// 动画执行时间
duration: const Duration(milliseconds: 2000));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("动画"),
),
body: Center(
// 过度动画组件,透明度渐变动画
child: FadeTransition(
opacity: controller,// 子空间透明度
child: const FlutterLogo(size: 100,),
),
),
floatingActionButton: FloatingActionButton(
tooltip: "Fade",
onPressed: (){
controller.forward();// 开始执行动画
},
child: const Icon(Icons.brush),
),
);
}
}
- 自定义state类 with TickerProviderStateMixin
- 重写 initState(),初始化AnimationController
- 调用controller.forwart()执行动画。
自定义Widget
class CustomButton extends StatelessWidget {
final String label;
const CustomButton(this.label, {super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text(label));
}
}
// 水平方向排列
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Row one"),
// 竖直方向排列
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Column One"),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('Row one'),
Text('Row two')
],
)
],
)
],
)
ListView
class _ListPagesState extends State<MyHomePage> {
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(
padding: const EdgeInsets.all(10),
child: Text("Row $i"),
));
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("listView"),
),
body: ListView(
children: _getListData(),
),
);
}
}
ListView.builder
少量数据用上面的ListView方式创建;当LisetView过多时,使用ListView.builder来构建。可以知道点击了哪个item。
class _ListPagesState extends State<MyHomePage> {
List<Widget> widgets = [];
List<Widget> _getListData() {
for (int i = 0; i < 100; i++) {
widgets.add(Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
));
}
return widgets;
}
@override
void initState() {
super.initState();
// 初始化item
_getListData();
}
Widget _getRow(int i) {
// 对于支持点击事件的Widget可以在外层包GestureDetector来通过onTap实现点击事件。
return GestureDetector(
onTap: () {
setState(() {
// 点击增加一个item
widgets.add(_getRow(widgets.length));
// 打印点击了哪个item
print("row $i");
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text("Row $i"),
),
);
}
@override
Widget build(BuildContext context) {
print("build...");
return Scaffold(
appBar: AppBar(
title: const Text("listView"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return _getRow(position);
},
),
);
}
}
绘制:CustomPaint、CutomPainter(支持自定义绘制算法)
绘制签名功能
class SignatureState extends State<MyHomePage> {
List<Offset?> _points = <Offset>[];
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(title: const Text("签名"),),
body: GestureDetector(
/// 当用户触摸屏幕并拖动时触发,接收一个details,包含触摸事件的详细信息。
onPanUpdate: (details) {
setState(() {
/// RenderBox对象表示组件在屏幕上几何形状和位置信息
RenderBox? referenceBox = context.findRenderObject() as RenderBox;
/// 将全局坐标转换为局部坐标,在SignaturePainter中绘制
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
/// 存储用户绘制点
_points = List.from(_points)..add(localPosition);
});
},
/// 用户松开手指时触发,将null添加到点列表中,表示绘制结束
onPanEnd: (details) {
_points.add(null);
},
/// 使用CustomPaint组件来绘制签名,传入自定义的Painter
/// 并设置组件的大小和无线大,占满整个屏幕。
child: CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite, // 无线大
),
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset?> points;
@override
void paint(Canvas canvas, Size size) {
// 创建paint对象,设置绘制签名所需要的样式和属性。
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5;
// 遍历点列表,当前点和下一个点都不会null,绘制从当前到下个点的线段。
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i]!, points[i + 1]!, paint);
}
}
}
// 判断是否需要重新绘制签名,新旧点数组比较
@override
bool shouldRepaint(SignaturePainter oldDelegate) {
return oldDelegate.points != points;
}
}