布局列表框使用指导
可见组件,把「界面设计」里拖入的布局当作行模板:运行时按数据项克隆模板里的组件生成每一行, 滚动时只创建可见行并回收复用,适合显示结构复杂、数量大的列表。
与列表显示框的区别:列表显示框只能在固定布局里显示文本和图像;
布局列表框的行内容完全由你自己的布局决定,行内每个组件(标签、图像、按钮等)都能按数据设置属性。
除了纵向列表,它还内置了现代列表的常用能力:更换数据时只刷新有变化的行(带增删动画)、 网格与横向排列、行间距、数据为空时的空提示文本、滚动到底部触发加载更多、 滚动到指定行——全部开箱即用,不用写一行额外代码。
注意:该组件为新增组件,联机调试需更新到包含它的 AI 伴侣版本,打包 APK 不受影响。
实际案例:中国大学排名及特色(完整 AIA、APK 和专项测试表)
循序渐进实战:从三行商品到搜索、收藏、购物车、聊天与网络分页
快速上手
- 拖一个布局列表框到屏幕;
- 往布局列表框里拖一个竖直布局(这就是行模板),再往里面拖标签、图像、按钮等组件。
模板里组件的组件名(如
标签1)就是行内组件的标签——字典自动绑定、取组件(标签)(GetComponent)都靠它; - 把字典列表设给数据(
Data)属性,键与组件名相同的项会自动填进组件的文本(或图片)属性:
when Screen1.Initialize() {
布局列表框1.Data = list(
{ "标签1": "张三", "标签2": "13800000001" },
{ "标签1": "李四", "标签2": "13800000002" }
)
}
每行的标签1、标签2 已按字典自动填充。需要更灵活的行逻辑时用遍历数据(索引,值)(ForEachData)事件(见使用示例)。
字典自动绑定规则:数据项是字典时,键与模板内组件名相同的项自动写入——组件有文本属性的写文本
(标签、按钮等),没有文本但有图片属性的写图片(图像);类型/type 键只用于多模板路由,
不参与绑定;其余键忽略,需要时在遍历数据事件里用取行数据(GetCurrentData)自行读取;
值会转成文本再写入,某一项写入失败不影响其余项。
实际案例:中国大学排名及特色(下拉自动加载更多)
用真实高校资料做一个可搜索、收藏和切换视图的大学目录,同时专项检查布局列表框的自动绑定、行内按钮、长按、下拉自动加载更多、回收复用与空状态。
| 难度 | 准备时间 | 测试平台 |
|---|---|---|
| 入门,导入工程即可开始 | 约 5 分钟 | Android APK / 包含布局列表框的最新版 AI 伴侣 |
第 1 步:导入完整工程
下载 大学排名与特色 AIA,在项目菜单选择「导入项目(.aia)」。 也可以直接安装 本次工程编译的 Android APK 对照测试。 工程包含完整界面、代码块、20 所高校的数据和 20 张校徽,不需要接口密钥或运行时网络请求。 Web1 只负责解析本地 JSON。
下载文件名应为 UniversityLayoutList.aia,直接导入即可。AIA 内部是 ZIP 容器;如果此前下载成了 .zip,把后缀改回 .aia,不要解压。页面下载链接已指定 .aia 文件名,并携带当前文件版本号。
导入后可立即按这个顺序测试自动加载:
- 首次打开显示首批高校,普通手机一般为 10 所,大屏会多显示几所。
- 手指向上滑动列表,滚到当前最后一所;自动追加下一批,每批最多 5 所,直到显示 20 所并提示「已全部加载」。
- 切换「双列卡片」后继续滚动,自动加载方式相同,已加载进度保留;搜索「北京」时 6 所结果会一次显示全部。
| 准备项 | 数量 | 说明 |
|---|---|---|
| AIA 工程 | 1 | 导入后直接打开 Screen1;不需要额外扩展 |
| Android 手机 | 1 | 伴侣必须包含新增的布局列表框;也可安装上面的 APK |
| universities.json | 1 | 已在素材中,使用文件组件读取 //universities.json |
| 校徽 PNG | 20 | 已在素材中,文件名为 university-01.png 至 university-20.png |
中途验证:设计器中能看到两个布局列表框及各自的行模板,素材列表中能看到 JSON 和校徽。设计器只展示模板,完整 20 条数据在运行时加载。
排名、地区、类型和总分来自 2026 软科中国大学排名主榜,本例取前 20 所。清华和北大的特色文字分别参考 清华本科生教育 与 北大简介;其余学校摘选 教育部 2022 年第二轮“双一流”建设学科名单。特色方向用于展示学校特点,不是 2026 年专业排名。
采集日期为 2026-10-03。逐校信息见 数据来源表,原始资料见 universities.json。详情弹窗也保留了每所大学的来源地址。
第 2 步:了解界面和模板
AIA 已搭好以下组件。需要自己重做时,以 完整界面配置 中的名称和属性为准。
| 组件类型 | 命名 | 关键属性或用途 |
|---|---|---|
| 屏幕 | Screen1 | 可滚动 = 假,尺寸调整 = 响应式 |
| 文本输入框 | 文本输入框_搜索 | 搜索大学名称、地区或特色方向 |
| 复选框 | 复选框_收藏 | 只看收藏 |
| 按钮 | 按钮_视图、按钮_恢复 | 切换图文列表/双列卡片、恢复全部 |
| 按钮 | 按钮_顶部、按钮_刷新、按钮_清空 | 回顶部、强制刷新、清空显示;加载更多由列表触底自动触发 |
| 标签 | 标签_状态、标签_事件 | 显示数量、收藏数及绑定/点击记录 |
| 布局列表框 | 布局列表框_列表 | 宽高填充父级,列数 1,方向纵向,行间距 10 |
| 布局列表框 | 布局列表框_网格 | 宽高填充父级,列数 2,方向纵向,行间距 8,初始不可见 |
| 竖直布局 | 竖直布局_模板_列表、竖直布局_模板_网格 | 分别直接放入对应列表框,宽填充父级、高自动、背景白色 |
| 文件、Web、本地数据库、对话框 | 文件管理器1、Web1、微数据库1、对话框1 | 读取素材、解析 JSON、保存收藏、显示详情 |
图文列表模板结构如下;网格模板把校徽和排名放在顶部,其余文字依次排列。所有网格组件的名称后缀改成 _网格,避免同屏重名。
布局列表框_列表
└─ 竖直布局_模板_列表
├─ 水平布局_图文_列表
│ ├─ 竖直布局_校徽_列表
│ │ ├─ 图像_校徽_列表(64 × 64,缩放图片适应)
│ │ └─ 标签_排名_列表
│ └─ 竖直布局_文字_列表
│ ├─ 标签_名_列表
│ ├─ 标签_地区_列表
│ ├─ 标签_特色_列表
│ └─ 标签_分数_列表
└─ 水平布局_操作_列表
├─ 按钮_详情_列表
└─ 按钮_收藏_列表
模板需要保持可见。运行时隐藏的是另一个列表框,不能把直接子模板隐藏。模板中的两个按钮宽度都设「填充父级」,由水平布局分配剩余宽度;网格行内不要用「宽度 50%」,百分比以屏幕宽度计算,会超出卡片。
列数是设计器属性,所以本例预先准备一列、两列的两个列表框,通过可见性切换。两者使用同一批数据、同一份收藏编号,各自拥有独立模板。
中途验证:连接伴侣或启动 APK 后,先看到清华、北大、浙大等高校,普通手机首批显示 10 所;大屏会根据屏幕高度多显示几所,确保双列卡片也能滚动。状态提示「向上滑动,自动加载更多」。点「双列卡片」后看到同一批高校的两列卡片,点「图文列表」切回。
本例采用下拉自动加载更多:手指向上滑动列表,查看下方内容;到达当前末尾后自动追加下一批。它使用「滚到底部」事件,不是从顶部向下拖动的刷新操作。普通手机首批 10 所,后续每批 5 所;大屏首批增加,筛选结果不足首批数量时直接全部显示。两种视图共享已加载数量,切换后保留进度。
第 3 步:完整代码块
下面是工程实际使用的完整逻辑,可以在预览中查看或复制积木。自行搭建时先按上表创建组件,上传 JSON 和校徽,再粘贴这组代码块;已有同名事件或全局变量时先清理,避免重复。
数据字典中,标签_名_列表、图像_校徽_列表 等键与模板组件名对应,组件自动填写文字或图片。额外的 大学 键保留完整原始记录,点击时直接读取,避免把筛选后的行号误当原排名。
global 大学数据 = []
global 筛选结果 = []
global 收藏编号 = []
global 已加载 = 0
global 绑定次数 = 0
global 正在加载 = false
procedure 更新状态() {
标签_状态.Text = join("已显示 ", 已加载, " / ", length(筛选结果), " 所 · 收藏 ", length(收藏编号), " 所")
if length(筛选结果) > 0 {
标签_状态.Text = join(标签_状态.Text, choose(已加载 < length(筛选结果), " · 向上滑动,自动加载更多", " · 已全部加载"))
}
}
function 制作行(大学) {
return makeDict(pair("大学", 大学), pair("标签_名_列表", dictLookup(大学, "name", "")), pair("标签_名_网格", dictLookup(大学, "name", "")), pair("图像_校徽_列表", dictLookup(大学, "logo", "")), pair("图像_校徽_网格", dictLookup(大学, "logo", "")), pair("标签_排名_列表", join("#", dictLookup(大学, "rank", 0))), pair("标签_排名_网格", join("#", dictLookup(大学, "rank", 0))), pair("标签_地区_列表", join(dictLookup(大学, "region", ""), " · ", dictLookup(大学, "category", ""))), pair("标签_地区_网格", join(dictLookup(大学, "region", ""), " · ", dictLookup(大学, "category", ""))), pair("标签_特色_列表", dictLookup(大学, "feature", "")), pair("标签_特色_网格", dictLookup(大学, "feature", "")), pair("标签_分数_列表", join("2026主榜 · 总分 ", dictLookup(大学, "score", 0))), pair("标签_分数_网格", join("2026主榜 · 总分 ", dictLookup(大学, "score", 0))))
}
procedure 筛选大学() {
筛选结果 = []
var 关键词 = trim(文本输入框_搜索.Text)
foreach 大学 in 大学数据 {
var 命中 = 关键词 == "" or textContains(join(dictLookup(大学, "name", ""), " ", dictLookup(大学, "region", ""), " ", dictLookup(大学, "feature", "")), 关键词)
var 满足收藏 = not (复选框_收藏.Checked) or contains(收藏编号, dictLookup(大学, "id", ""))
if 命中 and 满足收藏 {
listAdd(筛选结果, 大学)
}
}
// 首批兼顾两列卡片与大屏;每张卡片约 180 高,确保有足够内容可滚动。
var 首批数量 = max(10, 2 * ceil(Screen1.Height / 180))
已加载 = min(首批数量, length(筛选结果))
var 行列表 = []
for 行号 = 1 to 已加载 {
listAdd(行列表, 制作行(筛选结果[行号]))
}
布局列表框_列表.Data = 行列表
布局列表框_网格.Data = 行列表
更新状态()
}
procedure 加载下一页() {
if not (正在加载) and 已加载 > 0 and 已加载 < length(筛选结果) {
正在加载 = true
var 下一批 = []
var 结束位置 = min(已加载 + 5, length(筛选结果))
for 行号 = 已加载 + 1 to 结束位置 {
listAdd(下一批, 制作行(筛选结果[行号]))
}
已加载 = 结束位置
布局列表框_列表.AppendData(下一批)
布局列表框_网格.AppendData(下一批)
更新状态()
正在加载 = false
}
}
procedure 显示详情(行值, 当前索引) {
var 大学 = dictLookup(行值, "大学", makeDict())
标签_事件.Text = join("点击结果第", 当前索引, "行:", dictLookup(大学, "name", ""))
对话框1.ShowMessageDialog(join("2026软科主榜第 ", dictLookup(大学, "rank", 0), " 名,总分 ", dictLookup(大学, "score", 0), "\n地区:", dictLookup(大学, "region", ""), "\n特色方向:", dictLookup(大学, "feature", ""), "\n特色来源:", dictLookup(大学, "featureNote", ""), "\n\n排名:", dictLookup(大学, "rankSource", ""), "\n特色:", dictLookup(大学, "featureSource", "")), dictLookup(大学, "name", "大学详情"), "知道了")
}
procedure 切换收藏(行值) {
var 大学 = dictLookup(行值, "大学", makeDict())
var 编号 = dictLookup(大学, "id", "")
if contains(收藏编号, 编号) {
listRemove(收藏编号, indexOf(收藏编号, 编号))
} else {
listAdd(收藏编号, 编号)
}
微数据库1.StoreValue("university-favorites-v1", 收藏编号)
if 复选框_收藏.Checked {
筛选大学()
} else {
布局列表框_列表.Refresh()
布局列表框_网格.Refresh()
更新状态()
}
}
procedure 绑定当前行(列表组件, 后缀, 行值) {
var 大学 = dictLookup(行值, "大学", makeDict())
var 排名组件 = any LayoutListView(列表组件).GetComponent(join("标签_排名_", 后缀))
var 收藏组件 = any LayoutListView(列表组件).GetComponent(join("按钮_收藏_", 后缀))
var 已收藏 = contains(收藏编号, dictLookup(大学, "id", ""))
any Label(排名组件).TextColor = choose(dictLookup(大学, "rank", 0) <= 3, colorFromHex("#C26B1B"), colorFromHex("#36547A"))
any Button(收藏组件).Text = choose(已收藏, "已收藏", "收藏")
any Button(收藏组件).BackgroundColor = choose(已收藏, colorFromHex("#FFE2B4"), colorFromHex("#E8EEF8"))
绑定次数 = 绑定次数 + 1
标签_事件.Text = join("累计绑定 ", 绑定次数, " 次 · 当前:", dictLookup(大学, "name", ""))
}
when Screen1.Initialize() {
收藏编号 = 微数据库1.GetValue("university-favorites-v1", [])
文件管理器1.ReadFrom("//universities.json")
}
when 文件管理器1.GotText(文本) {
大学数据 = Web1.JsonTextDecodeWithDictionaries(文本)
筛选大学()
}
when 文本输入框_搜索.TextChanged() { 筛选大学() }
when 复选框_收藏.Changed() { 筛选大学() }
when 按钮_恢复.Click() {
文本输入框_搜索.Text = ""
复选框_收藏.Checked = false
筛选大学()
}
when 按钮_视图.Click() {
var 切到网格 = 布局列表框_列表.Visible
布局列表框_列表.Visible = not (切到网格)
布局列表框_网格.Visible = 切到网格
按钮_视图.Text = choose(切到网格, "图文列表", "双列卡片")
}
when 按钮_顶部.Click() {
if 已加载 > 0 {
if 布局列表框_列表.Visible { 布局列表框_列表.ScrollToIndex(1) }
else { 布局列表框_网格.ScrollToIndex(1) }
}
}
when 按钮_刷新.Click() {
布局列表框_列表.Refresh()
布局列表框_网格.Refresh()
}
when 按钮_清空.Click() {
已加载 = 0
布局列表框_列表.ClearData()
布局列表框_网格.ClearData()
标签_状态.Text = "已清空显示;点恢复全部可重新显示,收藏保留。"
}
when 布局列表框_列表.ReachEnd() {
if 布局列表框_列表.Visible { 加载下一页() }
}
when 布局列表框_网格.ReachEnd() {
if 布局列表框_网格.Visible { 加载下一页() }
}
when 布局列表框_列表.ForEachData(索引, 值) { 绑定当前行(布局列表框_列表, "列表", 值) }
when 布局列表框_网格.ForEachData(索引, 值) { 绑定当前行(布局列表框_网格, "网格", 值) }
when 布局列表框_列表.ItemClick(索引, 值) { 显示详情(值, 索引) }
when 布局列表框_网格.ItemClick(索引, 值) { 显示详情(值, 索引) }
when 布局列表框_列表.ItemLongClick(索引, 值) { 切换收藏(值) }
when 布局列表框_网格.ItemLongClick(索引, 值) { 切换收藏(值) }
when 布局列表框_列表.ComponentClick(标签, 索引, 值) {
if 标签 == "按钮_收藏_列表" { 切换收藏(值) }
else { 显示详情(值, 索引) }
}
when 布局列表框_网格.ComponentClick(标签, 索引, 值) {
if 标签 == "按钮_收藏_网格" { 切换收藏(值) }
else { 显示详情(值, 索引) }
}
when Screen1.ErrorOccurred(组件, 函数名称, 错误编号, 消息) {
标签_状态.Text = join("错误 ", 错误编号, ":", 消息)
}
中途验证:输入「口腔医学」应只剩武汉大学和四川大学;点「查看特色」能看到该校排名、总分和来源。点「收藏」后按钮变成「已收藏」;滚走再滚回,收藏样式仍对应正确的大学。
收藏使用固定学校编号保存到本地数据库,不使用列表行号。筛选改变行顺序后不会把收藏移到别的学校。行内按钮通过列表框的「组件点击」事件处理,不能只给设计器里的模板按钮写普通点击事件。每次「遍历数据」都重新设置收藏文字、背景和排名颜色,防止行回收后串色。
第 4 步:按表专项测试
建议先安装 APK 验证一遍,再用同一工程连接 AI 伴侣对照。下表是预期结果,真机与伴侣尚未代替用户完成实测。
| 操作 | 预期结果 | 覆盖能力 |
|---|---|---|
| 首次打开 | 普通手机首批 10 所,大屏会多显示几所;校徽、名称、排名、地区、特色和总分完整 | 读取素材、字典自动绑定 |
| 向上滑动列表,滚到当前最后一所 | 自动追加 5 所;普通手机数量从 10/20 到 15/20,再到 20/20,提示「已全部加载」 | ReachEnd、AppendData、分页边界 |
| 恢复全部,切到双列卡片,再向上滑到末尾 | 自动追加,每批最多 5 所,两种视图数据同步;达到 20 所后继续滑动不重复追加 | 双列触底自动加载、无重复、隐藏视图不触发加载 |
| 搜索「北京」 | 匹配 6 所,一次显示全部,提示「已全部加载」,无需手动加载 | 筛选结果不足首批数量时全部显示 |
| 搜索「上海」 | 上海交通大学、复旦大学、同济大学,共 3 所 | 短列表 |
| 搜索「湖北」 | 武汉大学、华中科技大学,共 2 所 | 地区筛选 |
| 搜索「口腔医学」 | 武汉大学、四川大学,共 2 所,原排名分别为 8、16 | 特色筛选、当前行号与排名区分 |
| 搜索「不存在的大学XYZ」 | 显示空提示;不追加数据,恢复后数据重新出现 | 空列表及恢复 |
| 点行空白/名称/查看特色 | 显示被点大学的详情;结果索引按当前筛选计算 | ItemClick、ComponentClick |
| 点收藏按钮,或长按行空白 | 收藏状态切换;详情不会代替收藏操作 | 子组件点击、ItemLongClick |
| 只收藏武汉和川大,勾「只看收藏」 | 只显示这两所;取消一个收藏后该行消失 | 外部状态刷新、重新筛选 |
| 取消收藏筛选,反复上下滚动 | 各学校的收藏文字、背景和前 3 名颜色正确,不串行 | 克隆行回收复用 |
| 切换图文列表/双列卡片 | 两种视图大学数据和收藏一致,卡片内容不越界 | 网格、独立模板、显隐切换 |
| 点「强制刷新」 | 当前可见行重新绑定,绑定计数增加,收藏不丢失 | Refresh;绑定次数不等于学校数量 |
| 加载全部后滚到末尾,点「回顶部」 | 当前可见视图回到第一行 | ScrollToIndex;索引从 1 开始 |
| 点「清空显示」,再点「恢复全部」 | 清空提示出现,再恢复首批数据,收藏保留 | ClearData、重新设置 Data |
| 关闭后重新打开 APK | 收藏仍保留,首次数据重新加载 | 本地数据库持久化 |
| 分别在两个视图横竖屏切换 | 列表可继续滚动,内容不消失,收藏与详情仍正确 | 尺寸变化、克隆布局重新测量 |
AI 伴侣里的多个工程可能共用本地数据库,收藏测试建议以 APK 重启结果为准;需要清除本例收藏时,逐个取消收藏或清除本例 APK 的应用数据。
常见问题
| 现象 | 检查与处理 |
|---|---|
| 伴侣报未知组件,或者整片列表空白 | 更新到包含布局列表框的伴侣;用本页 APK 对照。工程导入成功不代表旧伴侣支持新组件 |
| 状态一直是「正在读取本地数据」 | 查看错误标签;确认素材有 universities.json,文件名大小写一致,读取路径有 // 前缀 |
| 数据条数正常,但所有行都空白 | 确认行布局是列表框的直接子组件且可见;不能把模板放在列表框外面 |
| 文字正常,校徽不显示 | 检查 JSON 中的 logo 与素材文件名一致,素材确实导入;本例使用本地图片文件名,无需下载 |
| 只有一行、列表高度不对或不能滚动 | 屏幕关闭可滚动,列表高度填充父级;嵌入可滚动屏幕时改为固定高度 |
| 网格的按钮超出卡片 | 行内按钮宽度用填充父级;不要用以屏幕宽度计算的百分比 |
| 收藏滚动后跑到别的学校 | 每次绑定重新设置状态,收藏依据固定 id;不要保存当前行索引或克隆组件对象 |
| 模板按钮有点击事件,运行时没反应 | 用列表框的组件点击事件,按标签区分收藏和详情;模板原组件不是每一行的克隆组件 |
| 短列表不能滚动 | 筛选结果少于首批数量时已经全部显示,这是正常现象;首批按两列卡片和屏幕高度计算,不会把少量结果留在下一页 |
| 清空后再搜索又出现大学 | 清空显示只清列表,本例保留原始数据;搜索和恢复会重新生成筛选结果 |
工程校验与下一步
本次交付的最终 AIA 已通过归档结构检查、正式编辑器 528 个积木完整加载及完整 YAIL 生成、真实后台导入、Android APK 编译。 正式编辑器首次加载及保存后重开均为 0 个错误,已逐个检查 124 个变量引用的下拉框内部值,事件参数显示中文、保存原生标识,导入后无需重新选择参数。最终生成链 10 项回归也必须全部通过,校验报告包含编译器、转换器、预览、技能、参数背景、参数内部值与文档变量绑定的逐项结果。下载 SHA-256 对应的校验记录。浏览器人工导入、AI 伴侣联机、真机运行和鸿蒙 HAP 尚未验证。
需要扩充学校时,先更新 JSON 记录和对应校徽,保持 id 唯一、图片文件名与素材一致;然后继续学习多模板和嵌套列表。修改或重新生成 AIA 后需重新完成最终归档校验。
使用示例
手动设置行组件
每行每次绑定都会触发遍历数据(索引,值)(ForEachData)。事件内用取组件(标签)(GetComponent)
按标签拿到真实克隆组件:
when 布局列表框1.ForEachData(索引, 值) {
var 标题组件 = 布局列表框1.GetComponent("标签1")
var 行数据 = 布局列表框1.GetCurrentData()
any Label(标题组件).Text = join(行数据)
}
拿到组件后,用「任意组件」分类的积木(如「任意 标签 的 文本 属性」)设置属性,
把 标题组件 变量接到任意组件积木的组件参数上即可。
要点:
- 行被回收复用时会先按模板初始值复位,再绑定新数据——所以在事件里改过的属性每次绑定都要重新设置 (字典自动绑定也是每次都执行的);
- 多模板时只能取到当前行所用模板里的组件。
事件处理
when 布局列表框1.ItemClick(索引, 值) {
标签_详情.Text = 值
}
when 布局列表框1.ComponentClick(标签, 索引, 值) {
if 标签 = "按钮_删除" {
对话框1.ShowAlert(join("删除第 ", 索引, " 行"))
}
}
追加数据与滚动
when 按钮_加载更多.Click() {
布局列表框1.AppendData(list({ "标签1": "新行 1" }, { "标签1": "新行 2" }))
}
when 按钮_回顶部.Click() {
布局列表框1.ScrollToIndex(1)
}
when 按钮_看最新.Click() {
布局列表框1.ScrollToIndex(length(布局列表框1.Data))
}
(length 是列表长度积木——数据有几条就滚到第几行;也可以直接给固定行号。)
滚到底部自动加载更多
不需要「加载更多」按钮——列表滚到底部会触发滚到底部(ReachEnd)事件,
在事件里请求下一页并追加数据(值)(AppendData)即可:
global 页码 = 1
when Screen1.Initialize() {
布局列表框1.Data = list({ "标签1": "第 1 页数据" })
}
when 布局列表框1.ReachEnd() {
页码 = 页码 + 1
Web1.Url = join("https://example.com/api/list?page=", 页码)
Web1.Get()
}
when Web1.GotText(URL网址, 响应代码, 响应类型, 响应内容) {
var 结果 = Web1.JsonTextDecode(响应内容)
布局列表框1.AppendData(dictLookup(结果, "rows"))
}
要点:
- 追加后新行出现在底部,继续上滑会再次触发,翻页自动连续;
- 数据不足一屏时不触发——第一页至少要铺满一屏,加载更多才会接力;
- 没有更多数据时可以在事件里判断并停止请求(例如记一个「是否还有更多」全局变量)。
智能刷新与强制刷新
设置数据(Data)时自动按新旧差异刷新:只重新绑定有变化的行,
未变化的行不触发遍历数据(索引,值)(ForEachData),删除、移动带动画:
when 按钮_只看未读.Click() {
// 重新设整个列表:与旧数据相同的行不重绑,不同的行自动增删动画
布局列表框1.Data = list({ "标签1": "张三", "标签2": "2 条未读" })
}
两种情况要改用刷新(Refresh)强制全部行重新绑定:
- 行内显示依赖列表之外的全局状态(如夜间模式、选中项),改了状态要立刻反映到所有行;
- 你原地修改了某个字典的内容后,把同一个列表对象再设给数据属性。
global 夜间模式 = false
when 开关_夜间模式.Changed() {
夜间模式 = 开关_夜间模式.On
布局列表框1.Refresh()
}
空状态
数据为空时列表中央自动显示提示(默认「暂无数据」)。提示文字可以按场景改写, 也可以在代码块里读回:
when Screen1.Initialize() {
布局列表框1.EmptyText = "还没有订单,下单后显示在这里"
}
when 布局列表框1.ItemClick(索引, 值) {
标签_详情.Text = 值
}
设为空文本 "" 则不显示任何提示。搜索无结果时改提示、查到数据恢复,都不用手动管理显隐:
when 按钮_搜索.Click() {
布局列表框1.ClearData()
布局列表框1.EmptyText = join("没有找到「", 文本输入框_关键词.Text, "」相关内容")
}
(把你的查找结果设给数据(Data)即可:结果为空列表时提示自动显示,
有结果时自动隐藏。)
多模板
拖入布局列表框的直接子布局按拖入顺序作为类型 1、2、…、n 模板。例如先拖一个「左图右文」的 水平布局、再拖一个纯文本的竖直布局,就是类型 1 和类型 2:
when Screen1.Initialize() {
布局列表框1.Data = list(
{ "类型": 1, "标签1": "张三", "标签2": "在吗?" },
{ "类型": 2, "标签3": "系统通知", "标签4": "10:30" }
)
}
同一屏幕内组件名不能重复,所以每个模板里的组件要各自命名。上面的键名就是组件名:
类型 1 的水平布局里放 标签1、标签2,类型 2 的竖直布局里放 标签3、标签4,
数据里各写各的键——路由到哪个模板,就按哪个模板的组件名绑定。
路由规则:
- 数据项为字典时,用
类型(优先)或type键选模板;值接受数字或可转数字的文本; - 没有该键、值不是数字时都用类型 1;超出模板数时夹取到第一个/最后一个模板;
- 非字典数据项(文本、数字)一律用类型 1;
- 只有拖进布局列表框本身的直接子布局才算模板;拖进模板布局里的子组件属于模板内容。
网格、横向与行间距
列数(Columns)、方向(Orientation)、
行间距(Spacing)、从底部开始(StackFromEnd)是四个
只在设计器属性面板里设置的布局属性(没有对应代码块),常用的组合:
| 场景 | 设置 |
|---|---|
| 商品墙 / 相册 | 列数 = 2 或 3,行间距 = 8 |
| 横滑卡片(如头像栏) | 排列方向 = 横向,行间距 = 8 |
| 聊天窗口 | 从底部开始 = 真,行间距 = 4,配合追加数据(值)(AppendData) |
网格模式下每列宽度自动均分;行间距只在行与行、列与列之间生效,列表四周与屏幕对齐不留边。
滚到底部(ReachEnd)在横向列表里对应滚到最右侧。
聊天窗口的典型写法(设计器勾选「从底部开始」后):
when 按钮_发送.Click() {
布局列表框1.AppendData(list({ "类型": 2, "标签2": 文本输入框_消息.Text }))
文本输入框_消息.Text = ""
}
when Screen1.Initialize() {
布局列表框1.Data = list({ "类型": 1, "标签1": "你好,请问有什么可以帮你?" })
}
(配合多模板:对方气泡模板里放 标签1、自己气泡模板里放
标签2(两个模板组件不能同名),数据里按类型各写各的键;
新消息追加后视图自动停在最底部。)
嵌套:列表套列表
布局列表框可以拖进另一个布局列表框当模板组件。外层克隆行时会把内层连同它自己的模板一起 完整克隆——内层是一个真正能显示数据的列表,典型用法是「纵向首页 + 行内横滑栏目」。
嵌套时的规则:
- 内层的列数(
Columns)、方向(Orientation)、 行间距(Spacing)等设计器属性随克隆一并带过去;内层设计时设好的数据也会 按克隆那一刻的值带过去(与限制第 3 条的快照语义相同); - 要每行的内层显示不同数据:在外层的遍历数据(索引,值)(
ForEachData)事件里用 取组件(标签)(GetComponent)拿到内层克隆,再用 「任意组件」分类的积木(如「任意 布局列表框 的 数据」)设置—— 行被回收时会先复位到快照数据,所以每次绑定都要重新设置:
when 布局列表框1.ForEachData(索引, 值) {
var 内层列表 = 布局列表框1.GetComponent("布局列表框2")
any LayoutListView(内层列表).Data = 值
}
(拿到 内层列表 变量后,把它接到「任意 布局列表框」的属性/方法积木上即可,写法与
手动设置行组件相同。)
- 内层的事件接不到同名处理块:克隆出来的内层不是设计器里那个组件,
when 布局列表框2.行点击这样的块不会触发,只能用「任意组件」分类的 「任意 布局列表框 的 … 事件」接收(此时每一行的内层都会触发,注意区分来源)—— 相关逻辑尽量写在外层事件里(外层是真实组件,事件正常); - 方向:纵套横(外层纵向、内层方向(
Orientation))方向不同、互不干扰, 是首页横滑栏目的标准做法;纵套纵时手势会先滚动内层、滚到内层边缘才带动外层, 若内层只是展示少量数据,给它固定高度、数据控制在一屏内即可; - 内层高度必须设固定值(不要用自动/填充父级,嵌套布局里测量结果不可预期);
- 别套太多层:外层每建一行都要克隆整棵内层列表(含内层的全部模板),行数多时开销明显。
完整案例:校园资讯流
朋友圈 / 资讯类 app 的完整骨架:置顶公告与图文帖混排(多模板)、点赞只刷新被点的那一行 (智能刷新)、滚到底部自动加载下一页。照着搭完,得到的是一个能交互、能继续扩展的真实页面, 而不是孤立的积木演示。
第 1 步:搭界面
- 拖一个布局列表框到屏幕,在属性面板把高度设为固定值(如
500)—— 可滚动的屏幕上不要用填充父级(见限制); - 拖一个水平布局进布局列表框内部(这就是类型 1「图文帖」模板):
- 图像改名为
图像_头像,宽、高各48; - 竖直布局放进水平布局,里面放标签
标签_昵称(初始文本「昵称」)和 标签标签_内容(初始文本「内容预览」,让它有高度); - 按钮改名为
赞,初始文本0;
- 图像改名为
- 再拖一个竖直布局进布局列表框(类型 2「置顶公告」模板):标签
标签_置顶(初始文本「置顶」)、标签标签_公告内容; - 屏幕底部放标签
标签_状态,文本清空(用来显示交互结果)。
模板里的初始文本只是设计时能看清行的大致样子,运行时会被数据自动覆盖;
图像_头像 找不到同名素材时只是显示空白,不影响其它部分。
第 2 步:初始数据,先看到列表
global 资讯数据 = list(
{ "类型": 2, "标签_置顶": "置顶", "标签_公告内容": "本周六 14:00 操场「百团大战」,60 个社团现场招新" },
{ "类型": 1, "图像_头像": "avatar1.png", "标签_昵称": "篮球社",
"标签_内容": "今晚 7 点体育馆三对三,缺一个人,速来!", "赞": 12 },
{ "类型": 1, "图像_头像": "avatar2.png", "标签_昵称": "学生会",
"标签_内容": "宿舍文化节开始报名,优秀宿舍奖励 500 度电", "赞": 45 }
)
when Screen1.Initialize() {
布局列表框1.Data = 资讯数据
}
跑起来:第一条走类型 2 模板(置顶公告),后两条走类型 1(头像 + 昵称 + 内容),
「赞」按钮直接显示 12 和 45——绑定代码一行都没写,全是字典键名对上组件名的
自动绑定。
第 3 步:点赞——只刷新被点的那一行
when 布局列表框1.ComponentClick(标签, 索引, 值) {
if 标签 = "赞" {
var 新数据 = dictCopy(值)
dictSet(新数据, "赞", dictLookup(值, "赞") + 1)
资讯数据[索引] = 新数据
布局列表框1.Data = 资讯数据
标签_状态.Text = join("第 ", 索引, " 行被点赞,只有这一行在刷新")
}
}
when 布局列表框1.ItemClick(索引, 值) {
标签_状态.Text = join("点击了第 ", 索引, " 行")
}
跑起来:点一下「12」变「13」,带动画,其它行纹丝不动——改一条数据就只重绑这一行 (见智能刷新);点行的空白处则显示点的是第几行。
注意这里的写法是复制一份、改副本、再把副本换回该行,而不是直接改 值:
字典设值(dictSet)是原地修改,原地改完不换回列表,新旧列表内容完全相同,
智能刷新会认为什么都没变而整表全刷。先 dictCopy 复制、改完把新字典赋给
资讯数据[索引],该行才会真正「有变化」,从而只重绑这一行。
第 4 步:滚到底部自动加载
global 页码 = 1
when 布局列表框1.ReachEnd() {
if 页码 < 3 {
页码 = 页码 + 1
布局列表框1.AppendData(list(
{ "类型": 1, "图像_头像": "avatar1.png", "标签_昵称": join("校园号 ", 页码),
"标签_内容": join("第 ", 页码, " 页加载的资讯内容……"), "赞": 0 }
))
} else {
标签_状态.Text = "没有更多了"
}
}
跑起来:列表滚到底自动多出一条,连续滚连续加载,加载到第 3 页后提示「没有更多了」。
真实项目里把 AppendData 换成 Web1.Get() 请求接口、在 Web1.GotText 事件里解析并追加即可
(写法见上文滚到底部(ReachEnd))。
常见问题
| 现象 | 检查 |
|---|---|
| 列表空白、行不显示 | 数据项的键名与模板组件名一个字都不能差;类型 值要在模板数量范围内 |
| 点赞没反应 | 组件点击(标签,索引,值)(ComponentClick)里的标签是组件名——按钮必须真的叫 赞;每行数据都要带 赞 键 |
| 滚到底不触发 | 首屏数据要铺满一屏(内容不足一屏时不触发);高度别用填充父级 |
点赞数字变成 01 这类 |
该行数据缺 赞 键,dictLookup 返回空文本,+1 变成了文本拼接——补上键就好 |
限制与注意事项
- 模板里不要放重型/独占资源组件:地图、相机、视频播放器、Web浏览器、各类选择器 (拨号、联系人、文件等)——克隆会产生多份实例,资源独占与结果回调都无法对应。 推荐用标签、图像、按钮、复选框、开关和各种布局搭模板。
- 滚动屏幕 + 高度=填充父级会显示异常(与
列表显示框相同): 可滚动屏幕上请给固定高度。 - 运行期改模板组件的属性不会同步到已建行:模板快照在首次克隆时建立;改了模板后, 清空数据再重新设置即可。
- 取组件(标签)(
GetComponent)、取行数据(GetCurrentData)只在 遍历数据与三类点击事件内有效,返回的组件不要长期保存(见方法说明)。 - 性能:只创建可见行 + 少量回收行,行数与数据总量无关;万条数据滚动内存平稳; 遍历数据事件在滚动中高频触发,事件体尽量轻(属性设置没问题,网络请求别放这里)。
- 行内逻辑依赖外部状态时要主动刷新:智能刷新对内容相同的行不重新绑定(这是性能优势),
也就不会重新执行这些行的遍历数据事件——改了全局状态后调一次
刷新(
Refresh)即可(见智能刷新与强制刷新)。 - 列数 / 排列方向 / 从底部开始 / 行间距是设计期属性:在设计器里改,代码块里没有对应 积木;运行中不可切换。
完整 .aia 源码与操作指导
沿用中国大学排名案例的完整业务流程:真实大学资料、搜索、收藏、详情和自动加载更多。显示参数收在“列表显示设置”中,默认主界面直接查看大学。
大学列表收藏与分页完整案例
导入方法:项目 → 导入项目(.aia)→ 选择下载的 .aia 源码文件。 浏览真实大学资料,搜索、收藏、查看详情并触底自动加载更多;空关键词显示全部大学。尺寸和颜色设置默认收起。 覆盖检查 38/38,正式编辑器首次载入和保存重开均零错误,后台导入与安装包编译通过。真机/伴侣尚未实测。
先运行这个实际案例
- 打开即显示大学排名,不输入关键词也能浏览。
- 搜索、收藏、点击大学查看详情,滑到底自动加载下一批。
- 需要调整尺寸和颜色时,再打开“列表显示设置”。
高级面板默认收起,原完整功能入口仍可打开使用。源码为中文app.json和Screen1.ai-blocks,AIA包含全部素材和完整积木。正式编辑器、后台导入及安装包检查以更新后的最终归档报告为准;真机/伴侣尚未实测。
| API / 功能 | 操作入口 | 预期结果 |
|---|---|---|
| Data读取与设置、AppendData、ReachEnd | 初始真实资料读取;搜索;列表向上滑到底;横向卡片滑到末端 | 实际首批数据与后续批次;无网络请求,不伪造加载成功;到底事件驱动还有数据才追加 |
| GetComponent、ForEachData、GetCurrentData | 实际行绑定过程 | 按真实模板标签取排名和收藏按钮,依据真实行值染色;仅绑定回调读当前行上下文 |
| ItemClick、ItemLongClick、ComponentClick | 点击大学行、长按及收藏按钮 | 详情使用真实大学数据;收藏切换并存独立TinyDB;模板按钮标签正确 |
| Refresh、ScrollToIndex、ClearData | 刷新、顶部、清空、恢复全部 | 真实刷新绑定与回到顶部;清空仅显示数据,原资料和收藏保留;恢复重新筛选 |
| BackgroundColor、EmptyText | 完整主题按钮后搜索无匹配,再恢复 | 真实背景与空提示读回;恢复后大学继续显示 |
| Visible、Left、Top、Width、Height、WidthPercent、HeightPercent | 视图切换、列表像素、百分比及数据与属性 | 列表真实位置尺寸和可见性;控制区独立可用 |
| Columns、Orientation、Spacing、StackFromEnd设计器属性 | 单列竖向列表和横向双行卡片切换 | 设计器中各项真实配置;这些属性只有设计器入口,不编造运行时设置块 |
| 搜索、仅收藏、空数据、错误恢复 | 不输入关键词、输入不存在词、取消筛选、查看错误 | 空关键词显示真实大学;无匹配显示空提示;底部实际错误信息,不用预置结果伪造绑定 |
横向卡片采用Orientation=1、Columns=2与StackFromEnd=True,与原大学案例的竖向双列网格不同。组件设计器方向与行列数需在导入工程中修改后重载,界面不提供不存在的属性设置块。排名及特色素材与原大学案例完全一致,引用方式和适用范围见资料来源。
扫码添加客服咨询