From a281a1b496fe11ba55ee366e7a58da2e0367c2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sat, 24 Aug 2024 00:45:10 +0800 Subject: [PATCH 01/12] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/cookbook/effects/download-button.md=20#1171?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cookbook/effects/download-button.md | 107 +++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/src/content/cookbook/effects/download-button.md b/src/content/cookbook/effects/download-button.md index a28ed681c3..f56fd4609f 100644 --- a/src/content/cookbook/effects/download-button.md +++ b/src/content/cookbook/effects/download-button.md @@ -1,6 +1,5 @@ --- -# title: Create a download button -title: 创建一个带进度条的下载按钮 +title: Create a download button description: How to implement a download button. js: - defer: true @@ -19,18 +18,36 @@ to provide this feedback. In this recipe, you'll build a download button that transitions through multiple visual states, based on the status of an app download. +应用程序中充满了执行长时间操作的按钮。 +例如,一个按钮可能会触发下载, +启动下载过程,并在一段时间内接收数据, +然后提供对下载资源的访问。 +向用户显示长时间运行过程的进度是很有帮助的, +而按钮本身是提供此反馈的好地方。 +在这个示例中,你将构建一个下载按钮, +根据应用程序下载的状态在多个视觉状态之间切换。 + The following animation shows the app's behavior: +下面的动画展示了应用程序的行为: + ![The download button cycles through its stages](/assets/images/docs/cookbook/effects/DownloadButton.gif){:.site-mobile-screenshot} ## Define a new stateless widget +## 定义一个新的无状态小部件 + Your button widget needs to change its appearance over time. Therefore, you need to implement your button with a custom stateless widget. +你的按钮组件需要随着时间的推移改变其外观。 +因此,你需要使用自定义的无状态组件来实现你的按钮。 + Define a new stateless widget called `DownloadButton`. +定义一个名为 `DownloadButton` 的新无状态组件。 + ```dart @immutable @@ -49,12 +66,19 @@ class DownloadButton extends StatelessWidget { ## Define the button's possible visual states +## 定义按钮的可能的视觉状态 + The download button's visual presentation is based on a given download status. Define the possible states of the download, and then update `DownloadButton` to accept a `DownloadStatus` and a `Duration` for how long the button should take to animate from one status to another. +下载按钮的视觉呈现基于给定的下载状态。 +首先定义下载可能的状态,然后更新 `DownloadButton` +以接受一个 `DownloadStatus` 和一个 `Duration`, +用于指定按钮从一个状态动画过渡到另一个状态所需的时间。 + ```dart enum DownloadStatus { @@ -104,19 +128,42 @@ easier testing, and easier changes to application behavior in the future. ::: +:::note +每次定义自定义组件时, +你都必须决定是将所有相关信息从其父组件传递给该组件, +还是让该组件在其内部协调应用程序行为。 +例如,`DownloadButton` 可以从其父组件接收当前的 `DownloadStatus`, +也可以在其 `State` 对象内部管理下载过程。 +对于大多数组件来说,最佳做法是将相关信息从父组件传递到组件内部, +而不是在组件内部管理行为。 +通过传递所有相关信息, +你可以确保组件具有更高的可重用性、更容易进行测试, +以及在未来更容易更改应用程序行为。 +::: + ## Display the button shape +## 显示按钮形状 + The download button changes its shape based on the download status. The button displays a grey, rounded rectangle during the `notDownloaded` and `downloaded` states. The button displays a transparent circle during the `fetchingDownload` and `downloading` states. +下载按钮会根据下载状态改变其形状。 +在 `notDownloaded` 和 `downloaded` 状态下,按钮显示为灰色的圆角矩形。 +而在 `fetchingDownload` 和 `downloading` 状态下,按钮显示为透明的圆形。 + Based on the current `DownloadStatus`, build an `AnimatedContainer` with a `ShapeDecoration` that displays a rounded rectangle or a circle. +根据当前的 `DownloadStatus`, +构建一个带有 `ShapeDecoration` 的 `AnimatedContainer`, +该装饰可以显示圆角矩形或圆形。 + Consider defining the shape's widget tree in a separated `Stateless` widget so that the main `build()` method remains simple, allowing for the additions @@ -126,8 +173,13 @@ like `Widget _buildSomething() {}`, always prefer creating a considerations on this can be found in the [documentation]({{site.api}}/flutter/widgets/StatelessWidget-class.html) or in a dedicated video in the Flutter [YouTube channel]({{site.yt.watch}}?v=IOyq-eTRhvo). +建议将形状的组件树定义在一个单独的 `Stateless` 组件中,这样主 `build()` 方法可以保持简洁,便于后续添加功能。与其创建一个返回组件的函数(例如 `Widget _buildSomething() {}`),更好的做法是创建一个 `StatelessWidget` 或 `StatefulWidget`,这在性能上更优。有关更多的考虑因素,你可以在 [文档]({{site.api}}/flutter/widgets/StatelessWidget-class.html) 中找到,或者在 Flutter 的 [YouTube 频道]({{site.yt.watch}}?v=IOyq-eTRhvo) 上观看相关视频。 + For now, the `AnimatedContainer` child is just a `SizedBox` because we will come back at it in another step. +目前,`AnimatedContainer` 的子组件只是一个 `SizedBox`, +因为我们将在后续步骤中再来处理它。 + ```dart @immutable @@ -213,17 +265,27 @@ ending shape of a circle. But, you don't want the final circle to be visible, so you make it transparent, which causes an animated fade-out. +你可能会疑惑,为什么需要为一个透明的圆形使用 `ShapeDecoration` 组件,毕竟它是不可见的。这个透明圆形的目的是为了协调动画效果。`AnimatedContainer` 一开始显示为圆角矩形。当 `DownloadStatus` 变为 `fetchingDownload` 时,`AnimatedContainer` 需要从圆角矩形动画过渡到圆形,并在动画进行的过程中逐渐淡出。实现这种动画的唯一方法是定义圆角矩形的起始形状和圆形的结束形状。但是,你不希望最终的圆形可见,所以将其设置为透明,这样就能实现动画淡出的效果。 + ## Display the button text +## 显示按钮文本 + The `DownloadButton` displays `GET` during the `notDownloaded` phase, `OPEN` during the `downloaded` phase, and no text in between. +`DownloadButton` 在 `notDownloaded` 阶段显示 `GET`, +在 `downloaded` 阶段显示 `OPEN`,在中间阶段没有文本。 + Add widgets to display text during each download phase, and animate the text's opacity in between. Add the text widget tree as a child of the `AnimatedContainer` in the button wrapper widget. +添加用于显示每个下载阶段文本的组件,并在阶段之间动画化文本的透明度。 +将文本组件树作为 `AnimatedContainer` 的子组件添加到按钮包装组件中。 + ```dart @immutable @@ -283,17 +345,27 @@ class ButtonShapeWidget extends StatelessWidget { ## Display a spinner while fetching download +## 下载时显示一个进度条 + During the `fetchingDownload` phase, the `DownloadButton` displays a radial spinner. This spinner fades in from the `notDownloaded` phase and fades out to the `fetchingDownload` phase. +在 `fetchingDownload` 阶段,`DownloadButton` 会显示一个径向进度条。 +这个进度条从 `notDownloaded` 阶段淡入,到 `fetchingDownload` 阶段淡出。 + Implement a radial spinner that sits on top of the button shape and fades in and out at the appropriate times. +实现一个位于按钮形状之上的径向进度条,并在适当的时间淡入和淡出。 + We have removed the `ButtonShapeWidget`'s constructor to keep the focus on its build method and the `Stack` widget we've added. +我们删除了 `ButtonShapeWidget` 的构造函数, +以便集中精力关注其 build 方法和添加的 `Stack` 组件。 + ```dart @override @@ -328,19 +400,30 @@ Widget build(BuildContext context) { ## Display the progress and a stop button while downloading +## 下载时显示进度和停止按钮 + After the `fetchingDownload` phase is the `downloading` phase. During the `downloading` phase, the `DownloadButton` replaces the radial progress spinner with a growing radial progress bar. The `DownloadButton` also displays a stop button icon so that the user can cancel an in-progress download. +在 `fetchingDownload` 阶段之后,是 `downloading` 阶段。 +在 `downloading` 阶段,`DownloadButton` 会用增长型的径向进度条替换径向进度条。 +`DownloadButton` 还会显示一个停止按钮图标,以便用户可以取消正在进行的下载。 + Add a progress property to the `DownloadButton` widget, and then update the progress display to switch to a radial progress bar during the `downloading` phase. +将一个进度属性添加到 `DownloadButton` 组件中, +然后在 `downloading` 阶段切换到径向进度条。 + Next, add a stop button icon at the center of the radial progress bar. +接下来,在径向进度条的中心添加一个停止按钮图标。 + ```dart @override @@ -386,16 +469,24 @@ Widget build(BuildContext context) { ## Add button tap callbacks +## 添加按钮点击回调 + The last detail that your `DownloadButton` needs is the button behavior. The button must do things when the user taps it. +`DownloadButton` 的最后一个细节是按钮行为。当用户点击按钮时,该按钮必须做某些事情。 + Add widget properties for callbacks to start a download, cancel a download, and open a download. +添加回调属性,以开始下载、取消下载和打开下载。 + Finally, wrap `DownloadButton`'s existing widget tree with a `GestureDetector` widget, and forward the tap event to the corresponding callback property. +最后,用 `GestureDetector` 包装 `DownloadButton` 的现有组件树,并将点击事件转发到相应的回调属性。 + ```dart @immutable @@ -457,10 +548,18 @@ fetching download, downloading, and downloaded. Now, the user can tap to start a download, tap to cancel an in-progress download, and tap to open a completed download. +恭喜!您已经有一个按钮, +该按钮根据按钮所处的阶段而改变其显示:未下载、获取下载、下载中和已下载。 +现在,用户可以点击以启动下载、点击以取消正在进行的下载,并点击以打开已完成的下载。 + ## Interactive example +## 互动示例 + Run the app: +运行应用程序: + * Click the **GET** button to kick off a simulated download. * The button changes to a progress indicator @@ -470,6 +569,10 @@ Run the app: that the app is ready for the user to open the downloaded asset. +* 点击 **GET** 按钮以启动模拟下载。 +* 该按钮更改为进度指示器以模拟正在进行的下载。 +* 当模拟下载完成时,按钮将过渡到 **OPEN** ,表示应用已准备好让用户打开下载的资源。 + From 335b1c91ec2a155cbdd2d42395bbf38b2afc1040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sat, 24 Aug 2024 10:16:03 +0800 Subject: [PATCH 02/12] =?UTF-8?q?[=E5=AE=8C=E5=96=84=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/cookbook/effects/download-button.md=20#1171?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cookbook/effects/download-button.md | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/content/cookbook/effects/download-button.md b/src/content/cookbook/effects/download-button.md index f56fd4609f..2e4067b071 100644 --- a/src/content/cookbook/effects/download-button.md +++ b/src/content/cookbook/effects/download-button.md @@ -1,6 +1,8 @@ --- title: Create a download button +标题:创建一个下载按钮 description: How to implement a download button. +描述:如何实现下载按钮 js: - defer: true url: /assets/js/inject_dartpad.js @@ -35,18 +37,18 @@ The following animation shows the app's behavior: ## Define a new stateless widget -## 定义一个新的无状态小部件 +## 定义一个新的 stateless widget Your button widget needs to change its appearance over time. Therefore, you need to implement your button with a custom stateless widget. -你的按钮组件需要随着时间的推移改变其外观。 -因此,你需要使用自定义的无状态组件来实现你的按钮。 +你的button widget需要随着时间的推移改变其外观。 +因此,你需要使用 custom stateless widget来实现你的按钮。 Define a new stateless widget called `DownloadButton`. -定义一个名为 `DownloadButton` 的新无状态组件。 +定义一个名为 `DownloadButton` 的新stateless widget。 ```dart @@ -110,6 +112,7 @@ class DownloadButton extends StatelessWidget { ``` :::note + Each time you define a custom widget, you must decide whether all relevant information is provided to that widget @@ -126,19 +129,18 @@ By passing in all the relevant information, you ensure greater reusability for the widget, easier testing, and easier changes to application behavior in the future. -::: -:::note -每次定义自定义组件时, -你都必须决定是将所有相关信息从其父组件传递给该组件, +每次定义custom widget时, +你都必须决定是将所有相关信息从其parent widget传递给该组件, 还是让该组件在其内部协调应用程序行为。 -例如,`DownloadButton` 可以从其父组件接收当前的 `DownloadStatus`, +例如,`DownloadButton` 可以从其parent widget接收当前的 `DownloadStatus`, 也可以在其 `State` 对象内部管理下载过程。 -对于大多数组件来说,最佳做法是将相关信息从父组件传递到组件内部, +对于大多数组件来说,最佳做法是将相关信息从parent widget传递到组件内部, 而不是在组件内部管理行为。 通过传递所有相关信息, 你可以确保组件具有更高的可重用性、更容易进行测试, 以及在未来更容易更改应用程序行为。 + ::: ## Display the button shape @@ -173,11 +175,16 @@ like `Widget _buildSomething() {}`, always prefer creating a considerations on this can be found in the [documentation]({{site.api}}/flutter/widgets/StatelessWidget-class.html) or in a dedicated video in the Flutter [YouTube channel]({{site.yt.watch}}?v=IOyq-eTRhvo). -建议将形状的组件树定义在一个单独的 `Stateless` 组件中,这样主 `build()` 方法可以保持简洁,便于后续添加功能。与其创建一个返回组件的函数(例如 `Widget _buildSomething() {}`),更好的做法是创建一个 `StatelessWidget` 或 `StatefulWidget`,这在性能上更优。有关更多的考虑因素,你可以在 [文档]({{site.api}}/flutter/widgets/StatelessWidget-class.html) 中找到,或者在 Flutter 的 [YouTube 频道]({{site.yt.watch}}?v=IOyq-eTRhvo) 上观看相关视频。 +建议将形状的widget tree定义在一个单独的 `Stateless` 组件中, +这样主 `build()` 方法可以保持简洁,便于后续添加功能。 +与其创建一个返回组件的函数(例如 `Widget _buildSomething() {}`), +更好的做法是创建一个 `StatelessWidget` 或 `StatefulWidget`,这在性能上更优。 +有关更多的考虑因素,你可以在 [文档]({{site.api}}/flutter/widgets/StatelessWidget-class.html) 中找到, +或者在 Flutter 的 [YouTube 频道]({{site.yt.watch}}?v=IOyq-eTRhvo) 上观看相关视频。 For now, the `AnimatedContainer` child is just a `SizedBox` because we will come back at it in another step. -目前,`AnimatedContainer` 的子组件只是一个 `SizedBox`, +目前,`AnimatedContainer` 的child widget只是一个 `SizedBox`, 因为我们将在后续步骤中再来处理它。 @@ -265,7 +272,12 @@ ending shape of a circle. But, you don't want the final circle to be visible, so you make it transparent, which causes an animated fade-out. -你可能会疑惑,为什么需要为一个透明的圆形使用 `ShapeDecoration` 组件,毕竟它是不可见的。这个透明圆形的目的是为了协调动画效果。`AnimatedContainer` 一开始显示为圆角矩形。当 `DownloadStatus` 变为 `fetchingDownload` 时,`AnimatedContainer` 需要从圆角矩形动画过渡到圆形,并在动画进行的过程中逐渐淡出。实现这种动画的唯一方法是定义圆角矩形的起始形状和圆形的结束形状。但是,你不希望最终的圆形可见,所以将其设置为透明,这样就能实现动画淡出的效果。 +你可能会疑惑,为什么需要为一个透明的圆形使用 `ShapeDecoration` 组件,毕竟它是不可见的。 +这个透明圆形的目的是为了协调动画效果。`AnimatedContainer` 一开始显示为圆角矩形。 +当 `DownloadStatus` 变为 `fetchingDownload` 时, +`AnimatedContainer` 需要从圆角矩形动画过渡到圆形,并在动画进行的过程中逐渐淡出。 +实现这种动画的唯一方法是定义圆角矩形的起始形状和圆形的结束形状。 +但是,你不希望最终的圆形可见,所以将其设置为透明,这样就能实现动画淡出的效果。 ## Display the button text @@ -284,7 +296,7 @@ widget tree as a child of the `AnimatedContainer` in the button wrapper widget. 添加用于显示每个下载阶段文本的组件,并在阶段之间动画化文本的透明度。 -将文本组件树作为 `AnimatedContainer` 的子组件添加到按钮包装组件中。 +将text widget tree作为 `AnimatedContainer` 的child widget添加到按钮包装组件中。 ```dart @@ -485,7 +497,7 @@ Finally, wrap `DownloadButton`'s existing widget tree with a `GestureDetector` widget, and forward the tap event to the corresponding callback property. -最后,用 `GestureDetector` 包装 `DownloadButton` 的现有组件树,并将点击事件转发到相应的回调属性。 +最后,用 `GestureDetector` 包装 `DownloadButton` 的现有widget tree,并将点击事件转发到相应的回调属性。 ```dart From cf084055f956e5d622852ae7bd3b10a6495d8d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sat, 24 Aug 2024 17:25:44 +0800 Subject: [PATCH 03/12] =?UTF-8?q?[=E5=AE=8C=E5=96=84=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/cookbook/effects/download-button.md=20#1171?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 感谢你的悉心指导,我对该文件又进行了完善,采取了你的建议 由于初次接触,可能会对相关方面的步骤不太熟悉,会尽力配合更正的 --- .../cookbook/effects/download-button.md | 132 ++++++++++-------- 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/src/content/cookbook/effects/download-button.md b/src/content/cookbook/effects/download-button.md index 2e4067b071..01fdf45aff 100644 --- a/src/content/cookbook/effects/download-button.md +++ b/src/content/cookbook/effects/download-button.md @@ -1,8 +1,8 @@ --- -title: Create a download button -标题:创建一个下载按钮 -description: How to implement a download button. -描述:如何实现下载按钮 +# title: Create a download button +title:创建一个带进度条的下载按钮 +# description: How to implement a download button. +description:如何实现下载按钮 js: - defer: true url: /assets/js/inject_dartpad.js @@ -20,14 +20,15 @@ to provide this feedback. In this recipe, you'll build a download button that transitions through multiple visual states, based on the status of an app download. -应用程序中充满了执行长时间操作的按钮。 +应用程序中有许多按钮需要执行长时间的操作。 例如,一个按钮可能会触发下载, -启动下载过程,并在一段时间内接收数据, -然后提供对下载资源的访问。 -向用户显示长时间运行过程的进度是很有帮助的, -而按钮本身是提供此反馈的好地方。 -在这个示例中,你将构建一个下载按钮, -根据应用程序下载的状态在多个视觉状态之间切换。 +从而启动下载流程,并在一段时间内接收数据, +最终提供对下载资源的访问。 +向用户展示长时间运行过程的进度有助于提升体验, +而按钮本身正是传达这种反馈的理想位置。 +在本教程中, +你将构建一个能够根据应用程序的下载状态, +切换不同视觉效果的下载按钮。 The following animation shows the app's behavior: @@ -43,12 +44,12 @@ Your button widget needs to change its appearance over time. Therefore, you need to implement your button with a custom stateless widget. -你的button widget需要随着时间的推移改变其外观。 -因此,你需要使用 custom stateless widget来实现你的按钮。 +你的按钮 widget 需要随着时间的推移改变其外观。 +因此,你需要自定义 stateless widget 来实现这种按钮。 Define a new stateless widget called `DownloadButton`. -定义一个名为 `DownloadButton` 的新stateless widget。 +定义一个新的 stateless widget,名为 `DownloadButton`。 ```dart @@ -68,7 +69,7 @@ class DownloadButton extends StatelessWidget { ## Define the button's possible visual states -## 定义按钮的可能的视觉状态 +## 定义按钮的各种视觉状态 The download button's visual presentation is based on a given download status. Define the possible states of @@ -77,9 +78,10 @@ a `DownloadStatus` and a `Duration` for how long the button should take to animate from one status to another. 下载按钮的视觉呈现基于给定的下载状态。 -首先定义下载可能的状态,然后更新 `DownloadButton` -以接受一个 `DownloadStatus` 和一个 `Duration`, -用于指定按钮从一个状态动画过渡到另一个状态所需的时间。 +首先定义下载可能会出现的各种状态, +然后更新 `DownloadButton`, +使其接收一个 `DownloadStatus` 和一个 `Duration` 类型的参数, +`Duration` 用于指定按钮在不同状态之间切换时的动画过渡时间。 ```dart @@ -130,16 +132,17 @@ you ensure greater reusability for the widget, easier testing, and easier changes to application behavior in the future. -每次定义custom widget时, -你都必须决定是将所有相关信息从其parent widget传递给该组件, -还是让该组件在其内部协调应用程序行为。 -例如,`DownloadButton` 可以从其parent widget接收当前的 `DownloadStatus`, -也可以在其 `State` 对象内部管理下载过程。 -对于大多数组件来说,最佳做法是将相关信息从parent widget传递到组件内部, -而不是在组件内部管理行为。 -通过传递所有相关信息, -你可以确保组件具有更高的可重用性、更容易进行测试, -以及在未来更容易更改应用程序行为。 +每次自定义 widget 时, +你都需要决定所有相关信息, +是否从父级传递给该 widget, +还是让该 widget 在内部自行协调应用程序的行为。 +例如,`DownloadButton` 可以从父级接收当前的 `DownloadStatus`, +或者也可以在其 `State` 对象内部自行协调下载过程。 +对于大多数 widget 来说,最佳做法是将相关信息从父级传递给 widget, +而不是在 widget 内部自行协调行为。 +通过将所有相关信息传递给 widget, +你可以确保 widget 具有更高的可重用性、更易于测试, +同时也更方便在未来改变应用程序的行为。 ::: @@ -163,8 +166,8 @@ build an `AnimatedContainer` with a rectangle or a circle. 根据当前的 `DownloadStatus`, -构建一个带有 `ShapeDecoration` 的 `AnimatedContainer`, -该装饰可以显示圆角矩形或圆形。 +构建一个 `AnimatedContainer`, +其中使用 `ShapeDecoration` 来显示圆角矩形或圆形。 Consider defining the shape's widget tree in a separated `Stateless` widget so that the main `build()` @@ -175,16 +178,16 @@ like `Widget _buildSomething() {}`, always prefer creating a considerations on this can be found in the [documentation]({{site.api}}/flutter/widgets/StatelessWidget-class.html) or in a dedicated video in the Flutter [YouTube channel]({{site.yt.watch}}?v=IOyq-eTRhvo). -建议将形状的widget tree定义在一个单独的 `Stateless` 组件中, -这样主 `build()` 方法可以保持简洁,便于后续添加功能。 -与其创建一个返回组件的函数(例如 `Widget _buildSomething() {}`), +建议将形状的 widget 树定义在一个单独的 `Stateless` widget 中, +这样可以保持主 `build()` 方法的简洁,便于后续添加功能。 +与其创建一个返回 widget 的函数(例如 `Widget _buildSomething() {}`), 更好的做法是创建一个 `StatelessWidget` 或 `StatefulWidget`,这在性能上更优。 有关更多的考虑因素,你可以在 [文档]({{site.api}}/flutter/widgets/StatelessWidget-class.html) 中找到, 或者在 Flutter 的 [YouTube 频道]({{site.yt.watch}}?v=IOyq-eTRhvo) 上观看相关视频。 For now, the `AnimatedContainer` child is just a `SizedBox` because we will come back at it in another step. -目前,`AnimatedContainer` 的child widget只是一个 `SizedBox`, +目前,`AnimatedContainer` 的子 widget 只是一个 `SizedBox`, 因为我们将在后续步骤中再来处理它。 @@ -272,8 +275,10 @@ ending shape of a circle. But, you don't want the final circle to be visible, so you make it transparent, which causes an animated fade-out. -你可能会疑惑,为什么需要为一个透明的圆形使用 `ShapeDecoration` 组件,毕竟它是不可见的。 -这个透明圆形的目的是为了协调动画效果。`AnimatedContainer` 一开始显示为圆角矩形。 +你可能会疑惑,既然透明圆形是不可见的, +为什么还需要使用 `ShapeDecoration` widget 呢? +因为这个透明圆形的目的是为了协调动画效果。 +`AnimatedContainer` 一开始显示为圆角矩形。 当 `DownloadStatus` 变为 `fetchingDownload` 时, `AnimatedContainer` 需要从圆角矩形动画过渡到圆形,并在动画进行的过程中逐渐淡出。 实现这种动画的唯一方法是定义圆角矩形的起始形状和圆形的结束形状。 @@ -288,15 +293,18 @@ The `DownloadButton` displays `GET` during the phase, and no text in between. `DownloadButton` 在 `notDownloaded` 阶段显示 `GET`, -在 `downloaded` 阶段显示 `OPEN`,在中间阶段没有文本。 +在 `downloaded` 阶段显示 `OPEN`, +而在其他阶段则不显示文本。 Add widgets to display text during each download phase, and animate the text's opacity in between. Add the text widget tree as a child of the `AnimatedContainer` in the button wrapper widget. -添加用于显示每个下载阶段文本的组件,并在阶段之间动画化文本的透明度。 -将text widget tree作为 `AnimatedContainer` 的child widget添加到按钮包装组件中。 +添加用于显示每个下载阶段文本的 widget, +并在各阶段之间对文本的不透明度进行动画处理。 +将文本 widget 树作为 `AnimatedContainer` 的子 widget, +添加到包装按钮的 widget 中。 ```dart @@ -357,26 +365,26 @@ class ButtonShapeWidget extends StatelessWidget { ## Display a spinner while fetching download -## 下载时显示一个进度条 +## 获取下载时显示一个加载指示器 During the `fetchingDownload` phase, the `DownloadButton` displays a radial spinner. This spinner fades in from the `notDownloaded` phase and fades out to the `fetchingDownload` phase. -在 `fetchingDownload` 阶段,`DownloadButton` 会显示一个径向进度条。 -这个进度条从 `notDownloaded` 阶段淡入,到 `fetchingDownload` 阶段淡出。 +在 `fetchingDownload` 阶段,`DownloadButton` 会显示一个旋转的加载指示器。 +这个加载指示器从 `notDownloaded` 阶段淡入,到 `fetchingDownload` 阶段淡出。 Implement a radial spinner that sits on top of the button shape and fades in and out at the appropriate times. -实现一个位于按钮形状之上的径向进度条,并在适当的时间淡入和淡出。 +实现一个覆盖在按钮上方的加载指示器,并在适当的时机淡入和淡出。 We have removed the `ButtonShapeWidget`'s constructor to keep the focus on its build method and the `Stack` widget we've added. 我们删除了 `ButtonShapeWidget` 的构造函数, -以便集中精力关注其 build 方法和添加的 `Stack` 组件。 +以便将重点放在 build 方法和我们添加的 `Stack` widget 上。 ```dart @@ -421,15 +429,17 @@ radial progress bar. The `DownloadButton` also displays a stop button icon so that the user can cancel an in-progress download. 在 `fetchingDownload` 阶段之后,是 `downloading` 阶段。 -在 `downloading` 阶段,`DownloadButton` 会用增长型的径向进度条替换径向进度条。 +在 `downloading` 阶段, +`DownloadButton` 会用一个随进度增长的径向进度条来替代加载指示器。 `DownloadButton` 还会显示一个停止按钮图标,以便用户可以取消正在进行的下载。 Add a progress property to the `DownloadButton` widget, and then update the progress display to switch to a radial progress bar during the `downloading` phase. -将一个进度属性添加到 `DownloadButton` 组件中, -然后在 `downloading` 阶段切换到径向进度条。 +为 `DownloadButton` widget 添加一个进度属性, +然后更新进度的显示, +使其在 `downloading` 阶段切换到径向进度条。 Next, add a stop button icon at the center of the radial progress bar. @@ -486,18 +496,20 @@ Widget build(BuildContext context) { The last detail that your `DownloadButton` needs is the button behavior. The button must do things when the user taps it. -`DownloadButton` 的最后一个细节是按钮行为。当用户点击按钮时,该按钮必须做某些事情。 +最后一步,`DownloadButton` 还需要实现按钮的行为。 +按钮必须在用户点击时执行相应的操作。 Add widget properties for callbacks to start a download, cancel a download, and open a download. -添加回调属性,以开始下载、取消下载和打开下载。 +为 widget 添加回调,用于开始下载、取消下载和打开下载。 Finally, wrap `DownloadButton`'s existing widget tree with a `GestureDetector` widget, and forward the tap event to the corresponding callback property. -最后,用 `GestureDetector` 包装 `DownloadButton` 的现有widget tree,并将点击事件转发到相应的回调属性。 +最后,用 `GestureDetector` widget 包装 `DownloadButton` 现有的 widget 树, +并将点击事件转发到相应的回调属性。 ```dart @@ -560,13 +572,13 @@ fetching download, downloading, and downloaded. Now, the user can tap to start a download, tap to cancel an in-progress download, and tap to open a completed download. -恭喜!您已经有一个按钮, -该按钮根据按钮所处的阶段而改变其显示:未下载、获取下载、下载中和已下载。 -现在,用户可以点击以启动下载、点击以取消正在进行的下载,并点击以打开已完成的下载。 +恭喜!你已经完成了一个按钮, +它会根据按钮所处的阶段来改变显示效果:未下载、获取下载、下载中和已下载。 +现在,用户可以点击按钮来启动下载、取消正在进行的下载,或者打开已完成的下载。 ## Interactive example -## 互动示例 +## 交互示例 Run the app: @@ -574,16 +586,22 @@ Run the app: * Click the **GET** button to kick off a simulated download. + + 点击 **GET** 按钮开始模拟下载。 + * The button changes to a progress indicator to simulate an in-progress download. + + 按钮会切换到进度指示器,以模拟下载中的状态。 + * When the simulated download is complete, the button transitions to **OPEN**, to indicate that the app is ready for the user to open the downloaded asset. -* 点击 **GET** 按钮以启动模拟下载。 -* 该按钮更改为进度指示器以模拟正在进行的下载。 -* 当模拟下载完成时,按钮将过渡到 **OPEN** ,表示应用已准备好让用户打开下载的资源。 + 当模拟下载完成后, + 按钮将过渡到 **OPEN**, + 表示应用已准备好让用户打开下载的资源。 From aa315ca212519849e372d6f7a496a1f29d1cc1fa Mon Sep 17 00:00:00 2001 From: Amos Date: Sat, 24 Aug 2024 17:46:20 +0800 Subject: [PATCH 04/12] Fix: CI --- src/content/cookbook/effects/download-button.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/cookbook/effects/download-button.md b/src/content/cookbook/effects/download-button.md index 01fdf45aff..3daff3323d 100644 --- a/src/content/cookbook/effects/download-button.md +++ b/src/content/cookbook/effects/download-button.md @@ -1,8 +1,8 @@ --- # title: Create a download button -title:创建一个带进度条的下载按钮 +title: 创建一个带进度条的下载按钮 # description: How to implement a download button. -description:如何实现下载按钮 +description: 如何实现下载按钮 js: - defer: true url: /assets/js/inject_dartpad.js From 1f1caefd5b70e87c0679f78fb28fc184dcdf5bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sat, 24 Aug 2024 23:09:25 +0800 Subject: [PATCH 05/12] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/cookbook/effects/nested-nav.md=20#1172?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content/cookbook/effects/nested-nav.md | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/content/cookbook/effects/nested-nav.md b/src/content/cookbook/effects/nested-nav.md index 60acb5cc91..5a99a4af63 100644 --- a/src/content/cookbook/effects/nested-nav.md +++ b/src/content/cookbook/effects/nested-nav.md @@ -19,6 +19,13 @@ top-level `Navigator` widget. The list would be very long, and many of these routes would be better handled nested within another widget. +应用程序随着时间的推移会累积几十甚至上百个 routes。 +其中有些 routes 作为 top-level (global) routes 是合理的。 +例如,"/"、"profile"、"contact"、"social_feed" +这些都可能是应用中的 top-level routes 。 +但是,想象一下,如果你在 top-level `Navigator` widget 中定义了所有可能的 routes , +那么这个列表会非常长,其中很多 routes 更适合嵌套在另一个 widget 中处理。 + Consider an Internet of Things (IoT) setup flow for a wireless light bulb that you control with your app. This setup flow consists of 4 pages: @@ -32,21 +39,43 @@ in the setup flow. This delegation of navigation facilitates greater local control, which is generally preferable when developing software. +想象一个用于无线灯泡的物联网 (IoT) 设置流程, +你可以通过应用程序来控制这个灯泡。 +这个设置流程包括4个页面: +寻找附近的灯泡、选择你要添加的灯泡、添加灯泡、然后完成设置。 +你可以在 top-level `Navigator` widget 中协调这些操作。 +然而,更合理的做法是, +在你的 `SetupFlow` widget 中定义一个嵌套的 `Navigator` widget, +并让这个嵌套的 `Navigator` 负责管理设置流程中的这4个页面。 +这种导航方式有助于实现更大程度上的本地控制, +这在软件开发中通常是更可取的。 + The following animation shows the app's behavior: +下面的动画展示了应用程序的行为: + ![Gif showing the nested "setup" flow](/assets/images/docs/cookbook/effects/NestedNavigator.gif){:.site-mobile-screenshot} In this recipe, you implement a four-page IoT setup flow that maintains its own navigation nested beneath the top-level `Navigator` widget. +在这个教程中,你将实现一个包含四个页面的物联网 (IoT) 设置流程, +该流程的 Navigation 将嵌套在 top-level `Navigator` widget 之下,并自行管理。 + ## Prepare for navigation +## 准备 Navigation + This IoT app has two top-level screens, along with the setup flow. Define these route names as constants so that they can be referenced within code. +这个物联网 (IoT) 应用程序有两个 top-level 屏幕, +以及一个设置流程。 +将这些 routes 名称定义为常量,以便在代码中引用它们。 + ```dart const routeHome = '/'; @@ -68,6 +97,13 @@ that a route name is intended for the setup flow without recognizing all the individual pages associated with the setup flow. +主屏幕和设置屏幕的 routes 是使用静态名称引用的。 +然而,设置流程中的页面是通过两个路径组合来生成它们的 routes 名称的: +首先是一个 `/setup/` 前缀,然后是具体页面的名称。 +通过将这两个路径组合在一起, +你的 `Navigator` 可以判断出 routes 名称是为设置流程设计的, +而无需识别与设置流程相关的所有单个页面。 + The top-level `Navigator` isn't responsible for identifying individual setup flow pages. Therefore, your top-level `Navigator` needs to parse the incoming route name to @@ -76,9 +112,19 @@ means that you can't use the `routes` property of your top-level `Navigator`. Instead, you must provide a function for the `onGenerateRoute` property. + Top-level `Navigator` 不负责识别具体的设置流程页面。 +因此, top-level `Navigator` 需要解析传入的 routes 名称, +以识别设置流程的前缀。 +由于需要解析 routes 名称, +不能使用 top-level `Navigator` 的 `routes` 属性。 +相反,你必须为 `onGenerateRoute` 属性提供一个函数。 + Implement `onGenerateRoute` to return the appropriate widget for each of the three top-level paths. +实现 `onGenerateRoute` 函数, +以便为三个 top-level paths 中的每一个返回相应的 widget。 + ```dart onGenerateRoute: (settings) { @@ -115,9 +161,18 @@ This splitting of the route name is what allows the top-level `Navigator` to be agnostic toward the various subroutes within the setup flow. +请注意,home 和 settings routes 是与精确的 routes name 匹配的。 +然而,设置流程的 routes 条件只检查前缀。 +如果 routes name 包含设置流程的前缀,那么 routes name 的其余部分将被忽略, +并传递给 `SetupFlow` widget 进行处理。 + Routes name 的这种拆分使 top-level `Navigator` 可以不关注设置流程中的各个 subroutes 。 + Create a stateful widget called `SetupFlow` that accepts a route name. +创建一个名为 `SetupFlow` 的 statefull widget, +该 widget 接受一个 routes name 作为参数。 + ```dart class SetupFlow extends StatefulWidget { @@ -139,13 +194,20 @@ class SetupFlowState extends State { ## Display an app bar for the setup flow +## 为设置流程显示一个应用栏 + The setup flow displays a persistent app bar that appears across all pages. +设置流程显示一个持久的应用栏,该应用栏会在所有页面上保持显示。 + Return a `Scaffold` widget from your `SetupFlow` widget's `build()` method, and include the desired `AppBar` widget. +在你的 `SetupFlow` widget 的 `build()` 方法中返回一个 `Scaffold` widget, +并包含所需的 `AppBar` widget。 + ```dart @override @@ -169,10 +231,18 @@ exiting the flow causes the user to lose all progress. Therefore, the user is prompted to confirm whether they want to exit the setup flow. +应用栏显示一个返回箭头, +当返回箭头被按下时,会退出设置流程。 +然而,退出流程会导致用户丢失所有进度。 +因此,系统会提示用户确认是否真的想要退出设置流程。 + Prompt the user to confirm exiting the setup flow, and ensure that the prompt appears when the user presses the hardware back button on Android. +提示用户确认是否退出设置流程, +并确保在用户按下 Android 硬件返回按钮时也会出现该提示。 + ```dart Future _onExitPressed() async { @@ -251,20 +321,37 @@ If the user presses **Leave**, then the setup flow pops itself from the top-level navigation stack. If the user presses **Stay**, then the action is ignored. +当用户点击应用栏中的返回箭头或按下 Android 硬件返回按钮时, +会弹出一个警告对话框,确认用户是否要离开设置流程。 +如果用户点击 **Leave**,则设置流程会从 top-level navigation stack 中弹出。 +如果用户点击 **Stay**,则忽略该操作。 + You might notice that the `Navigator.pop()` is invoked by both the **Leave** and **Stay** buttons. To be clear, this `pop()` action pops the alert dialog off the navigation stack, not the setup flow. +你可能会注意到, +**Leave** 和 **Stay** 按钮都会调用 `Navigator.pop()`。 +需要明确的是,这个 `pop()` 操作是将警告对话框从 navigation stack 中弹出, +而不是设置流程。 + ## Generate nested routes +## 生成 nested routes + The setup flow's job is to display the appropriate page within the flow. +设置流程的任务是显示流程中的适当页面。 + Add a `Navigator` widget to `SetupFlow`, and implement the `onGenerateRoute` property. +在 `SetupFlow` 中添加一个 `Navigator` widget , +并实现 `onGenerateRoute` 属性。 + ```dart final _navigatorKey = GlobalKey(); @@ -338,6 +425,10 @@ which includes the route's `name`. Based on that route name, one of four flow pages is returned. +`_onGenerateRoute` 函数的工作方式与 top-level `Navigator` 相同。 +一个 `RouteSettings` 对象会传递给函数,其中包含 route's `name` 。 +根据该 routes name ,将返回四个流程页面之一。 + The first page, called `find_devices`, waits a few seconds to simulate network scanning. After the wait period, the page invokes its callback. @@ -348,6 +439,13 @@ Therefore, in `_onDiscoveryComplete`, the `_navigatorKey` instructs the nested `Navigator` to navigate to the `select_device` page. +第一页称为 `find_devices`,等待几秒钟以模拟网络扫描。 +在等待时间结束后,页面会调用其回调函数。 +在这种情况下,回调函数是 `_onDiscoveryComplete`。 +设置流程识别到设备发现完成后,应该显示设备选择页面。 +因此,在 `_onDiscoveryComplete` 中, +`_navigatorKey` 指示嵌套的 `Navigator` 导航到 `select_device` 页面。 + The `select_device` page asks the user to select a device from a list of available devices. In this recipe, only one device is presented to the user. @@ -358,6 +456,13 @@ should be shown. Therefore, in `_onDeviceSelected`, the `_navigatorKey` instructs the nested `Navigator` to navigate to the `"connecting"` page. +`select_device` 页面要求用户从可用设备列表中选择一个设备。 +在这个示例中,只向用户展示了一个设备。 +当用户点击设备时,`onDeviceSelected` 回调被调用。 +设置流程识别到设备选择后,应该显示连接页面。 +因此,在 `_onDeviceSelected` 中, +`_navigatorKey` 指示嵌套的 `Navigator` 导航到 `"connecting"` 页面。 + The `connecting` page works the same way as the `find_devices` page. The `connecting` page waits for a few seconds and then invokes its callback. @@ -368,27 +473,55 @@ in `_onConnectionEstablished`, the `_navigatorKey` instructs the nested `Navigator` to navigate to the `finished` page. +`connecting` 页面与 `find_devices` 页面工作方式相同。 +`connecting` 页面等待几秒钟,然后调用其回调函数。 +在这种情况下,回调函数是 `_onConnectionEstablished`。 +设置流程识别到连接建立后,应该显示最终页面。 +因此,在 `_onConnectionEstablished` 中, +`_navigatorKey` 指示嵌套的 `Navigator` 导航到 `finished` 页面。 + The `finished` page provides the user with a **Finish** button. When the user taps **Finish**, the `_exitSetup` callback is invoked, which pops the entire setup flow off the top-level `Navigator` stack, taking the user back to the home screen. +`finished` 页面提供了一个 **Finish** 按钮。 +当用户点击 **Finish** 时,`_exitSetup` 回调被调用, +这将从 top-level `Navigator` stack 中弹出整个设置流程,将用户带回到首页。 + Congratulations! You implemented nested navigation with four subroutes. +恭喜你!你实现了具有四个 subroutes 的 nested navigation 。 + ## Interactive example +## 交互示例 + Run the app: +运行应用程序: + * On the **Add your first bulb** screen, click the FAB, shown with a plus sign, **+**. This brings you to the **Select a nearby device** screen. A single bulb is listed. + + 在 **添加你的第一个灯泡** 屏幕上, + 点击带有加号 **+** 的 FAB。 + 这会将你带到 **选择附近设备** 屏幕。 + 屏幕上画了一个灯泡。 + * Click the listed bulb. A **Finished!** screen appears. + + 按下画着的灯泡。屏幕上出现 **结束!** 。 + * Click the **Finished** button to return to the first screen. + 按下 **结束** 按钮返回第一页。 + ```dartpad title="Flutter nested navigation hands-on example in DartPad" run="true" import 'package:flutter/material.dart'; From 8620009baf0701ff4a3453c4ae0444fa69934630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Mon, 26 Aug 2024 18:26:57 +0800 Subject: [PATCH 06/12] =?UTF-8?q?[=E5=AE=8C=E5=96=84=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/content/cookbook/effects/nested-nav.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抱歉可能对一些专用名词的翻译存在错误 --- src/content/cookbook/effects/nested-nav.md | 115 +++++++++++---------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/src/content/cookbook/effects/nested-nav.md b/src/content/cookbook/effects/nested-nav.md index 5a99a4af63..9ccb8cb63d 100644 --- a/src/content/cookbook/effects/nested-nav.md +++ b/src/content/cookbook/effects/nested-nav.md @@ -19,12 +19,13 @@ top-level `Navigator` widget. The list would be very long, and many of these routes would be better handled nested within another widget. -应用程序随着时间的推移会累积几十甚至上百个 routes。 -其中有些 routes 作为 top-level (global) routes 是合理的。 -例如,"/"、"profile"、"contact"、"social_feed" -这些都可能是应用中的 top-level routes 。 -但是,想象一下,如果你在 top-level `Navigator` widget 中定义了所有可能的 routes , -那么这个列表会非常长,其中很多 routes 更适合嵌套在另一个 widget 中处理。 +随着应用程序的发展,它会累积几十甚至上百条路由。 +虽然,有些路由可以作为顶层(全局)路由。 +例如,“/”、“profile”、“contact”、“social_feed” +这些都是应用中可能存在的顶层路由。 +但是,如果你在顶层 `Navigator` widget 中定义了所有可能的路由, +那么路由列表将会非常庞大, +实际上,许多路由更适合嵌套在其他 widget 中处理。 Consider an Internet of Things (IoT) setup flow for a wireless light bulb that you control with your app. @@ -39,15 +40,15 @@ in the setup flow. This delegation of navigation facilitates greater local control, which is generally preferable when developing software. -想象一个用于无线灯泡的物联网 (IoT) 设置流程, +设想一个用于无线灯泡的物联网 (IoT) 设置流程, 你可以通过应用程序来控制这个灯泡。 -这个设置流程包括4个页面: -寻找附近的灯泡、选择你要添加的灯泡、添加灯泡、然后完成设置。 -你可以在 top-level `Navigator` widget 中协调这些操作。 +该设置流程包括 4 个页面: +查找附近的灯泡、选择你要添加的灯泡、添加灯泡、最后完成设置。 +你可以在顶层 `Navigator` widget 中协调这些操作。 然而,更合理的做法是, 在你的 `SetupFlow` widget 中定义一个嵌套的 `Navigator` widget, -并让这个嵌套的 `Navigator` 负责管理设置流程中的这4个页面。 -这种导航方式有助于实现更大程度上的本地控制, +并让这个嵌套的 `Navigator` 负责管理设置流程中的这 4 个页面。 +这种导航委托方式有助于加强局部控制, 这在软件开发中通常是更可取的。 The following animation shows the app's behavior: @@ -61,20 +62,20 @@ flow that maintains its own navigation nested beneath the top-level `Navigator` widget. 在这个教程中,你将实现一个包含四个页面的物联网 (IoT) 设置流程, -该流程的 Navigation 将嵌套在 top-level `Navigator` widget 之下,并自行管理。 +该流程在顶层 `Navigator` widget 下嵌套了单独管理的导航。 ## Prepare for navigation -## 准备 Navigation +## 导航准备阶段 This IoT app has two top-level screens, along with the setup flow. Define these route names as constants so that they can be referenced within code. -这个物联网 (IoT) 应用程序有两个 top-level 屏幕, +这个物联网 (IoT) 应用程序包含两个顶层页面, 以及一个设置流程。 -将这些 routes 名称定义为常量,以便在代码中引用它们。 +将这些路由名称定义为常量,以便在代码中引用它们。 ```dart @@ -97,12 +98,12 @@ that a route name is intended for the setup flow without recognizing all the individual pages associated with the setup flow. -主屏幕和设置屏幕的 routes 是使用静态名称引用的。 -然而,设置流程中的页面是通过两个路径组合来生成它们的 routes 名称的: +主页和设置页的路由是使用静态名称引用的。 +然而,设置流程中的页面是通过两个路径组合来生成它们的路由名称的: 首先是一个 `/setup/` 前缀,然后是具体页面的名称。 通过将这两个路径组合在一起, -你的 `Navigator` 可以判断出 routes 名称是为设置流程设计的, -而无需识别与设置流程相关的所有单个页面。 +你的 `Navigator` 可以判断出某个路由名称是否属于设置流程, +而无需识别所有与设置流程相关的具体页面。 The top-level `Navigator` isn't responsible for identifying individual setup flow pages. Therefore, your top-level @@ -112,18 +113,18 @@ means that you can't use the `routes` property of your top-level `Navigator`. Instead, you must provide a function for the `onGenerateRoute` property. - Top-level `Navigator` 不负责识别具体的设置流程页面。 -因此, top-level `Navigator` 需要解析传入的 routes 名称, +顶层 `Navigator` 不负责识别具体的设置流程页面。 +因此,顶层 `Navigator` 需要解析传入的路由名称, 以识别设置流程的前缀。 -由于需要解析 routes 名称, -不能使用 top-level `Navigator` 的 `routes` 属性。 +由于需要解析路由名称, +不能使用顶层 `Navigator` 的 `routes` 属性。 相反,你必须为 `onGenerateRoute` 属性提供一个函数。 Implement `onGenerateRoute` to return the appropriate widget for each of the three top-level paths. 实现 `onGenerateRoute` 函数, -以便为三个 top-level paths 中的每一个返回相应的 widget。 +以便为三个顶层路径分别返回相应的 widget。 ```dart @@ -161,17 +162,17 @@ This splitting of the route name is what allows the top-level `Navigator` to be agnostic toward the various subroutes within the setup flow. -请注意,home 和 settings routes 是与精确的 routes name 匹配的。 -然而,设置流程的 routes 条件只检查前缀。 -如果 routes name 包含设置流程的前缀,那么 routes name 的其余部分将被忽略, -并传递给 `SetupFlow` widget 进行处理。 - Routes name 的这种拆分使 top-level `Navigator` 可以不关注设置流程中的各个 subroutes 。 +请注意,主页和设置页的路由是与精确的路由名称相匹配的。 +然而,设置流程的路由条件只检查前缀。 +如果路由名称包含设置流程的前缀,那么路由名称的其余部分将被忽略, +并将其余部分传递给 `SetupFlow` widget 进行处理。 +这种对路由名称的拆分方式,使顶层 `Navigator` 可以不关注设置流程中的各个子路由。 Create a stateful widget called `SetupFlow` that accepts a route name. -创建一个名为 `SetupFlow` 的 statefull widget, -该 widget 接受一个 routes name 作为参数。 +创建一个名为 `SetupFlow` 的 stateful widget, +该 widget 接收一个路由名称作为参数。 ```dart @@ -194,12 +195,12 @@ class SetupFlowState extends State { ## Display an app bar for the setup flow -## 为设置流程显示一个应用栏 +## 为设置流程显示一个 AppBar The setup flow displays a persistent app bar that appears across all pages. -设置流程显示一个持久的应用栏,该应用栏会在所有页面上保持显示。 +设置流程显示一个始终可见的 AppBar,贯穿设置流程中的所有页面。 Return a `Scaffold` widget from your `SetupFlow` widget's `build()` method, @@ -231,7 +232,7 @@ exiting the flow causes the user to lose all progress. Therefore, the user is prompted to confirm whether they want to exit the setup flow. -应用栏显示一个返回箭头, +AppBar 显示一个返回箭头, 当返回箭头被按下时,会退出设置流程。 然而,退出流程会导致用户丢失所有进度。 因此,系统会提示用户确认是否真的想要退出设置流程。 @@ -241,7 +242,7 @@ and ensure that the prompt appears when the user presses the hardware back button on Android. 提示用户确认是否退出设置流程, -并确保在用户按下 Android 硬件返回按钮时也会出现该提示。 +并确保在用户按下 Android 设备上的实体返回按钮时也会出现该提示。 ```dart @@ -321,9 +322,9 @@ If the user presses **Leave**, then the setup flow pops itself from the top-level navigation stack. If the user presses **Stay**, then the action is ignored. -当用户点击应用栏中的返回箭头或按下 Android 硬件返回按钮时, +当用户点击 AppBar 中的返回箭头或按下 Android 设备上的实体返回按钮时, 会弹出一个警告对话框,确认用户是否要离开设置流程。 -如果用户点击 **Leave**,则设置流程会从 top-level navigation stack 中弹出。 +如果用户点击 **Leave**,则设置流程会从顶层导航堆栈中移除。 如果用户点击 **Stay**,则忽略该操作。 You might notice that the `Navigator.pop()` @@ -334,17 +335,17 @@ the navigation stack, not the setup flow. 你可能会注意到, **Leave** 和 **Stay** 按钮都会调用 `Navigator.pop()`。 -需要明确的是,这个 `pop()` 操作是将警告对话框从 navigation stack 中弹出, -而不是设置流程。 +需要明确的是,这个 `pop()` 操作是将警告对话框从导航堆栈中移除, +而不是移除设置流程。 ## Generate nested routes -## 生成 nested routes +## 创建嵌套路由 The setup flow's job is to display the appropriate page within the flow. -设置流程的任务是显示流程中的适当页面。 +设置流程的任务是显示流程中相应的页面。 Add a `Navigator` widget to `SetupFlow`, and implement the `onGenerateRoute` property. @@ -425,9 +426,9 @@ which includes the route's `name`. Based on that route name, one of four flow pages is returned. -`_onGenerateRoute` 函数的工作方式与 top-level `Navigator` 相同。 -一个 `RouteSettings` 对象会传递给函数,其中包含 route's `name` 。 -根据该 routes name ,将返回四个流程页面之一。 +`_onGenerateRoute` 函数的工作方式与顶层 `Navigator` 相同。 +该函数接收一个`RouteSettings` 对象,其中包含了路由名称 `name`。 +根据路由名称,将返回四个流程页面之一。 The first page, called `find_devices`, waits a few seconds to simulate network scanning. @@ -439,9 +440,9 @@ Therefore, in `_onDiscoveryComplete`, the `_navigatorKey` instructs the nested `Navigator` to navigate to the `select_device` page. -第一页称为 `find_devices`,等待几秒钟以模拟网络扫描。 +第一页名为 `find_devices`,它会等待几秒钟来模拟网络扫描。 在等待时间结束后,页面会调用其回调函数。 -在这种情况下,回调函数是 `_onDiscoveryComplete`。 +在这个教程中,回调函数是 `_onDiscoveryComplete`。 设置流程识别到设备发现完成后,应该显示设备选择页面。 因此,在 `_onDiscoveryComplete` 中, `_navigatorKey` 指示嵌套的 `Navigator` 导航到 `select_device` 页面。 @@ -457,7 +458,7 @@ the `_navigatorKey` instructs the nested `Navigator` to navigate to the `"connecting"` page. `select_device` 页面要求用户从可用设备列表中选择一个设备。 -在这个示例中,只向用户展示了一个设备。 +在这个教程中,只向用户展示了一个设备。 当用户点击设备时,`onDeviceSelected` 回调被调用。 设置流程识别到设备选择后,应该显示连接页面。 因此,在 `_onDeviceSelected` 中, @@ -475,7 +476,7 @@ instructs the nested `Navigator` to navigate to the `connecting` 页面与 `find_devices` 页面工作方式相同。 `connecting` 页面等待几秒钟,然后调用其回调函数。 -在这种情况下,回调函数是 `_onConnectionEstablished`。 +在这个教程中,回调函数是 `_onConnectionEstablished`。 设置流程识别到连接建立后,应该显示最终页面。 因此,在 `_onConnectionEstablished` 中, `_navigatorKey` 指示嵌套的 `Navigator` 导航到 `finished` 页面。 @@ -488,12 +489,12 @@ taking the user back to the home screen. `finished` 页面提供了一个 **Finish** 按钮。 当用户点击 **Finish** 时,`_exitSetup` 回调被调用, -这将从 top-level `Navigator` stack 中弹出整个设置流程,将用户带回到首页。 +这会将整个设置流程从顶层 `Navigator` 堆栈中移除,使用户回到主页。 Congratulations! You implemented nested navigation with four subroutes. -恭喜你!你实现了具有四个 subroutes 的 nested navigation 。 +恭喜你!你实现了具有四个子路由的嵌套导航。 ## Interactive example @@ -508,19 +509,19 @@ Run the app: This brings you to the **Select a nearby device** screen. A single bulb is listed. - 在 **添加你的第一个灯泡** 屏幕上, - 点击带有加号 **+** 的 FAB。 - 这会将你带到 **选择附近设备** 屏幕。 - 屏幕上画了一个灯泡。 + 在 **Add your first bulb** 页面上, + 点击带有加号 **+** 的悬浮操作按钮。 + 这会将你带到 **Select a nearby device** 页面。 + 页面上列出了一个灯泡设备。 * Click the listed bulb. A **Finished!** screen appears. - 按下画着的灯泡。屏幕上出现 **结束!** 。 + 点击列出的灯泡设备。页面上出现 **Finish** 按钮。 * Click the **Finished** button to return to the first screen. - 按下 **结束** 按钮返回第一页。 + 按下 **Finish** 按钮返回第一页。 ```dartpad title="Flutter nested navigation hands-on example in DartPad" run="true" From 09f2d8b405bdc6b3d9bcfa89cc3ab7bdf0aea270 Mon Sep 17 00:00:00 2001 From: Amos Date: Mon, 26 Aug 2024 19:10:14 +0800 Subject: [PATCH 07/12] Update: src/content/cookbook/effects/nested-nav.md --- src/content/cookbook/effects/nested-nav.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/cookbook/effects/nested-nav.md b/src/content/cookbook/effects/nested-nav.md index 9ccb8cb63d..c16f1dcc56 100644 --- a/src/content/cookbook/effects/nested-nav.md +++ b/src/content/cookbook/effects/nested-nav.md @@ -350,7 +350,7 @@ page within the flow. Add a `Navigator` widget to `SetupFlow`, and implement the `onGenerateRoute` property. -在 `SetupFlow` 中添加一个 `Navigator` widget , +在 `SetupFlow` 中添加一个 `Navigator` widget, 并实现 `onGenerateRoute` 属性。 From 167876939474bf8369afea86ccc9bd0f0b6fbc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sat, 31 Aug 2024 15:19:43 +0800 Subject: [PATCH 08/12] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/content/cookbook/effects/parallax-scrolling.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cookbook/effects/parallax-scrolling.md | 162 +++++++++++++++++- 1 file changed, 160 insertions(+), 2 deletions(-) diff --git a/src/content/cookbook/effects/parallax-scrolling.md b/src/content/cookbook/effects/parallax-scrolling.md index 6ed1f42464..8c07444578 100644 --- a/src/content/cookbook/effects/parallax-scrolling.md +++ b/src/content/cookbook/effects/parallax-scrolling.md @@ -1,6 +1,8 @@ --- -title: Create a scrolling parallax effect -description: How to implement a scrolling parallax effect. +# title: Create a scrolling parallax effect +title: 创建一个视差滚动效果 +# description: How to implement a scrolling parallax effect. +description: 如何实现视差滚动效果 js: - defer: true url: /assets/js/inject_dartpad.js @@ -16,26 +18,50 @@ are in the foreground, but the images themselves sit far off in the distant background. This effect is known as parallax. +当你在应用程序中滚动一个包含图片的卡片列表时, +你可能会注意到那些图片的滚动速度比屏幕的其余部分要慢一些。 +看起来似乎列表中的卡片位于前面, +而图片本身则像是远远地在背景中。 +这种效果被称为视差。 + In this recipe, you create the parallax effect by building a list of cards (with rounded corners containing some text). Each card also contains an image. As the cards slide up the screen, the images within each card slide down. +在这个教程中, +你通过构建一个卡片列表来创建视差效果 +(这些卡片具有圆角并包含一些文本)。 +每个卡片还包含一张图片。 +当卡片向上滑动时,卡片中的图片会向下滑动。 + The following animation shows the app's behavior: +下面的动画展示了应用程序的行为: + ![Parallax scrolling](/assets/images/docs/cookbook/effects/ParallaxScrolling.gif){:.site-mobile-screenshot} ## Create a list to hold the parallax items +## 创建一个列表来保存视差项 + To display a list of parallax scrolling images, you must first display a list. +要实现一个带有视差滚动效果的图片列表,首先必须显示一个列表。 + Create a new stateless widget called `ParallaxRecipe`. Within `ParallaxRecipe`, build a widget tree with a `SingleChildScrollView` and a `Column`, which forms a list. +创建一个新的 stateless widget , +名为 `ParallaxRecipe` 。 +在 `ParallaxRecipe` 中, +构建一个包含 `SingleChildScrollView` 和 `Column` 的组件树, +这样就形成了一个列表。 + ```dart class ParallaxRecipe extends StatelessWidget { @@ -54,6 +80,8 @@ class ParallaxRecipe extends StatelessWidget { ## Display items with text and a static image +## 显示带有文本和静态图片的元素 + Each list item displays a rounded-rectangle background image, representing one of seven locations in the world. Stacked on top of that background image is the @@ -63,11 +91,23 @@ background image and the text is a dark gradient, which improves the legibility of the text against the background. +每个列表项显示一个圆角矩形背景图片, +代表世界上的七个地点之一。 +在该背景图片的上方叠加了地点名称及其所在国家, +位置在左下角。 +在背景图片和文字之间有一个深色渐变效果, +以提高文字在背景上的可读性。 + Implement a stateless widget called `LocationListItem` that consists of the previously mentioned visuals. For now, use a static `Image` widget for the background. Later, you'll replace that widget with a parallax version. +实现一个名为 `LocationListItem` 的 stateless widget , +该 widget 包含之前提到的视觉效果。 +现在,使用一个静态的 `Image` widget 作为背景。 +稍后,你将用视差版本的 widget 替换这个静态图片 widget 。 + ```dart @immutable @@ -159,6 +199,8 @@ class LocationListItem extends StatelessWidget { Next, add the list items to your `ParallaxRecipe` widget. +然后,将列表项添加到你的 `ParallaxRecipe` widget 中。 + ```dart class ParallaxRecipe extends StatelessWidget { @@ -187,8 +229,14 @@ that displays seven unique locations in the world. In the next step, you add a parallax effect to the background image. +现在,你已经有了一个典型的可滚动卡片列表, +展示了世界上七个独特的地点。 +在下一步中,你将为背景图片添加视差效果。 + ## Implement the parallax effect +## 实现视差滚动效果 + A parallax scrolling effect is achieved by slightly pushing the background image in the opposite direction of the rest of the list. As the list items slide up @@ -197,6 +245,13 @@ Conversely, as the list items slide down the screen, each background image slides slightly upward. Visually, this results in parallax. +视差滚动效果通过将背景图片 +稍微向与列表其余部分相反的方向推移来实现。 +当列表项向上滑动时,每个背景图片会稍微向下滑动。 +相反,当列表项向下滑动时, +每个背景图片会稍微向上滑动。 +从视觉上看,这就产生了视差效果。 + The parallax effect depends on the list item's current position within its ancestor `Scrollable`. As the list item's scroll position changes, the position @@ -213,26 +268,53 @@ before the widget is painted. In other words, you can intercept the painting phase and take control to reposition your child widgets however you want. +视差效果依赖于列表项在其祖先 `Scrollable` 中的当前位置。 +随着列表项的滚动位置变化, +列表项的背景图片位置也必须随之变化。 +这是一个有趣的问题。 +列表项在 `Scrollable` 中的位置 +在 Flutter 的布局阶段完成之前是不可用的。 +这意味着背景图片的位置必须在绘制阶段确定, +而绘制阶段在布局阶段之后进行。 +幸运的是,Flutter 提供了一个名为 `Flow` 的 widget , +专门设计用于在 widget 被绘制之前立即控制子 widget 的变换。 +换句话说,你可以拦截绘制阶段并控制子 widget 的位置, +以便按照你的需求重新定位。 + :::note + To learn more, check out this short Widget of the Week video on the `Flow` widget: +要了解更多信息,请查看这段关于 `Flow` widget 的简短视频: + {% ytEmbed 'NG6pvXpnIso', 'Flow | Flutter widget of the week' %} ::: :::note + In cases where you need control over what a child paints, rather than where a child is painted, consider using a [`CustomPaint`][] widget. +在需要控制子 widget 绘制内容, +而不是子 widget 的绘制位置时, +可以考虑使用 [CustomPaint][] widget 。 + In cases where you need control over the layout, painting, and hit testing, consider defining a custom [`RenderBox`][]. + +在需要控制布局、绘制和点击测试时, +可以考虑定义一个自定义 [RenderBox][] 。 + ::: Wrap your background `Image` widget with a [`Flow`][] widget. +用 [`Flow`][] widget 包裹你的背景 `Image` widget 。 + ```dart Widget _buildParallaxBackground(BuildContext context) { @@ -249,6 +331,8 @@ Widget _buildParallaxBackground(BuildContext context) { Introduce a new `FlowDelegate` called `ParallaxFlowDelegate`. +引入一个新的 `FlowDelegate`,名为 `ParallaxFlowDelegate`。 + ```dart Widget _buildParallaxBackground(BuildContext context) { @@ -292,8 +376,15 @@ and where those children are painted. In this case, your `Flow` widget has only one child: the background image. That image must be exactly as wide as the `Flow` widget. +`FlowDelegate` 控制其子 widget 的大小和绘制位置。 +在这种情况下, +你的 `Flow` widget 只有一个子 widget :背景图片。 +该图片的宽度必须与 `Flow` 组件的宽度完全一致。 + Return tight width constraints for your background image child. +为你的背景图片子 widget 返回严格的宽度约束。 + ```dart @override @@ -309,28 +400,54 @@ but you still need to calculate the vertical position of each background image based on its scroll position, and then paint it. +现在你的背景图片大小已经合适, +但你仍然需要根据每个背景图片的滚动位置计算其垂直位置, +然后进行绘制。 + There are three critical pieces of information that you need to compute the desired position of a background image: +计算背景图片所需位置的三个关键要素是: + * The bounds of the ancestor `Scrollable` + + 祖先 Scrollable 的边界 + * The bounds of the individual list item + + 单个列表项的边界 + * The size of the image after it's scaled down to fit in the list item + 图片在缩放以适应列表项后的大小 + To look up the bounds of the `Scrollable`, you pass a `ScrollableState` into your `FlowDelegate`. +要查找 Scrollable 的边界, +可以将 ScrollableState 传递给你的 FlowDelegate 。 + To look up the bounds of your individual list item, pass your list item's `BuildContext` into your `FlowDelegate`. +要查找单个列表项的边界, +将列表项的 BuildContext 传递给你的 FlowDelegate 。 + To look up the final size of your background image, assign a `GlobalKey` to your `Image` widget, and then you pass that `GlobalKey` into your `FlowDelegate`. +要查找背景图片的最终大小, +为 Image widget 分配一个 GlobalKey, +然后将该 GlobalKey 传递给你的 FlowDelegate 。 + Make this information available to `ParallaxFlowDelegate`. +将这些信息提供给 `ParallaxFlowDelegate` 。 + ```dart @immutable @@ -374,6 +491,9 @@ class ParallaxFlowDelegate extends FlowDelegate { Having all the information needed to implement parallax scrolling, implement the `shouldRepaint()` method. +在拥有实现视差滚动所需的所有信息之后, +实现 `shouldRepaint()` 方法。 + ```dart @override @@ -386,9 +506,13 @@ bool shouldRepaint(ParallaxFlowDelegate oldDelegate) { Now, implement the layout calculations for the parallax effect. +现在,实施视差效果的布局计算。 + First, calculate the pixel position of a list item within its ancestor `Scrollable`. +首先,计算列表项在其祖先 `Scrollable` 中的像素位置。 + ```dart @override @@ -408,6 +532,10 @@ A list item at the top of the scrollable area should produce 0%, and a list item at the bottom of the scrollable area should produce 100%. +使用列表项的像素位置来计算它距离 `Scrollable` 顶部的百分比。 +位于可滚动区域顶部的列表项应产生 0%, +而位于可滚动区域底部的列表项应产生 100%。 + ```dart @override @@ -434,6 +562,11 @@ and at 100%, you want `Alignment(0.0, 1.0)`. These coordinates correspond to top and bottom alignment, respectively. +使用滚动百分比来计算 `Alignment`。 +在 0% 时,你需要 `Alignment(0.0, -1.0)`, +在 100% 时,你需要 `Alignment(0.0, 1.0)`。 +这些坐标分别对应于顶部对齐和底部对齐。 + ```dart @override @@ -462,6 +595,10 @@ list item and the size of the background image, to produce a `Rect` that determines where the background image should be positioned. +使用 `verticalAlignment`, +结合列表项的大小和背景图片的大小, +生成一个 `Rect`,以确定背景图片的位置。 + ```dart @override @@ -499,6 +636,9 @@ the desired translation transformation. It's this transformation over time that gives you the parallax effect. +使用 `childRect`,根据所需的平移变换绘制背景图片。 +正是这种随时间变化的变换效果产生了视差效果。 + ```dart @override @@ -543,10 +683,19 @@ The `ParallaxFlowDelegate` repaints when the inputs change, but the `ParallaxFlowDelegate` doesn't repaint every time the scroll position changes. +你还需要一个最后的细节来实现视差效果。 + `ParallaxFlowDelegate` 在输入发生变化时会重新绘制, + 但它不会在每次滚动位置变化时都重新绘制。 + Pass the `ScrollableState`'s `ScrollPosition` to the `FlowDelegate` superclass so that the `FlowDelegate` repaints every time the `ScrollPosition` changes. +将 `ScrollableState` 的 `ScrollPosition` +传递给 `FlowDelegate` 超类, +以便在 `ScrollPosition` 每次变化时, + `FlowDelegate` 都会重新绘制。 + ```dart class ParallaxFlowDelegate extends FlowDelegate { @@ -562,12 +711,21 @@ Congratulations! You now have a list of cards with parallax, scrolling background images. +恭喜! +现在你拥有了一个带有视差滚动背景图片的卡片列表。 + ## Interactive example +## 交互示例 + Run the app: +运行应用程序: + * Scroll up and down to observe the parallax effect. + 向上和向下滚动,以观察视差效果。 + ```dartpad title="Flutter parallax scrolling hands-on example in DartPad" run="true" import 'package:flutter/material.dart'; From 7db990abe68985dea8be6962c0d236b877712763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sat, 31 Aug 2024 23:51:25 +0800 Subject: [PATCH 09/12] =?UTF-8?q?[=E5=AE=8C=E5=96=84=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/content/cookbook/effects/parallax-scrolling.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对git的相关操作还不太熟练......麻烦了 --- .../cookbook/effects/parallax-scrolling.md | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/content/cookbook/effects/parallax-scrolling.md b/src/content/cookbook/effects/parallax-scrolling.md index 8c07444578..0acabd8415 100644 --- a/src/content/cookbook/effects/parallax-scrolling.md +++ b/src/content/cookbook/effects/parallax-scrolling.md @@ -56,10 +56,10 @@ Within `ParallaxRecipe`, build a widget tree with a `SingleChildScrollView` and a `Column`, which forms a list. -创建一个新的 stateless widget , -名为 `ParallaxRecipe` 。 +创建一个新的 stateless widget, +名为 `ParallaxRecipe`。 在 `ParallaxRecipe` 中, -构建一个包含 `SingleChildScrollView` 和 `Column` 的组件树, +构建一个包含 `SingleChildScrollView` 和 `Column` 的 widget 树, 这样就形成了一个列表。 @@ -80,7 +80,7 @@ class ParallaxRecipe extends StatelessWidget { ## Display items with text and a static image -## 显示带有文本和静态图片的元素 +## 显示带有文本和静态图片的列表项 Each list item displays a rounded-rectangle background image, representing one of seven locations in the world. @@ -93,8 +93,8 @@ of the text against the background. 每个列表项显示一个圆角矩形背景图片, 代表世界上的七个地点之一。 -在该背景图片的上方叠加了地点名称及其所在国家, -位置在左下角。 +在该背景图片的左下角叠加了地点名称及其所在国家。 +在背景图片和文字之间有一个深色渐变层, 在背景图片和文字之间有一个深色渐变效果, 以提高文字在背景上的可读性。 @@ -103,10 +103,10 @@ that consists of the previously mentioned visuals. For now, use a static `Image` widget for the background. Later, you'll replace that widget with a parallax version. -实现一个名为 `LocationListItem` 的 stateless widget , +实现一个名为 `LocationListItem` 的 stateless widget, 该 widget 包含之前提到的视觉效果。 现在,使用一个静态的 `Image` widget 作为背景。 -稍后,你将用视差版本的 widget 替换这个静态图片 widget 。 +稍后,你将用视差版本的 widget 替换这个静态图片 widget。 ```dart @@ -229,7 +229,7 @@ that displays seven unique locations in the world. In the next step, you add a parallax effect to the background image. -现在,你已经有了一个典型的可滚动卡片列表, +现在,你有了一个常见的可滚动卡片列表, 展示了世界上七个独特的地点。 在下一步中,你将为背景图片添加视差效果。 @@ -245,11 +245,11 @@ Conversely, as the list items slide down the screen, each background image slides slightly upward. Visually, this results in parallax. -视差滚动效果通过将背景图片 -稍微向与列表其余部分相反的方向推移来实现。 -当列表项向上滑动时,每个背景图片会稍微向下滑动。 +视差滚动效果通过轻微地将背景图片推向 +与列表其余部分相反的方向来实现。 +当列表项向上滑动时,每个背景图片会轻微向下滑动。 相反,当列表项向下滑动时, -每个背景图片会稍微向上滑动。 +每个背景图片会轻微向上滑动。 从视觉上看,这就产生了视差效果。 The parallax effect depends on the list item's @@ -271,12 +271,12 @@ to reposition your child widgets however you want. 视差效果依赖于列表项在其祖先 `Scrollable` 中的当前位置。 随着列表项的滚动位置变化, 列表项的背景图片位置也必须随之变化。 -这是一个有趣的问题。 -列表项在 `Scrollable` 中的位置 -在 Flutter 的布局阶段完成之前是不可用的。 +这是一个有趣的问题, +因为在 Flutter 的布局阶段完成之前, +无法获取列表项在 `Scrollable` 中的位置。 这意味着背景图片的位置必须在绘制阶段确定, 而绘制阶段在布局阶段之后进行。 -幸运的是,Flutter 提供了一个名为 `Flow` 的 widget , +幸运的是,Flutter 提供了一个名为 `Flow` 的 widget, 专门设计用于在 widget 被绘制之前立即控制子 widget 的变换。 换句话说,你可以拦截绘制阶段并控制子 widget 的位置, 以便按照你的需求重新定位。 @@ -286,7 +286,8 @@ to reposition your child widgets however you want. To learn more, check out this short Widget of the Week video on the `Flow` widget: -要了解更多信息,请查看这段关于 `Flow` widget 的简短视频: +要了解更多信息, +请观看这段关于 `Flow` widget (Flutter widget of the week) 的简短视频: {% ytEmbed 'NG6pvXpnIso', 'Flow | Flutter widget of the week' %} ::: @@ -297,23 +298,23 @@ In cases where you need control over what a child paints, rather than where a child is painted, consider using a [`CustomPaint`][] widget. -在需要控制子 widget 绘制内容, +如果你需要控制子 widget 绘制内容, 而不是子 widget 的绘制位置时, -可以考虑使用 [CustomPaint][] widget 。 +可以考虑使用 [`CustomPaint`][] widget。 In cases where you need control over the layout, painting, and hit testing, consider defining a custom [`RenderBox`][]. -在需要控制布局、绘制和点击测试时, -可以考虑定义一个自定义 [RenderBox][] 。 +如果你需要控制布局、绘制和点击测试时, +可以考虑自定义一个 [`RenderBox`][]。 ::: Wrap your background `Image` widget with a [`Flow`][] widget. -用 [`Flow`][] widget 包裹你的背景 `Image` widget 。 +用 [`Flow`][] widget 包裹你的背景 `Image` widget。 ```dart @@ -377,9 +378,9 @@ your `Flow` widget has only one child: the background image. That image must be exactly as wide as the `Flow` widget. `FlowDelegate` 控制其子 widget 的大小和绘制位置。 -在这种情况下, -你的 `Flow` widget 只有一个子 widget :背景图片。 -该图片的宽度必须与 `Flow` 组件的宽度完全一致。 +在本教程中, +你的 `Flow` widget 只有一个子 widget:背景图片。 +该图片的宽度必须与 `Flow` widget 的宽度完全一致。 Return tight width constraints for your background image child. @@ -412,7 +413,7 @@ background image: * The bounds of the ancestor `Scrollable` - 祖先 Scrollable 的边界 + 祖先 `Scrollable` 的边界 * The bounds of the individual list item @@ -421,32 +422,32 @@ background image: * The size of the image after it's scaled down to fit in the list item - 图片在缩放以适应列表项后的大小 + 图片缩放后的尺寸(为了适应列表项) To look up the bounds of the `Scrollable`, you pass a `ScrollableState` into your `FlowDelegate`. -要查找 Scrollable 的边界, -可以将 ScrollableState 传递给你的 FlowDelegate 。 +要获取 `Scrollable` 的边界, +可以将 `ScrollableState` 传递给你的 `FlowDelegate`。 To look up the bounds of your individual list item, pass your list item's `BuildContext` into your `FlowDelegate`. -要查找单个列表项的边界, -将列表项的 BuildContext 传递给你的 FlowDelegate 。 +要获取单个列表项的边界, +可以将列表项的 `BuildContext` 传递给你的 `FlowDelegate`。 To look up the final size of your background image, assign a `GlobalKey` to your `Image` widget, and then you pass that `GlobalKey` into your `FlowDelegate`. -要查找背景图片的最终大小, -为 Image widget 分配一个 GlobalKey, -然后将该 GlobalKey 传递给你的 FlowDelegate 。 +要获取背景图片最终的尺寸, +可以为 `Image` widget 分配一个 `GlobalKey`, +然后将该 `GlobalKey` 传递给你的 `FlowDelegate`。 Make this information available to `ParallaxFlowDelegate`. -将这些信息提供给 `ParallaxFlowDelegate` 。 +将这些信息提供给 `ParallaxFlowDelegate`。 ```dart @@ -637,7 +638,7 @@ It's this transformation over time that gives you the parallax effect. 使用 `childRect`,根据所需的平移变换绘制背景图片。 -正是这种随时间变化的变换效果产生了视差效果。 +正是这种随时间推移的变换效果产生了视差效果。 ```dart @@ -684,17 +685,17 @@ but the `ParallaxFlowDelegate` doesn't repaint every time the scroll position changes. 你还需要一个最后的细节来实现视差效果。 - `ParallaxFlowDelegate` 在输入发生变化时会重新绘制, - 但它不会在每次滚动位置变化时都重新绘制。 +`ParallaxFlowDelegate` 在参数发生变化时会重新绘制, +但它不会在每次滚动位置变化时都重新绘制。 Pass the `ScrollableState`'s `ScrollPosition` to the `FlowDelegate` superclass so that the `FlowDelegate` repaints every time the `ScrollPosition` changes. 将 `ScrollableState` 的 `ScrollPosition` -传递给 `FlowDelegate` 超类, +传递给 `FlowDelegate` 的父类, 以便在 `ScrollPosition` 每次变化时, - `FlowDelegate` 都会重新绘制。 +`FlowDelegate` 都会重新绘制。 ```dart From 7c5db054235f127e15a05e667c8e3e0cff36db26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sun, 1 Sep 2024 00:14:35 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content/cookbook/effects/parallax-scrolling.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/content/cookbook/effects/parallax-scrolling.md b/src/content/cookbook/effects/parallax-scrolling.md index 0acabd8415..a656b5cdd7 100644 --- a/src/content/cookbook/effects/parallax-scrolling.md +++ b/src/content/cookbook/effects/parallax-scrolling.md @@ -95,7 +95,6 @@ of the text against the background. 代表世界上的七个地点之一。 在该背景图片的左下角叠加了地点名称及其所在国家。 在背景图片和文字之间有一个深色渐变层, -在背景图片和文字之间有一个深色渐变效果, 以提高文字在背景上的可读性。 Implement a stateless widget called `LocationListItem` From b35bb961bca2a2ae7710b8bd790e146cf331c96b Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Tue, 3 Sep 2024 14:27:12 +0800 Subject: [PATCH 11/12] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/content/cookbook/effects/shimmer-loading.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cookbook/effects/shimmer-loading.md | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/src/content/cookbook/effects/shimmer-loading.md b/src/content/cookbook/effects/shimmer-loading.md index f9f068d9f0..8f3e369a44 100644 --- a/src/content/cookbook/effects/shimmer-loading.md +++ b/src/content/cookbook/effects/shimmer-loading.md @@ -18,8 +18,16 @@ to communicate to users that data is loading is to display a chrome color with a shimmer animation over the shapes that approximate the type of content that is loading. +在应用开发中,加载时间是不可避免的。 +从用户体验 (UX) 的角度来看, +最重要的是向用户展示正在进行加载操作。 +一种常见的方式是通过显示带有微光动画的镀铬颜色覆盖在近似于正在加载内容形状的区域上, +以此来告知用户数据正在加载。 + The following animation shows the app's behavior: +下面的动画展示了应用程序的行为: + ![Gif showing the UI loading](/assets/images/docs/cookbook/effects/UILoadingAnimation.gif){:.site-mobile-screenshot} This recipe begins with the content widgets defined and positioned. @@ -27,20 +35,37 @@ There is also a Floating Action Button (FAB) in the bottom-right corner that toggles between a loading mode and a loaded mode so that you can easily validate your implementation. +该示例从定义和定位内容 widgets 开始。 +界面右下角还放置了一个浮动操作按钮 (FAB), +用于在加载模式和已加载模式之间切换, +以便您可以轻松验证您的实现。 + ## Draw the shimmer shapes +## 绘制微光效果的形状 + The shapes that shimmer in this effect are independent from the actual content that eventually loads. +在这个效果中闪烁的形状是独立于最终加载的实际内容的。 + Therefore, the goal is to display shapes that represent the eventual content as accurately as possible. +因此,目标是尽可能准确地显示代表最终内容的形状。 + Displaying accurate shapes is easy in situations where the content has a clear boundary. For example, in this recipe, there are some circular images and some rounded rectangle images. You can draw shapes that precisely match the outlines of those images. +在内容有明确边界的情况下, +显示准确的形状很容易。 +例如,在这个示例中, +有一些圆形图片和一些圆角矩形图片。 +你可以绘制与这些图片轮廓完全匹配的形状。 + On the other hand, consider the text that appears beneath the rounded rectangle images. You won't know how many lines of text exist until the text loads. @@ -50,10 +75,21 @@ you draw a couple of very thin rounded rectangles that represent the text that will appear. The shape and size doesn't quite match, but that is OK. +另一方面,考虑显示在圆角矩形图片下方的文本。 +在文本加载之前,你不会知道有多少行文本。 +因此,尝试为每行文本绘制一个矩形是没有意义的。 +相反,当数据加载时, +你可以绘制几个非常细的圆角矩形来代表即将出现的文本。 +形状和大小可能不完全匹配, +但这样做是可以的。 + Start with the circular list items at the top of the screen. Ensure that each `CircleListItem` widget displays a circle with a color while the image is loading. +从屏幕顶部的圆形列表项开始。确 +保每个 `CircleListItem` widget 在图片加载时显示一个有颜色的圆形。 + ```dart class CircleListItem extends StatelessWidget { @@ -86,6 +122,9 @@ class CircleListItem extends StatelessWidget { As long as your widgets display some kind of shape, you can apply the shimmer effect in this recipe. +只要你的 widgets 显示某种形状, +你就可以在这个示例中应用微光效果。 + Similar to the `CircleListItem` widgets, ensure that the `CardListItem` widgets display a color where the image will appear. @@ -93,6 +132,11 @@ Also, in the `CardListItem` widget, switch between the display of the text and the rectangles based on the current loading status. +类似于 `CircleListItem` widget, +确保 `CardListItem` widget 件在图片将出现的地方显示颜色。 +同时,在 `CardListItem` widget 中, +根据当前的加载状态在文本和矩形的显示之间切换。 + ```dart class CardListItem extends StatelessWidget { @@ -181,14 +225,22 @@ whether it's loading or loaded. By temporarily commenting out the image URLs, you can see the two ways your UI renders. +现在,您的UI会根据是否正在加载或已加载来以不同方式呈现。 +通过暂时注释掉图像URL, +您可以看到UI呈现的两种方式。 + ![Gif showing the shimmer animation](/assets/images/docs/cookbook/effects/LoadingShimmer.gif){:.site-mobile-screenshot} The next goal is to paint all of the colored areas with a single gradient that looks like a shimmer. +接下来的目标是将所有着色区域涂上一个看起来像微光效果的渐变。 + ## Paint the shimmer gradient +## 绘制微光渐变效果 + The key to the effect achieved in this recipe is to use a widget called [`ShaderMask`][]. The `ShaderMask` widget, as the name suggests, applies a shader to its child, but only in the areas where @@ -196,9 +248,17 @@ the child already painted something. For example, you'll apply a shader to only the black shapes that you configured earlier. +实现这一效果的关键是使用一个名为 [`ShaderMask`][] 的 widget。 +顾名思义, `ShaderMask` widget 将着色器应用于其子 widget, +但仅在子小部件已经绘制了内容的区域。 +例如,你将把着色器应用于之前配置的黑色形状上。 + Define a chrome-colored, linear gradient that gets applied to the shimmer shapes. +定义一个铬色的线性渐变, +将其应用于闪烁的形状。 + ```dart const _shimmerGradient = LinearGradient( @@ -225,6 +285,12 @@ gradient as a shader with a `blendMode` of `srcATop`. The `srcATop` blend mode replaces any color that your `child` widget painted with the shader color. +定义一个新的叫做 `ShimmerLoading` 的 stateful widget, +它将给定的 `child` widget 包裹在 `ShaderMask` 中。 +配置 `ShaderMask` widget 以将微光渐变作为着色器应用, +并使用 `srcATop` 的混合模式。 +`srcATop` 混合模式会将 `child` widget 绘制的任何颜色替换为着色器颜色。 + ```dart class ShimmerLoading extends StatefulWidget { @@ -261,6 +327,8 @@ class _ShimmerLoadingState extends State { Wrap your `CircleListItem` widgets with a `ShimmerLoading` widget. +将您的 `CircleListItem` widgets 用 `ShimmerLoading` widget 进行包裹。 + ```dart Widget _buildTopRowItem() { @@ -273,6 +341,8 @@ Widget _buildTopRowItem() { Wrap your `CardListItem` widgets with a `ShimmerLoading` widget. +将您的 `CardListItem` widgets 用 `ShimmerLoading` widget 进行包裹。 + ```dart Widget _buildListItem() { @@ -289,6 +359,9 @@ When your shapes are loading, they now display the shimmer gradient that is returned from the `shaderCallback`. +当您的形状正在加载时, +它们现在显示了从 `shaderCallback` 返回的微光加载。 + This is a big step in the right direction, but there's a problem with this gradient display. Each `CircleListItem` widget and each `CardListItem` widget @@ -297,14 +370,27 @@ For this recipe, the entire screen should look like one, big shimmering surface. You solve this problem in the next step. +这是朝着正确方向迈出的重要一步, +但这个渐变显示存在一个问题。 +每个 `CircleListItem` widget 和每个 `CardListItem` widget +都显示了渐变的一个新版本。 +对于这个示例,整个屏幕应该看起来像一个大的微光表面。 +您将在下一步解决这个问题。 + ## Paint one big shimmer +## 绘制一个大面积的微光效果 + To paint one big shimmer across the screen, each `ShimmerLoading` widget needs to paint the same full-screen gradient based on the position of that `ShimmerLoading` widget on the screen. +为了在整个屏幕上绘制一个大面积的微光效果, +每个 `ShimmerLoading` widget 需要根据 +该 `ShimmerLoading` widget 在屏幕上的位置绘制相同的全屏渐变。 + To be more precise, rather than assume that the shimmer should take up the entire screen, there should be some area that shares the shimmer. @@ -317,10 +403,22 @@ Then, each `ShimmerLoading` widget gets a reference to the `Shimmer` ancestor and requests the desired size and gradient to display. +更准确地说,不应该假设微光效果会占据整个屏幕, +而是应该有一个区域共享微光效果。 +这个区域可能会占据整个屏幕,也可能不会。 +解决这个问题的方式是在所有的 `ShimmerLoading` widget 上方 +定义另一个 widget ,称为 `Shimmer`。 +然后,每个 `ShimmerLoading` widget 可以引用 `Shimmer` 祖先, +并请求显示所需的大小和渐变。 + Define a new stateful widget called `Shimmer` that takes in a [`LinearGradient`][] and provides descendants with access to its `State` object. +定义一个新的 stateful widget ,命名为 `Shimmer`, +它接受一个 [`LinearGradient`][] 并为 +后代 widget 提供访问其 `State` 对象的权限。 + ```dart class Shimmer extends StatefulWidget { @@ -355,6 +453,10 @@ the size of the `ShimmerState`'s `RenderBox`, and look up the position of a descendant within the `ShimmerState`'s `RenderBox`. +在 `ShimmerState` 类中添加方法, +以提供对 `linearGradient`、`ShimmerState` 的 `RenderBox` 的大小 +以及查找 `ShimmerState` 的 `RenderBox` 中某个后代位置的访问权限。 + ```dart class ShimmerState extends State { @@ -387,6 +489,8 @@ class ShimmerState extends State { Wrap all of your screen's content with the `Shimmer` widget. +将屏幕上的所有内容都用 `Shimmer` widget 包裹起来。 + ```dart class _ExampleUiLoadingAnimationState extends State { @@ -407,6 +511,8 @@ class _ExampleUiLoadingAnimationState extends State { Use the `Shimmer` widget within your `ShimmerLoading` widget to paint the shared gradient. +在你的 `ShimmerLoading` widget 中使用 `Shimmer` widget 来绘制共享的渐变效果。 + ```dart class _ShimmerLoadingState extends State { @@ -451,19 +557,33 @@ Your `ShimmerLoading` widgets now display a shared gradient that takes up all of the space within the `Shimmer` widget. +你的 `ShimmerLoading` widget 现在显示了一个共享的渐变效果, +这个效果覆盖了 `Shimmer` widget 内部的所有空间。 + ## Animate the shimmer +## 给微光效果添加动画 + The shimmer gradient needs to move in order to give the appearance of a shimmering shine. +微光渐变需要移动以产生微光的效果。 + The `LinearGradient` has a property called `transform` that can be used to transform the appearance of the gradient, for example, to move it horizontally. The `transform` property accepts a `GradientTransform` instance. +`LinearGradient` 有一个名为 `transform` 的属性, +可以用来改变渐变的外观,例如,使其水平移动。 +`transform` 属性接受一个 `GradientTransform` 实例。 + Define a class called `_SlidingGradientTransform` that implements `GradientTransform` to achieve the appearance of horizontal sliding. +定义一个名为 `_SlidingGradientTransform` 的类, +来实现 `GradientTransform` 接口,以实现水平滑动的效果。 + ```dart class _SlidingGradientTransform extends GradientTransform { @@ -485,6 +605,9 @@ in order to create the appearance of motion. To change the percentage, configure an [`AnimationController`][] in the `ShimmerState` class. +渐变滑动百分比随时间变化,以创造运动的效果。 +要改变百分比,请在 `ShimmerState` 类中配置一个 [`AnimationController`][]。 + ```dart class ShimmerState extends State with SingleTickerProviderStateMixin { @@ -509,6 +632,9 @@ class ShimmerState extends State with SingleTickerProviderStateMixin { Apply the `_SlidingGradientTransform` to the `gradient` by using the `_shimmerController`'s `value` as the `slidePercent`. +通过使用 `_shimmerController` 的 `value` 作为 `slidePercent`, +将 `_SlidingGradientTransform` 应用到 `gradient` 上。 + ```dart LinearGradient get gradient => LinearGradient( @@ -526,9 +652,15 @@ The gradient now animates, but your individual as the gradient changes. Therefore, it looks like nothing is happening. +渐变现在已经动画化了, +但你的单个 `ShimmerLoading` widget 在渐变变化时没有重新绘制自己。 +因此,看起来什么也没有发生。 + Expose the `_shimmerController` from `ShimmerState` as a [`Listenable`][]. +在 `ShimmerState` 中将 `_shimmerController` 暴露为一个 [`Listenable`][]。 + ```dart Listenable get shimmerChanges => _shimmerController; @@ -538,6 +670,10 @@ In `ShimmerLoading`, listen for changes to the ancestor `ShimmerState`'s `shimmerChanges` property, and repaint the shimmer gradient. +在 `ShimmerLoading` 中, +监听祖先 `ShimmerState` 的 `shimmerChanges` 属性的变化, +并重新绘制微光渐变效果。 + ```dart class _ShimmerLoadingState extends State { @@ -576,8 +712,14 @@ You now have a full-screen, animated shimmer effect that turns on and off as the content loads. +恭喜! +你现在拥有了一个全屏的动画微光效果, +它在内容加载时会打开和关闭。 + ## Interactive example +## 交互示例 + ```dartpad title="Flutter shimmer loading hands-on example in DartPad" run="true" import 'package:flutter/material.dart'; From dfa041b231cf81f9aa771d5f896e09057e40d3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <144885467+Pleasurecruise@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:55:11 +0800 Subject: [PATCH 12/12] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?src/content/cookbook/effects/drag-a-widget.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content/cookbook/effects/drag-a-widget.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/content/cookbook/effects/drag-a-widget.md b/src/content/cookbook/effects/drag-a-widget.md index c34da22cc0..82d0523023 100644 --- a/src/content/cookbook/effects/drag-a-widget.md +++ b/src/content/cookbook/effects/drag-a-widget.md @@ -2,6 +2,7 @@ # title: Drag a UI element title: 创建一个可拖放的 UI 组件 description: How to implement a draggable UI element. +description: 如何实现一个可拖放的 UI 组件 js: - defer: true url: /assets/js/inject_dartpad.js @@ -19,8 +20,18 @@ where the user long presses on a choice of food, and then drags that food to the picture of the customer who is paying for it. +拖放是移动应用程序中的一种常见交互。 +当用户长按(有时称为“触摸并按住”)某个 widget 时, +另一个 widget 会出现在用户的手指下方, +用户可以将该 widget 拖动到最终位置并释放。 +在这个示例中,你将构建一个拖放交互, +用户可以长按一个食物选项, +然后将该食物拖动到正在为其付款的顾客图片上。 + The following animation shows the app's behavior: +下面的动画展示了应用程序的行为: + ![Ordering the food by dragging it to the person](/assets/images/docs/cookbook/effects/DragAUIElement.gif){:.site-mobile-screenshot} This recipe begins with a prebuilt list of menu items and @@ -28,8 +39,13 @@ a row of customers. The first step is to recognize a long press and display a draggable photo of a menu item. +本示例从一个预先构建的菜单项列表和一排顾客开始。 +第一步是识别长按操作并显示一个可拖动的菜单项图片。 + ## Press and drag +## 按压和拖动 + Flutter provides a widget called [`LongPressDraggable`][] that provides the exact behavior that you need to begin a drag-and-drop interaction. A `LongPressDraggable` @@ -39,9 +55,18 @@ As the user drags, the widget follows the user's finger. `LongPressDraggable` gives you full control over the widget that the user drags. +Flutter 提供了一个名为 [`LongPressDraggable`][] 的 widget, +它提供了开始拖放交互所需的确切行为。 +`LongPressDraggable` widget 能够识别长按操作, +然后在用户手指附近显示一个新 widget。 +当用户拖动时,该 widget 会跟随用户的手指移动。 +`LongPressDraggable` 让你完全控制用户拖动的 widget。 + Each menu list item is displayed with a custom `MenuListItem` widget. +每个菜单列表项都通过一个自定义的 `MenuListItem` widget 来显示。 + ```dart MenuListItem( @@ -53,6 +78,8 @@ MenuListItem( Wrap the `MenuListItem` widget with a `LongPressDraggable` widget. +用 `LongPressDraggable` widget 包裹 `MenuLIstItem` widget。 + ```dart LongPressDraggable( @@ -77,6 +104,10 @@ This `DraggingListItem` displays a photo of the selected food item, centered beneath the user's finger. +在这种情况下,当用户长按 `MenuListItem` widget 时, +`LongPressDraggable` widget 会显示一个 `DraggingListItem`。 +这个 `DraggingListItem` 会在用户手指下方居中显示所选的食物图片。 + The `dragAnchorStrategy` property is set to [`pointerDragAnchorStrategy`][]. This property value instructs `LongPressDraggable` @@ -84,6 +115,11 @@ to base the `DraggableListItem`'s position on the user's finger. As the user moves a finger, the `DraggableListItem` moves with it. +`dragAnchorStrategy` 属性设置为 [`pointerDragAnchorStrategy`][]。 +这个属性的值指示 `LongPressDraggable` +将 `DraggableListItem` 的位置基于用户的手指来定位。 +当用户移动手指时,`DraggableListItem` 也会跟随移动。 + Dragging and dropping is of little use if no information is transmitted when the item is dropped. For this reason, `LongPressDraggable` takes a `data` parameter. @@ -91,22 +127,42 @@ In this case, the type of `data` is `Item`, which holds information about the food menu item that the user pressed on. +如果拖放操作在释放时没有传递任何信息,那么它几乎没有什么用处。 +为此,`LongPressDraggable` 提供了一个 `data` 参数。 +在这种情况下,`data` 的类型是 `Item`, +它包含了用户按下的菜单项的食物信息。 + The `data` associated with a `LongPressDraggable` is sent to a special widget called `DragTarget`, where the user releases the drag gesture. You'll implement the drop behavior next. +与 `LongPressDraggable` 相关联的 `data` +会被传递到一个名为 `DragTarget` 的特殊 widget 上, +用户在此释放拖动手势。接下来你将实现拖放行为。 + ## Drop the draggable +## 放下可拖动组件 + The user can drop a `LongPressDraggable` wherever they choose, but dropping the draggable has no effect unless it's dropped on top of a `DragTarget`. When the user drops a draggable on top of a `DragTarget` widget, the `DragTarget` widget can either accept or reject the data from the draggable. +用户可以将 `LongPressDraggable` 放在任意位置, +但除非将其放在 `DragTarget` 之上, +否则拖放操作不会有任何效果。 +当用户将可拖动 widget 放在 `DragTarget` widget 上时, +`DragTarget` 可以选择接受或拒绝来自可拖动 widget 的数据。 + In this recipe, the user should drop a menu item on a `CustomerCart` widget to add the menu item to the user's cart. +在本示例中,用户应将菜单项拖放到 `CustomerCart` widget 上, +以将菜单项添加到用户的购物车中。 + ```dart CustomerCart( @@ -118,6 +174,8 @@ CustomerCart( Wrap the `CustomerCart` widget with a `DragTarget` widget. +用 `DragTarget` widget 包裹 `CustomCart` widget。 + ```dart DragTarget( @@ -143,6 +201,11 @@ when the user drags a draggable on top of the `DragTarget`. The `DragTarget` also recognizes when the user drops a draggable on top of the `DragTarget` widget. +`DragTarget` 显示你现有的 widget, +并与 `LongPressDraggable` 协调, +识别用户何时将可拖动的 widget 拖动到 `DragTarget` 之上。 +同时,`DragTarget` 也能识别用户何时在其上方放下一个可拖动 widget。 + When the user drags a draggable on the `DragTarget` widget, `candidateItems` contains the data items that the user is dragging. This draggable allows you to change what your widget looks @@ -151,6 +214,12 @@ the `Customer` widget turns red whenever any items are dragged above the `DragTarget` widget. The red visual appearance is configured with the `highlighted` property within the `CustomerCart` widget. +当用户将可拖动 widget 拖动到 `DragTarget` widget 上时, +`candidateItems` 包含用户正在拖动的数据项。 +你可以根据用户拖动的情况更改 widget 的外观。 +在本例中,`Customer` widget 在有项目拖动到 `DragTarget` widget 上方时会变成红色。 +这个红色的外观是通过 `CustomerCart` widget 中的 `highlighted` 属性配置的。 + When the user drops a draggable on the `DragTarget` widget, the `onAcceptWithDetails` callback is invoked. This is when you get to decide whether or not to accept the data that was dropped. @@ -158,23 +227,40 @@ In this case, the item is always accepted and processed. You might choose to inspect the incoming item to make a different decision. +当用户将可拖动 widget 放在 `DragTarget` widget 上时, +会调用 `onAcceptWithDetails` 回调。这时你可以决定是否接受拖放的数据。 +在示例中,项目总是会被接受和处理。 +你也可以选择检查传入的项目以做出不同的决定。 + Notice that the type of item dropped on `DragTarget` must match the type of the item dragged from `LongPressDraggable`. If the types are not compatible, then the `onAcceptWithDetails` method isn't invoked. +注意,拖放到 `DragTarget` 上的项目类型必须与从 `LongPressDraggable` 拖出的项目类型匹配。 +如果类型不兼容,`onAcceptWithDetails` 方法将不会被调用。 + With a `DragTarget` widget configured to accept your desired data, you can now transmit data from one part of your UI to another by dragging and dropping. +通过配置 `DragTarget` widget 来接受你所需的数据, +现在你可以通过拖放在 UI 的不同部分之间传递数据。 + In the next step, you update the customer's cart with the dropped menu item. +在下一步中,你将更新顾客的购物车,将放下的菜单项添加进去。 + ## Add a menu item to a cart +## 添加一个菜单项到购物车 + Each customer is represented by a `Customer` object, which maintains a cart of items and a price total. +每位顾客由一个 `Customer` 对象表示,该对象维护一个物品购物车和总价格。 + ```dart class Customer { @@ -199,9 +285,13 @@ class Customer { The `CustomerCart` widget displays the customer's photo, name, total, and item count based on a `Customer` instance. +`CustomerCart` widget 根据一个 `Customer` 实例显示顾客的照片、姓名、总价和物品数量。 + To update a customer's cart when a menu item is dropped, add the dropped item to the associated `Customer` object. +要在菜单项被拖放时更新顾客的购物车,需要将放下的物品添加到关联的 `Customer` 对象中。 + ```dart void _itemDroppedOnCustomerCart({ @@ -221,19 +311,40 @@ The `_itemDroppedOnCustomerCart` method is invoked in layout update, the UI refreshes with the new customer's price total and item count. +当用户将菜单项拖放到 `CustomerCart` widget 上时, +`onAcceptWithDetails()` 中会调用 `_itemDroppedOnCustomerCart` 方法。 +通过将放下的物品添加到 `customer` 对象中, +并调用 `setState()` 来触发布局更新, +UI 将会刷新,显示新的顾客总价和物品数量。 + Congratulations! You have a drag-and-drop interaction that adds food items to a customer's shopping cart. +恭喜!你现在已经实现了一个将食物项添加到顾客购物车的拖放交互。 + ## Interactive example +## 交互示例 + Run the app: +运行应用程序 + * Scroll through the food items. + + 浏览食物项列表。 + * Press and hold on one with your finger or click and hold with the mouse. + + 用手指长安其中一个食物项,或用鼠标点击并按住。 + * While holding, the food item's image will appear above the list. + + 按住时,食物项的图片将出现在列表上方。 + * Drag the image and drop it on one of the people at the bottom of the screen. The text under the image updates to @@ -241,6 +352,10 @@ Run the app: You can continue to add food items and watch the charges accumulate. + 将图片拖动并放到屏幕底部的某个人身上。 +图片下方的文本会更新,显示该人的费用。 +你可以继续添加食物项,观察费用的累积情况。 +