From 2014217eab46650d1e81e07063e4a5a7eefafc64 Mon Sep 17 00:00:00 2001 From: yl Date: Mon, 21 Aug 2023 11:33:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20chapter5=20fittedbox=E6=96=87?= =?UTF-8?q?=E5=AD=97=E4=BF=AE=E6=AD=A3=20&=20=E6=96=87=E6=A1=A3=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chapter5/fittedbox.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/chapter5/fittedbox.md b/src/chapter5/fittedbox.md index 54d1ad58..fca67db7 100644 --- a/src/chapter5/fittedbox.md +++ b/src/chapter5/fittedbox.md @@ -10,13 +10,14 @@ Padding( child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出 ) ``` -运行效果如图5-13所示: + +运行效果如图 5-13 所示: ![图5-13](../imgs/5-13.png) 可以看到右边溢出了 45 像素。 -上面只是一个例子,理论上我们经常会遇到子元素的大小超过他父容器的大小的情况,比如一张很大图片要在一个较小的空间显示,根据Flutter 的布局协议,父组件会将自身的最大显示空间作为约束传递给子组件,子组件应该遵守父组件的约束,如果子组件原始大小超过了父组件的约束区域,则需要进行一些缩小、裁剪或其他处理,而不同的组件的处理方式是特定的,比如 Text 组件,如果它的父组件宽度固定,高度不限的话,则默认情况下 Text 会在文本到达父组件宽度的时候换行。那如果我们想让 Text 文本在超过父组件的宽度时不要换行而是字体缩小呢?还有一种情况,比如父组件的宽高固定,而 Text 文本较少,这时候我们想让文本放大以填充整个父组件空间该怎么做呢? +上面只是一个例子,理论上我们经常会遇到子元素的大小超过他父容器的大小的情况,比如一张很大图片要在一个较小的空间显示,根据 Flutter 的布局协议,父组件会将自身的最大显示空间作为约束传递给子组件,子组件应该遵守父组件的约束,如果子组件原始大小超过了父组件的约束区域,则需要进行一些缩小、裁剪或其他处理,而不同的组件的处理方式是特定的,比如 Text 组件,如果它的父组件宽度固定,高度不限的话,则默认情况下 Text 会在文本到达父组件宽度的时候换行。那如果我们想让 Text 文本在超过父组件的宽度时不要换行而是字体缩小呢?还有一种情况,比如父组件的宽高固定,而 Text 文本较少,这时候我们想让文本放大以填充整个父组件空间该怎么做呢? 实际上,上面这两个问题的本质就是:子组件如何适配父组件空间。而根据 Flutter 布局协议适配算法应该在容器或布局组件的 layout 中实现,为了方便开发者自定义适配规则,Flutter 提供了一个 FittedBox 组件,定义如下: @@ -32,9 +33,9 @@ const FittedBox({ ### 适配原理 -1. FittedBox 在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即FittedBox 传递给子组件的约束为(0<=width<=double.infinity, 0<= height <=double.infinity)。 +1. FittedBox 在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即 FittedBox 传递给子组件的约束为(0<=width<=double.infinity, 0<= height <=double.infinity)。 2. FittedBox 对子组件布局结束后就可以获得子组件真实的大小。 -3. FittedBox 知道子组件的真实大小也知道他父组件的约束,那么FittedBox 就可以通过指定的适配方式(BoxFit 枚举中指定),让起子组件在 FittedBox 父组件的约束范围内按照指定的方式显示。 +3. FittedBox 知道子组件的真实大小也知道他父组件的约束,那么 FittedBox 就可以通过指定的适配方式(BoxFit 枚举中指定),让其子组件在 FittedBox 父组件的约束范围内按照指定的方式显示。 我们通过一个简单的例子说明: @@ -66,13 +67,13 @@ Widget wContainer(BoxFit boxFit) { } ``` -运行后效果如图5-14所示: +运行后效果如图 5-14 所示: ![图5-14](../imgs/5-14.png) -因为父Container要比子Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制,所以第一个蓝色区域会超出父组件的空间,因而看不到红色区域。第二个我们指定了适配方式为 BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分。 +因为父 Container 要比子 Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制,所以第一个蓝色区域会超出父组件的空间,因而看不到红色区域。第二个我们指定了适配方式为 BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分。 -要注意一点,在未指定适配方式时,虽然 FittedBox 子组件的大小超过了 FittedBox 父 Container 的空间,**但FittedBox 自身还是要遵守其父组件传递的约束**,所以最终 FittedBox 的本身的大小是 50×50,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父Container只占50×50的大小,接下来文本会紧挨着Container进行布局,而此时Container 中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了Container,但布局位置是正常的,所以就重叠了。如果我们不想让蓝色超出父组件布局范围,那么可以可以使用 ClipRect 对超出的部分剪裁掉即可: +要注意一点,在未指定适配方式时,虽然 FittedBox 子组件的大小超过了 FittedBox 父 Container 的空间,**但 FittedBox 自身还是要遵守其父组件传递的约束**,所以最终 FittedBox 的本身的大小是 50×50,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父 Container 只占 50×50 的大小,接下来文本会紧挨着 Container 进行布局,而此时 Container 中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了 Container,但布局位置是正常的,所以就重叠了。如果我们不想让蓝色超出父组件布局范围,那么可以可以使用 ClipRect 对超出的部分剪裁掉即可: ```dart ClipRect( // 将超出子组件布局范围的绘制内容剪裁掉 @@ -124,15 +125,16 @@ Widget wContainer(BoxFit boxFit) { return child; } ``` -运行后效果如图5-15所示: + +运行后效果如图 5-15 所示: ![图5-15](../imgs/5-15.png) -首先,因为我们给Row在主轴的对齐方式指定为`MainAxisAlignment.spaceEvenly`,这会将水平方向的剩余显示空间均分成多份穿插在每一个 child之间。 +首先,因为我们给 Row 在主轴的对齐方式指定为`MainAxisAlignment.spaceEvenly`,这会将水平方向的剩余显示空间均分成多份穿插在每一个 child 之间。 -可以看到,当数字为' 90000000000000000 '时,三个数字的长度加起来已经超出了测试设备的屏幕宽度,所以直接使用 Row 会溢出,当给 Row 添加上如果加上 FittedBox时,就可以按比例缩放至一行显示,实现了我们预期的效果。但是当数字没有那么大时,比如下面的 ' 800 ',直接使用 Row 是可以的,但加上 FittedBox 后三个数字虽然也能正常显示,但是它们却挤在了一起,这不符合我们的期望。之所以会这样,原因其实很简单:在指定主轴对齐方式为 spaceEvenly 的情况下,Row 在进行布局时会拿到父组件的约束,如果约束的 maxWidth 不是无限大,则 Row 会根据子组件的数量和它们的大小在主轴方向来根据 spaceEvenly 填充算法来分割水平方向的长度,最终Row 的宽度为 maxWidth;但如果 maxWidth 为无限大时,就无法在进行分割了,所以此时 Row 就会将子组件的宽度之和作为自己的宽度。 +可以看到,当数字为' 90000000000000000 '时,三个数字的长度加起来已经超出了测试设备的屏幕宽度,所以直接使用 Row 会溢出,当给 Row 添加上如果加上 FittedBox 时,就可以按比例缩放至一行显示,实现了我们预期的效果。但是当数字没有那么大时,比如下面的 ' 800 ',直接使用 Row 是可以的,但加上 FittedBox 后三个数字虽然也能正常显示,但是它们却挤在了一起,这不符合我们的期望。之所以会这样,原因其实很简单:在指定主轴对齐方式为 spaceEvenly 的情况下,Row 在进行布局时会拿到父组件的约束,如果约束的 maxWidth 不是无限大,则 Row 会根据子组件的数量和它们的大小在主轴方向来根据 spaceEvenly 填充算法来分割水平方向的长度,最终 Row 的宽度为 maxWidth;但如果 maxWidth 为无限大时,就无法在进行分割了,所以此时 Row 就会将子组件的宽度之和作为自己的宽度。 -回到示例中,当 Row 没有被 FittedBox 包裹时,此时父组件传给 Row 的约束的 maxWidth 为屏幕宽度,此时,Row 的宽度也就是屏幕宽度,而当被FittedBox 包裹时,FittedBox 传给 Row 的约束的 maxWidth 为无限大(double.infinity),因此Row 的最终宽度就是子组件的宽度之和。 +回到示例中,当 Row 没有被 FittedBox 包裹时,此时父组件传给 Row 的约束的 maxWidth 为屏幕宽度,此时,Row 的宽度也就是屏幕宽度,而当被 FittedBox 包裹时,FittedBox 传给 Row 的约束的 maxWidth 为无限大(double.infinity),因此 Row 的最终宽度就是子组件的宽度之和。 父组件传递给子组件的约束可以用我们上一章中封装的 LayoutLogPrint 来打印出来: @@ -148,7 +150,7 @@ flutter: 1: BoxConstraints(0.0<=w<=396.0, 0.0<=h<=Infinity) flutter: 2: BoxConstraints(unconstrained) ``` -问题原因找到了,那解决的思路就很简单了,我们只需要让FittedBox 子元素接收到的约束的 maxWidth 为屏幕宽度即可,为此我们封装了一个 SingleLineFittedBox 来替换 FittedBox 以达到我们预期的效果,实现如下: +问题原因找到了,那解决的思路就很简单了,我们只需要让 FittedBox 子元素接收到的约束的 maxWidth 为屏幕宽度即可,为此我们封装了一个 SingleLineFittedBox 来替换 FittedBox 以达到我们预期的效果,实现如下: ```dart class SingleLineFittedBox extends StatelessWidget { @@ -183,11 +185,11 @@ wRow(' 800 '), SingleLineFittedBox(child: wRow(' 800 ')), ``` -运行后效果如图5-16所示: +运行后效果如图 5-16 所示: ![图5-16](../imgs/5-16.png) -返现 800 正常显示了,但用SingleLineFittedBox包裹的 ' 90000000000000000 ' 的那个 Row 却溢出了!溢出的原因其实也很简单,因为我们在 SingleLineFittedBox 中将传给 Row 的 maxWidth 置为屏幕宽度后,效果和不加SingleLineFittedBox 的效果是一样的,Row 收到父组件约束的 maxWidth 都是屏幕的宽度,所以搞了半天实现了个寂寞。但是,不要放弃,其实离胜利只有一步,只要我们稍加修改,就能实现我们的预期,话不多说,直接上代码: +返现 800 正常显示了,但用 SingleLineFittedBox 包裹的 ' 90000000000000000 ' 的那个 Row 却溢出了!溢出的原因其实也很简单,因为我们在 SingleLineFittedBox 中将传给 Row 的 maxWidth 置为屏幕宽度后,效果和不加 SingleLineFittedBox 的效果是一样的,Row 收到父组件约束的 maxWidth 都是屏幕的宽度,所以搞了半天实现了个寂寞。但是,不要放弃,其实离胜利只有一步,只要我们稍加修改,就能实现我们的预期,话不多说,直接上代码: ```dart class SingleLineFittedBox extends StatelessWidget { @@ -214,10 +216,10 @@ class SingleLineFittedBox extends StatelessWidget { } ``` -代码很简单,我们将最小宽度(minWidth)约束指定为屏幕宽度,因为Row必须得遵守父组件的约束,所以 Row 的宽度至少等于屏幕宽度,所以就不会出现缩在一起的情况;同时我们将 maxWidth 指定为无限大,则就可以处理数字总长度超出屏幕宽度的情况。 +代码很简单,我们将最小宽度(minWidth)约束指定为屏幕宽度,因为 Row 必须得遵守父组件的约束,所以 Row 的宽度至少等于屏幕宽度,所以就不会出现缩在一起的情况;同时我们将 maxWidth 指定为无限大,则就可以处理数字总长度超出屏幕宽度的情况。 -重新运行后如图5-17所示: +重新运行后如图 5-17 所示: ![图5-17](../imgs/5-17.png) -发现无论长数字还是短数字,我们的SingleLineFittedBox 都可以正常工作,大功告成!我们的组件库里面又多了一个组件 。 +发现无论长数字还是短数字,我们的 SingleLineFittedBox 都可以正常工作,大功告成!我们的组件库里面又多了一个组件 。 From 44a21555e34acb04480a31568e4b0325a28d0cc4 Mon Sep 17 00:00:00 2001 From: yl Date: Wed, 23 Aug 2023 11:52:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E9=94=99=E8=AF=AF=E5=AD=97?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3:=20=E5=9C=A8=E7=94=A8=E6=88=B7=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E4=B8=80=E4=B8=AA=E6=96=87=E4=BB=B6=E5=A4=B9=E6=98=AF?= =?UTF-8?q?=20=E6=94=B9=E4=B8=BA=20=E5=9C=A8=E7=94=A8=E6=88=B7=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E4=B8=80=E4=B8=AA=E6=96=87=E4=BB=B6=E5=A4=B9=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chapter7/dailog.md | 80 ++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/src/chapter7/dailog.md b/src/chapter7/dailog.md index ac4e0edf..115823d1 100644 --- a/src/chapter7/dailog.md +++ b/src/chapter7/dailog.md @@ -1,14 +1,14 @@ # 7.7 对话框详解 -本节将详细介绍一下Flutter中对话框的使用方式、实现原理、样式定制及状态管理。 +本节将详细介绍一下 Flutter 中对话框的使用方式、实现原理、样式定制及状态管理。 ## 7.7.1 使用对话框 -对话框本质上也是UI布局,通常一个对话框会包含标题、内容,以及一些操作按钮,为此,Material库中提供了一些现成的对话框组件来用于快速的构建出一个完整的对话框。 +对话框本质上也是 UI 布局,通常一个对话框会包含标题、内容,以及一些操作按钮,为此,Material 库中提供了一些现成的对话框组件来用于快速的构建出一个完整的对话框。 ### 1. AlertDialog -下面我们主要介绍一下Material库中的`AlertDialog`组件,它的构造函数定义如下: +下面我们主要介绍一下 Material 库中的`AlertDialog`组件,它的构造函数定义如下: ```dart const AlertDialog({ @@ -27,7 +27,7 @@ const AlertDialog({ }) ``` -参数都比较简单,不在赘述。下面我们看一个例子,假如我们要在删除文件时弹出一个确认对话框,该对话框如图7-11所示: +参数都比较简单,不在赘述。下面我们看一个例子,假如我们要在删除文件时弹出一个确认对话框,该对话框如图 7-11 所示: ![图7-11](../imgs/7-11.png) @@ -55,7 +55,7 @@ AlertDialog( 实现代码很简单,不在赘述。唯一需要注意的是我们是通过`Navigator.of(context).pop(…)`方法来关闭对话框的,这和路由返回的方式是一致的,并且都可以返回一个结果数据。现在,对话框我们已经构建好了,那么如何将它弹出来呢?还有对话框返回的数据应如何被接收呢?这些问题的答案都在`showDialog()`方法中。 -`showDialog()`是Material组件库提供的一个用于弹出Material风格对话框的方法,签名如下: +`showDialog()`是 Material 组件库提供的一个用于弹出 Material 风格对话框的方法,签名如下: ```dart Future showDialog({ @@ -65,7 +65,7 @@ Future showDialog({ }) ``` -该方法只有两个参数,含义见注释。该方法返回一个`Future`,它正是用于接收对话框的返回值:如果我们是通过点击对话框遮罩关闭的,则`Future`的值为`null`,否则为我们通过`Navigator.of(context).pop(result)`返回的result值,下面我们看一下整个示例: +该方法只有两个参数,含义见注释。该方法返回一个`Future`,它正是用于接收对话框的返回值:如果我们是通过点击对话框遮罩关闭的,则`Future`的值为`null`,否则为我们通过`Navigator.of(context).pop(result)`返回的 result 值,下面我们看一下整个示例: ```dart //点击该按钮后弹出对话框 @@ -116,7 +116,7 @@ Future showDeleteConfirmDialog1() { ### 2. SimpleDialog -`SimpleDialog`也是Material组件库提供的对话框,它会展示一个列表,用于列表选择的场景。下面是一个选择APP语言的示例,运行结果如图7-12。 +`SimpleDialog`也是 Material 组件库提供的对话框,它会展示一个列表,用于列表选择的场景。下面是一个选择 APP 语言的示例,运行结果如图 7-12。 ![图7-12](../imgs/7-12.png) @@ -160,7 +160,7 @@ Future changeLanguage() async { } ``` -列表项组件我们使用了`SimpleDialogOption`组件来包装了一下,它相当于一个TextButton,只不过按钮文案是左对齐的,并且padding较小。上面示例运行后,用户选择一种语言后,控制台就会打印出它。 +列表项组件我们使用了`SimpleDialogOption`组件来包装了一下,它相当于一个 TextButton,只不过按钮文案是左对齐的,并且 padding 较小。上面示例运行后,用户选择一种语言后,控制台就会打印出它。 ### 3. Dialog @@ -184,7 +184,7 @@ Dialog( ); ``` -下面我们看一个弹出一个有30个列表项的对话框示例,运行效果如图7-12所示: +下面我们看一个弹出一个有 30 个列表项的对话框示例,运行效果如图 7-12 所示: ![图7-13](../imgs/7-13.png) @@ -224,7 +224,7 @@ Future showListDialog() async { 现在,我们己经介绍完了`AlertDialog`、`SimpleDialog`以及`Dialog`。上面的示例中,我们在调用`showDialog`时,在`builder`中都是构建了这三个对话框组件的一种,可能有些读者会惯性的以为在`builder`中只能返回这三者之一,其实这不是必须的!就拿`Dialog`的示例来举例,我们完全可以用下面的代码来替代`Dialog`: ```dart -// return Dialog(child: child) +// return Dialog(child: child) return UnconstrainedBox( constrainedAxis: Axis.vertical, child: ConstrainedBox( @@ -237,7 +237,7 @@ return UnconstrainedBox( ); ``` -上面代码运行后可以实现一样的效果。现在我们总结一下:`AlertDialog`、`SimpleDialog`以及`Dialog`是Material组件库提供的三种对话框,旨在帮助开发者快速构建出符合Material设计规范的对话框,但读者完全可以自定义对话框样式,因此,我们仍然可以实现各种样式的对话框,这样即带来了易用性,又有很强的扩展性。 +上面代码运行后可以实现一样的效果。现在我们总结一下:`AlertDialog`、`SimpleDialog`以及`Dialog`是 Material 组件库提供的三种对话框,旨在帮助开发者快速构建出符合 Material 设计规范的对话框,但读者完全可以自定义对话框样式,因此,我们仍然可以实现各种样式的对话框,这样即带来了易用性,又有很强的扩展性。 ## 7.7.2 对话框打开动画及遮罩 @@ -245,7 +245,7 @@ return UnconstrainedBox( > 关于动画相关内容我们将在本书第九章中详细介绍,下面内容读者可以先了解一下(不必深究),读者可以在学习完动画相关内容后再回头来看。 -我们已经介绍过了`showDialog`方法,它是Material组件库中提供的一个打开Material风格对话框的方法。那如何打开一个普通风格的对话框呢(非Material风格)? Flutter 提供了一个`showGeneralDialog`方法,签名如下: +我们已经介绍过了`showDialog`方法,它是 Material 组件库中提供的一个打开 Material 风格对话框的方法。那如何打开一个普通风格的对话框呢(非 Material 风格)? Flutter 提供了一个`showGeneralDialog`方法,签名如下: ```dart Future showGeneralDialog({ @@ -260,7 +260,7 @@ Future showGeneralDialog({ }) ``` -实际上,`showDialog`方法正是`showGeneralDialog`的一个封装,定制了Material风格对话框的遮罩颜色和动画。Material风格对话框打开/关闭动画是一个Fade(渐隐渐显)动画,如果我们想使用一个缩放动画就可以通过`transitionBuilder`来自定义。下面我们自己封装一个`showCustomDialog`方法,它定制的对话框动画为缩放动画,并同时制定遮罩颜色为`Colors.black87`: +实际上,`showDialog`方法正是`showGeneralDialog`的一个封装,定制了 Material 风格对话框的遮罩颜色和动画。Material 风格对话框打开/关闭动画是一个 Fade(渐隐渐显)动画,如果我们想使用一个缩放动画就可以通过`transitionBuilder`来自定义。下面我们自己封装一个`showCustomDialog`方法,它定制的对话框动画为缩放动画,并同时制定遮罩颜色为`Colors.black87`: ```dart Future showCustomDialog({ @@ -335,7 +335,7 @@ showCustomDialog( ); ``` -运行效果如图7-14所示: +运行效果如图 7-14 所示: ![图7-14](../imgs/7-14.png) @@ -373,13 +373,11 @@ Future showGeneralDialog({ ## 7.7.4 对话框状态管理 -我们在用户选择删除一个文件时,会询问是否删除此文件;在用户选择一个文件夹是,应该再让用户确认是否删除子文件夹。为了在用户选择了文件夹时避免二次弹窗确认是否删除子目录,我们在确认对话框底部添加一个“同时删除子目录?”的复选框,如图7-15所示: +我们在用户选择删除一个文件时,会询问是否删除此文件;在用户选择一个文件夹时,应该再让用户确认是否删除子文件夹。为了在用户选择了文件夹时避免二次弹窗确认是否删除子目录,我们在确认对话框底部添加一个“同时删除子目录?”的复选框,如图 7-15 所示: ![图7-15](../imgs/7-15.png) - - -现在就有一个问题:如何管理复选框的选中状态?习惯上,我们会在路由页的State中来管理选中状态,我们可能会写出如下这样的代码: +现在就有一个问题:如何管理复选框的选中状态?习惯上,我们会在路由页的 State 中来管理选中状态,我们可能会写出如下这样的代码: ```dart class _DialogRouteState extends State { @@ -453,11 +451,11 @@ class _DialogRouteState extends State { } ``` -然后,当我们运行上面的代码时我们会发现复选框根本选不中!为什么会这样呢?其实原因很简单,我们知道`setState`方法只会针对当前context的子树重新build,但是我们的对话框并不是在`_DialogRouteState`的`build` 方法中构建的,而是通过`showDialog`单独构建的,所以在`_DialogRouteState`的context中调用`setState`是无法影响通过`showDialog`构建的UI的。另外,我们可以从另外一个角度来理解这个现象,前面说过对话框也是通过路由的方式来实现的,那么上面的代码实际上就等同于企图在父路由中调用`setState`来让子路由更新,这显然是不行的!简尔言之,根本原因就是context不对。那如何让复选框可点击呢?通常有如下三种方法: +然后,当我们运行上面的代码时我们会发现复选框根本选不中!为什么会这样呢?其实原因很简单,我们知道`setState`方法只会针对当前 context 的子树重新 build,但是我们的对话框并不是在`_DialogRouteState`的`build` 方法中构建的,而是通过`showDialog`单独构建的,所以在`_DialogRouteState`的 context 中调用`setState`是无法影响通过`showDialog`构建的 UI 的。另外,我们可以从另外一个角度来理解这个现象,前面说过对话框也是通过路由的方式来实现的,那么上面的代码实际上就等同于企图在父路由中调用`setState`来让子路由更新,这显然是不行的!简尔言之,根本原因就是 context 不对。那如何让复选框可点击呢?通常有如下三种方法: -### 1. 单独抽离出StatefulWidget +### 1. 单独抽离出 StatefulWidget -既然是context不对,那么直接的思路就是将复选框的选中逻辑单独封装成一个`StatefulWidget`,然后在其内部管理复选状态。我们先来看看这种方法,下面是实现代码: +既然是 context 不对,那么直接的思路就是将复选框的选中逻辑单独封装成一个`StatefulWidget`,然后在其内部管理复选状态。我们先来看看这种方法,下面是实现代码: ```dart // 单独封装一个内部管理选中状态的复选框组件 @@ -566,15 +564,15 @@ ElevatedButton( ), ``` -运行后效果如图7-16所示: +运行后效果如图 7-16 所示: ![图7-16](../imgs/7-16.png) 可见复选框能选中了,点击“取消”或“删除”后,控制台就会打印出最终的确认状态。 -### 2. 使用StatefulBuilder方法 +### 2. 使用 StatefulBuilder 方法 -上面的方法虽然能解决对话框状态更新的问题,但是有一个明显的缺点——对话框上所有可能会改变状态的组件都得单独封装在一个在内部管理状态的`StatefulWidget`中,这样不仅麻烦,而且复用性不大。因此,我们来想想能不能找到一种更简单的方法?上面的方法本质上就是将对话框的状态置于一个`StatefulWidget`的上下文中,由`StatefulWidget`在内部管理,那么我们有没有办法在不需要单独抽离组件的情况下创建一个`StatefulWidget`的上下文呢?想到这里,我们可以从`Builder`组件的实现获得灵感。在前面介绍过`Builder`组件可以获得组件所在位置的真正的Context,那它是怎么实现的呢,我们看看它的源码: +上面的方法虽然能解决对话框状态更新的问题,但是有一个明显的缺点——对话框上所有可能会改变状态的组件都得单独封装在一个在内部管理状态的`StatefulWidget`中,这样不仅麻烦,而且复用性不大。因此,我们来想想能不能找到一种更简单的方法?上面的方法本质上就是将对话框的状态置于一个`StatefulWidget`的上下文中,由`StatefulWidget`在内部管理,那么我们有没有办法在不需要单独抽离组件的情况下创建一个`StatefulWidget`的上下文呢?想到这里,我们可以从`Builder`组件的实现获得灵感。在前面介绍过`Builder`组件可以获得组件所在位置的真正的 Context,那它是怎么实现的呢,我们看看它的源码: ```dart class Builder extends StatelessWidget { @@ -590,7 +588,7 @@ class Builder extends StatelessWidget { } ``` -可以看到,`Builder`实际上只是继承了`StatelessWidget`,然后在`build`方法中获取当前context后将构建方法代理到了`builder`回调,可见,`Builder`实际上是获取了`StatelessWidget` 的上下文(context)。那么我们能否用相同的方法获取`StatefulWidget` 的上下文,并代理其`build`方法呢?下面我们照猫画虎,来封装一个`StatefulBuilder`方法: +可以看到,`Builder`实际上只是继承了`StatelessWidget`,然后在`build`方法中获取当前 context 后将构建方法代理到了`builder`回调,可见,`Builder`实际上是获取了`StatelessWidget` 的上下文(context)。那么我们能否用相同的方法获取`StatefulWidget` 的上下文,并代理其`build`方法呢?下面我们照猫画虎,来封装一个`StatefulBuilder`方法: ```dart class StatefulBuilder extends StatefulWidget { @@ -639,11 +637,11 @@ Row( ), ``` -实际上,这种方法本质上就是子组件通知父组件(StatefulWidget)重新build子组件本身来实现UI更新的,读者可以对比代码理解。实际上`StatefulBuilder`正是Flutter SDK中提供的一个类,它和`Builder`的原理是一样的,在此,提醒读者一定要将`StatefulBuilder`和`Builder`理解透彻,因为它们在Flutter中是非常实用的。 +实际上,这种方法本质上就是子组件通知父组件(StatefulWidget)重新 build 子组件本身来实现 UI 更新的,读者可以对比代码理解。实际上`StatefulBuilder`正是 Flutter SDK 中提供的一个类,它和`Builder`的原理是一样的,在此,提醒读者一定要将`StatefulBuilder`和`Builder`理解透彻,因为它们在 Flutter 中是非常实用的。 ### 3. 精妙的解法 -是否还有更简单的解决方案呢?要确认这个问题,我们就得先搞清楚UI是怎么更新的,我们知道在调用`setState`方法后`StatefulWidget`就会重新build,那`setState`方法做了什么呢?我们能不能从中找到方法?顺着这个思路,我们就得看一下`setState`的核心源码: +是否还有更简单的解决方案呢?要确认这个问题,我们就得先搞清楚 UI 是怎么更新的,我们知道在调用`setState`方法后`StatefulWidget`就会重新 build,那`setState`方法做了什么呢?我们能不能从中找到方法?顺着这个思路,我们就得看一下`setState`的核心源码: ```dart void setState(VoidCallback fn) { @@ -652,7 +650,7 @@ void setState(VoidCallback fn) { } ``` -可以发现,`setState`中调用了`Element`的`markNeedsBuild()`方法,我们前面说过,Flutter是一个响应式框架,要更新UI只需改变状态后通知框架页面需要重构即可,而`Element`的`markNeedsBuild()`方法正是来实现这个功能的!`markNeedsBuild()`方法会将当前的`Element`对象标记为“dirty”(脏的),在每一个Frame,Flutter都会重新构建被标记为“dirty”`Element`对象。既然如此,我们有没有办法获取到对话框内部UI的`Element`对象,然后将其标示为为“dirty”呢?答案是肯定的!我们可以通过Context来得到`Element`对象,至于`Element`与`Context`的关系我们将会在后面“Flutter核心原理”一章中再深入介绍,现在只需要简单的认为:在组件树中,`context`实际上就是`Element`对象的引用。知道这个后,那么解决的方案就呼之欲出了,我们可以通过如下方式来让复选框可以更新: +可以发现,`setState`中调用了`Element`的`markNeedsBuild()`方法,我们前面说过,Flutter 是一个响应式框架,要更新 UI 只需改变状态后通知框架页面需要重构即可,而`Element`的`markNeedsBuild()`方法正是来实现这个功能的!`markNeedsBuild()`方法会将当前的`Element`对象标记为“dirty”(脏的),在每一个 Frame,Flutter 都会重新构建被标记为“dirty”`Element`对象。既然如此,我们有没有办法获取到对话框内部 UI 的`Element`对象,然后将其标示为为“dirty”呢?答案是肯定的!我们可以通过 Context 来得到`Element`对象,至于`Element`与`Context`的关系我们将会在后面“Flutter 核心原理”一章中再深入介绍,现在只需要简单的认为:在组件树中,`context`实际上就是`Element`对象的引用。知道这个后,那么解决的方案就呼之欲出了,我们可以通过如下方式来让复选框可以更新: ```dart Future showDeleteConfirmDialog4() { @@ -673,7 +671,7 @@ Future showDeleteConfirmDialog4() { Checkbox( // 依然使用Checkbox组件 value: _withTree, onChanged: (bool value) { - // 此时context为对话框UI的根Element,我们 + // 此时context为对话框UI的根Element,我们 // 直接将对话框UI对应的Element标记为dirty (context as Element).markNeedsBuild(); _withTree = !_withTree; @@ -702,7 +700,7 @@ Future showDeleteConfirmDialog4() { } ``` -上面的代码运行后复选框也可以正常选中。可以看到,我们只用了一行代码便解决了这个问题!当然上面的代码并不是最优,因为我们只需要更新复选框的状态,而此时的`context`我们用的是对话框的根`context`,所以会导致整个对话框UI组件全部rebuild,因此最好的做法是将`context`的“范围”缩小,也就是说只将`Checkbox`的Element标记为`dirty`,优化后的代码为: +上面的代码运行后复选框也可以正常选中。可以看到,我们只用了一行代码便解决了这个问题!当然上面的代码并不是最优,因为我们只需要更新复选框的状态,而此时的`context`我们用的是对话框的根`context`,所以会导致整个对话框 UI 组件全部 rebuild,因此最好的做法是将`context`的“范围”缩小,也就是说只将`Checkbox`的 Element 标记为`dirty`,优化后的代码为: ```dart ... //省略无关代码 @@ -730,7 +728,7 @@ Row( ### 1. 底部菜单列表 -`showModalBottomSheet`方法可以弹出一个Material风格的底部菜单列表模态对话框,示例如下: +`showModalBottomSheet`方法可以弹出一个 Material 风格的底部菜单列表模态对话框,示例如下: ```dart // 弹出底部菜单列表模态对话框 @@ -764,15 +762,13 @@ ElevatedButton( ), ``` -运行后效果如图7-17所示: +运行后效果如图 7-17 所示: ![图7-17](../imgs/7-17.png) +### 2. Loading 框 - -### 2. Loading框 - -其实Loading框可以直接通过`showDialog`+`AlertDialog`来自定义: +其实 Loading 框可以直接通过`showDialog`+`AlertDialog`来自定义: ```dart showLoadingDialog() { @@ -797,11 +793,11 @@ showLoadingDialog() { } ``` -显示效果如图7-18所示: +显示效果如图 7-18 所示: ![图7-18](../imgs/7-18.png) -如果我们嫌Loading框太宽,想自定义对话框宽度,这时只使用`SizedBox`或`ConstrainedBox`是不行的,原因是`showDialog`中已经给对话框设置了最小宽度约束,根据我们在第五章“尺寸限制类容器”一节中所述,我们可以使用`UnconstrainedBox`先抵消`showDialog`对宽度的约束,然后再使用`SizedBox`指定宽度,代码如下: +如果我们嫌 Loading 框太宽,想自定义对话框宽度,这时只使用`SizedBox`或`ConstrainedBox`是不行的,原因是`showDialog`中已经给对话框设置了最小宽度约束,根据我们在第五章“尺寸限制类容器”一节中所述,我们可以使用`UnconstrainedBox`先抵消`showDialog`对宽度的约束,然后再使用`SizedBox`指定宽度,代码如下: ```dart ... //省略无关代码 @@ -825,13 +821,13 @@ UnconstrainedBox( ); ``` -代码运行后,效果如图7-19所示: +代码运行后,效果如图 7-19 所示: ![图7-19](../imgs/7-19.png) ### 3. 日历选择器 -我们先看一下Material风格的日历选择器,如图7-20所示: +我们先看一下 Material 风格的日历选择器,如图 7-20 所示: ![图7-20](../imgs/7-20.png) @@ -851,7 +847,7 @@ Future _showDatePicker1() { } ``` -iOS风格的日历选择器需要使用`showCupertinoModalPopup`方法和`CupertinoDatePicker`组件来实现: +iOS 风格的日历选择器需要使用`showCupertinoModalPopup`方法和`CupertinoDatePicker`组件来实现: ```dart Future _showDatePicker2() { @@ -878,6 +874,6 @@ Future _showDatePicker2() { } ``` -运行效果如图7-21所示: +运行效果如图 7-21 所示: ![图7-21](../imgs/7-21.png)