HarmonyOS ArkUI Grid + ArkTS:折叠屏图集筛选状态跨布局保持【鸿蒙心迹】
李游
折叠屏打开前,用户在相册里勾上“旅行”和“仅收藏”,眼前还有两张照片;合上屏幕后,界面从四列改成两列,筛选按钮却回到了“全部”。看起来只是少了一次筛选,实际上用户会把错误的图片带到后续分享流程。这里需要守住的不是某个组件,而是筛选条件的业务含义。本文用一个可独立运行逻辑的图集演示来拆开布局变化与筛选状态,不依赖设备型号,也不把布局自适应误当作持久化。

一、不要让布局决定数据的生死
示例项目叫 FilterLedger,任务编号 FOLD-2606。准备六张模拟相片:P-101山湖、P-102樱花、P-103群山、P-104夜景、P-105海岸、P-106街景。前三个标签分别为旅行、人像、旅行,后三个标签依次为城市、旅行、城市。收藏项设置为P-101、P-102与P-105。选择“旅行”并开启“仅收藏”,可见结果应恒为P-101和P-105。这是整个Demo的断言,不因为屏幕宽窄而改变。
业务状态包括当前标签、收藏开关和数据集;视图状态包括宽窄模式、网格列数和每张卡片宽度。两个状态域可以互相触发重绘,却不能互相覆盖。换句话说,折叠只允许改变列数,不能顺手调用清空筛选的初始化逻辑。否则在短暂的视图重建或二次布局中,用户操作的意图就丢失了。
计划对宽度约920vp的展开状态显示四列,对约560vp的折叠状态显示两列。这里的640vp只是本Demo用于比较的设计阈值,不代表HarmonyOS官方规定的折叠屏断点;项目实际阈值应根据窗口内容宽度、字体缩放和卡片最小可读宽度调整。选用容器尺寸而不是物理屏幕型号,也是为了兼容分屏、悬浮窗口和多窗口场景。
二、把筛选写成没有界面依赖的纯函数
第一段代码解决数据被重新过滤时顺序、标签和收藏状态容易混杂的问题。用不可变条目描述每张图片,过滤函数只读入条件并返回新数组;它不写UI状态,也不会对原始数据做原地排序。
interface PhotoItem {
id: string
name: string
tag: string
favorite: boolean
}
class FilterLedger {
static readonly photos: PhotoItem[] = [
{ id: 'P-101', name: '山湖', tag: '旅行', favorite: true },
{ id: 'P-102', name: '樱花', tag: '人像', favorite: true },
{ id: 'P-103', name: '群山', tag: '旅行', favorite: false },
{ id: 'P-104', name: '夜景', tag: '城市', favorite: false },
{ id: 'P-105', name: '海岸', tag: '旅行', favorite: true },
{ id: 'P-106', name: '街景', tag: '城市', favorite: false }
]
static visible(tag: string, favoriteOnly: boolean): PhotoItem[] {
return FilterLedger.photos.filter((p: PhotoItem) =>
(tag === '全部' || p.tag === tag) && (!favoriteOnly || p.favorite))
}
}
PhotoItem.id是后续展示和日志共用的业务主键。名称与标签只是字段,不应该靠数组下标判断收藏;数据源如果重排,下标会变化而图片身份不会变化。visible返回新数组,内存由普通对象的垃圾回收管理,不持有外部文件句柄。这里把样本作为只读约定,真实服务端更新时应替换数据副本并触发状态更新,不能在原数组里偷偷修改单项后指望界面必然察觉。
一次筛选求值可得到两个ID,展开/折叠都重新调用同一个过滤入口。列表为空时应显示“当前条件下无照片”,而不是自动选择全部标签。后者容易被误解成用户改了筛选。标签值来自受控枚举或白名单;对于服务端临时提供的新标签,应先更新可选条件,再决定如何呈现未知分类。
三、Grid只负责列数,@State持有业务选择
第二段代码把纯函数接到ArkUI界面。这里使用Grid、GridItem、ForEach和@State,在容器宽度变化时修改compact,不触碰tag与favoriteOnly。按钮允许切换条件,页面重绘时再次从业务条件推导可见条目。
@Entry
@Component
struct FilterPage {
@State tag: string = '旅行'
@State favoriteOnly: boolean = true
@State compact: boolean = false
build() {
Column({ space: 12 }) {
Text('FilterLedger · FOLD-2606').fontSize(22)
Row({ space: 8 }) {
Button(this.tag).onClick(() => {
this.tag = this.tag === '旅行' ? '全部' : '旅行'
})
Button(this.favoriteOnly ? '仅收藏:开' : '仅收藏:关')
.onClick(() => { this.favoriteOnly = !this.favoriteOnly })
}
Text(`结果 ${FilterLedger.visible(this.tag, this.favoriteOnly).length} 张`)
Grid() {
ForEach(FilterLedger.visible(this.tag, this.favoriteOnly),
(photo: PhotoItem) => {
GridItem() {
Column() {
Text(photo.name).fontSize(18)
Text(photo.id).fontSize(13)
}.padding(12).backgroundColor('#EAF6F3')
}
}, (photo: PhotoItem) => photo.id)
}
.columnsTemplate(this.compact ? '1fr 1fr' : '1fr 1fr 1fr 1fr')
.columnsGap(8).rowsGap(8).height('65%')
Text(this.compact ? 'COMPACT · 2列' : 'EXPANDED · 4列')
}
.width('100%').padding(16)
.onAreaChange((_oldArea, newArea) => {
this.compact = Number(newArea.width) < 640
})
}
}
Grid负责内容栅格,@State让按钮和列数更新触发界面刷新。这里对newArea.width做数值转换,是为了按该属性的长度字符串表现形式取出宽度;正式项目还应确认实际单位、窗口缩放和组件可用尺寸。ForEach使用稳定的photo.id作为键,减少重排时错把一张图片的组件身份借给另一张。代码没有数据库和外部订阅,因此页面销毁时没有定时器需要清理,但页面从导航栈退出后内部@State不保证保留;如需跨进程恢复,必须另做持久化设计。若用户正处于键盘输入或筛选弹层状态,折叠过程还可能使焦点重新分配;建议测试焦点回落位置及可访问性朗读顺序,而不是仅比较卡片是否还在。列数正确也不等于交互完整,尤其需要验证宽态左侧筛选区变成窄态顶部弹层以后,确认按钮是否仍指向同一业务状态。

布局回调可能在一次视图构建期间触发多次。把compact限制为布尔值,只有越过阈值后才会发生实际状态变化,避免宽度的微小抖动导致每个像素都更新Grid模板。如果阈值恰在宽度震荡范围内,可加入迟滞区间,例如低于620vp进窄态、高于660vp恢复宽态,而不是把屏幕方向作为唯一依据。
四、检查用例要比“展开看一眼”多一步
第三段代码只验证纯业务筛选,不会伪装成设备实测。可以将这段检查放在独立的ArkTS测试入口;它验证状态不受传入的视图模式影响,并给出文章和演示图共用的结果字符串。
function verifyFilterLedger(): string {
const tag: string = '旅行'
const favoriteOnly: boolean = true
const wide: string[] = FilterLedger.visible(tag, favoriteOnly).map(p => p.id)
const narrow: string[] = FilterLedger.visible(tag, favoriteOnly).map(p => p.id)
if (wide.join(',') !== 'P-101,P-105' ||
narrow.join(',') !== wide.join(',')) {
return 'FOLD-2606 FAIL'
}
return 'FOLD-2606 PASS · P-101,P-105 · 2张'
}
console.info(verifyFilterLedger())
这段检查覆盖的是确定性输入,而不是折叠屏实际尺寸事件。它的价值在于拆出一个独立判断:如果筛选结果在纯函数层已经变了,问题不在布局;如果纯函数始终输出两张,而页面切换后变成六张,就该查看页面初始化、导航重建或条件写回。console.info为样本日志,不是已经取得的设备HiLog。真正调试时应记录宽度变化前后tag、favoriteOnly和可见ID,但避免在正式日志中打印敏感图片路径或用户搜索词。
五、状态演示:展开四列,折叠两列,照片仍是两张
按照示例样本推演,宽态920vp时显示EXPANDED · 4列,条件为旅行 / 仅收藏:开,结果为P-101,P-105 · 2张。窗口收窄到560vp,网格应变为COMPACT · 2列,但标签和收藏开关保持不变,列表依旧是山湖与海岸。用户再关闭“仅收藏”,新结果才应变成三张,包括P-103群山;这一步是主动操作,不属于窗口折叠的副作用。

若把文章接到真实相册,需要另外处理图片缩略图的异步加载、空态、资源缓存释放、图片权限和滚动位置恢复。这些属于独立问题,不应因为当前筛选样例通过就宣布多形态适配已经全面完成。尤其是窗口被系统回收后重新拉起,三个@State字段会按初始值重置,需明确产品是否承诺恢复旧筛选。
六、边界和可复现的调试顺序
建议先在普通窗口里人工调整可用宽度,而不是只按设备名称想象折叠行为。还应记录用户在窄态主动修改标签以后再次展开的路径,确认修改是双向生效而非仅保证从宽到窄的单方向展示。调试顺序是固定样本数量、验证纯函数、观察宽窄模式切换、核对按钮文字,最后才考虑接入真实图片源。遇到卡片消失时,先看业务结果数组长度,再看Grid的高度约束和滚动区域;布局被裁剪与筛选结果缺失在视觉上相似,但根因完全不同。
onAreaChange本身不负责保存用户设置,其宽度只反映布局测量。@State也不意味着跨页面、跨Ability持久化。示例不使用外部监听和订阅,不需要额外解绑;真实项目若引入窗口大小事件订阅、异步加载器或图片缓存,应在所属生命周期解除监听、取消过期任务并回收不再使用的资源,避免旧页面继续改写新状态。
这套方案适合图片浏览、商品筛选与多列卡片集合:先把业务条件从布局里独立出来,再用响应式容器决定排列方式。结论并不是“折叠屏只改两行代码”,而是同一组用户条件必须在所有布局形态下解释为同一批业务数据。它能降低布局切换造成的误操作风险,却不能替代端到端的设备测试,也不应把示例的640vp写成系统统一标准。
参考资料:华为ArkUI响应式布局说明:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-layout-development-grid-layout ;HarmonyOS ArkUI:https://developer.huawei.com/consumer/cn/arkui/ 。
更多推荐





所有评论(0)