在上一步中,我们创建了一个 ImageItem
来显示单张图片,作为构建应用图片网格的一部分。
回顾一下我们的图片网格结构:
ImageItem
中。ImageItem
水平排列在一个 ImageRow
中。ImageRow
垂直排列组成一个 ImageGrid
。这一步中,我们将把多个 ImageItem
组合到一个 ImageRow
中,以显示一行图片。
注意:如果你不想跟着敲代码,可以在这里找到本步骤的全部代码:https://github.com/makepad/image_viewer/tree/main/step_4
在本步骤中,你将学习:
PortalList
来显示一组项目。PortalList
的内容。ImageRow
我们首先添加一个 ImageRow
的定义。ImageRow
负责将多个 ImageItem
水平排列。
在 app.rs
中,将以下代码添加到 live design
块中,紧跟在 ImageItem
的定义之后:
这段代码定义了一个 ImageRow
。{{ImageRow}}
语法将我们在 DSL 中定义的 ImageRow
关联到 Rust 代码中的 ImageRow
结构体(我们稍后会介绍这个结构体)。
在 ImageRow
内部,我们使用了一个 PortalList
来列出其中的条目。
这个 PortalList
具有以下属性:
height: 256
:确保列表具有足够的垂直空间来显示每一个条目。flow: Right
:确保列表的子元素从左到右排列。注意:
PortalList
类似于一个标准列表,但它支持“无限滚动”:它只会渲染可见的条目,因此可以高效地处理大型列表。尽管我们其实不需要无限滚动的功能,但在撰写本文时,Makepad 还没有标准列表组件,因此我们使用PortalList
作为替代方案。
与其他组件不同,PortalList
的内容并不是通过 DSL 代码来决定的,而是必须通过 Rust 代码动态生成。
因此,下面这一行代码:
它并不像你可能预期的那样定义了一个 ImageItem
实例。相反,它定义了一个 ImageItem
的模板。稍后,当我们在 Rust 代码中生成这个 PortalList
的内容时,就可以使用这个模板来创建所需的各个项的实例。
注意: 回忆一下,
{{ImageRow}}
语法告诉 Makepad,ImageRow
的定义关联到了 Rust 代码中的一个ImageRow
结构体。正因为PortalList
的内容必须由 Rust 代码生成,我们才需要使用一个 Rust 结构体来承载它。
现在我们已经定义了 ImageRow
,接下来我们将更新 App
的定义,使其显示一个 ImageRow
,而不是一个 ImageItem
。
在 app.rs
中,将 live design 块中的 App
定义替换为下面这一段代码:
ImageRow
结构体在之前的 DSL 代码中,我们使用 {{ImageRow}}
语法将 ImageRow
链接到了 Rust 代码中的 ImageRow
结构体。通过定义这样一个结构体,我们可以使用 Rust 代码来重写 ImageRow
的行为。
将以下代码添加到 app.rs
中:
请注意,我们为 ImageRow
结构体派生了多个 trait。我们已经了解了 Live
和 LiveHook
这两个 trait 的作用,但 Widget
是新的。我们来更详细地看看这个 trait 的作用。
Widget
traitWidget
trait 允许我们自定义一个小部件(widget)的行为。
有些令人困惑的是,派生 Widget
trait 并不会自动生成该 trait 的实现。相反,它会为 Widget
所依赖的一些辅助 trait 生成实现。这使得我们更容易手动实现 Widget
trait,但我们仍然需要自己编写具体的实现代码,我们将在下一节中完成这部分工作。
#[deref]
属性在派生 Widget
trait 时会用到。将此属性放在 ImageRow
结构体的 view
字段上,可以让我们像使用 View
一样使用 ImageRow
:派生 Widget
trait 会自动生成将 ImageRow
解引用为 View
的代码,并建立相应的 DSL 绑定。
Widget
trait的实现增加这段代码到 app.rs
:
Widget
trait 包含两个方法:
handle_event
方法控制 ImageRow
如何响应事件。目前我们不需要对事件进行自定义处理,因此我们只是将所有事件转发给 view
。draw_walk
方法控制小部件的绘制方式。由于 ImageRow
中的 PortalList
内容必须在 Rust 代码中定义,因此我们确实需要自定义绘制逻辑,所以该方法的实现会稍微复杂一些。下面我们将详细介绍这个实现。View
中绘制每个项目让我们仔细看看 draw_walk
方法:
在 draw_walk
方法内部,我们首先在一个循环中调用 self.view.draw_walk(cx, scope, walk)
来绘制视图中的每个项目。
view
上的 draw_walk
函数是一个所谓的可恢复函数(resumable function)——它的行为类似于迭代器,在绘制过程中逐个生成项目。
每次调用 draw_walk
,它会返回一个特殊的 DrawStep
对象,表示绘制过程的当前状态。然后我们调用该对象的 step
方法,获取下一个应该绘制的项目。调用者(也就是我们)负责绘制每个项目,这使得我们可以自定义它的绘制方式。
当没有更多项目需要绘制时,调用 step
方法会返回 None
,循环结束。
PortalList
draw_walk
方法中以下代码负责绘制 PortalList
(回想一下,在 ImageRow
中,我们使用 PortalList
来列出其项目。)
对于每个要绘制的项目,我们首先调用 as_portal_list
来检查该项目是否是一个 PortalList。一旦获得对 PortalList 的引用:
list.set_item_range(cx, 0, 4)
,告诉列表我们需要 4 个项目。next_visible_item(cx)
来迭代每个可见项目的索引。对于每个索引:
list.item(...)
来获取该索引处项目的引用。item.draw_all(...)
将该项目绘制到屏幕上。每个 PortalList 有自己独立的索引命名空间 —— 这意味着每个列表中每个项目的索引是唯一的。调用 list.item(cx, item_index, live_id!(ImageItem))
会检查给定索引是否已有对应的项目实例。如果没有,它会创建该项目的实例。
那么,调用 list.item(cx, item_index, live_id!(ImageItem))
是如何知道要实例化什么呢?很简单:它使用我们之前在 DSL 中定义的 ImageItem 模板:
注意:
live_id!
宏用于在 Makepad 中生成唯一标识符。在这里,live_id!(ImageItem)
指的是 ImageItem 的模板。
让我们检查一下目前为止的进展。
确保你在你的包目录中,然后运行:
如果一切正常,屏幕上应该会出现一个包含一排占位符图片的窗口:
在这一步中,我们创建了一个 ImageRow
用来显示一排图片。下一步,我们将把多个 ImageRow
组合到一个 ImageGrid
中,以显示一个图片网格。