穩(wěn)定恢復(fù)【鴻蒙心跡】)
把文檔目錄放進(jìn)大屏的左欄右欄顯示正文這種結(jié)構(gòu)本身并不難。真正讓人覺得別扭的往往是一個(gè)很小的動作讀者正在看第十六份文檔隨手切換到“待處理”篩選再返回目錄列表卻跳到了第一條。右邊文章還在左邊卻找不到剛才那份材料用戶需要重新滑動。對閱讀類應(yīng)用來說這種丟失位置的感覺比布局從兩欄變成一欄更明顯。這篇用SplitLedger作為演示項(xiàng)目專門處理這個(gè)問題。它不模擬系統(tǒng)提供了某種自動保持滾動位置的神奇能力而是把業(yè)務(wù)層的穩(wěn)定文檔 ID和 ArkUIList的當(dāng)前索引分開保存。示例含 36 份文檔其中 18 份屬于“待處理”。讀者的錨點(diǎn)為DOC_016篩選前它的索引是 15篩選后是 7最后狀態(tài)顯示RESTORED。這些數(shù)字是為了解釋算法而固定的示例不是實(shí)測性能數(shù)據(jù)。圖 01平行視界目錄篩選與閱讀錨點(diǎn)恢復(fù)的技術(shù)封面左右列表只是同一邏輯的示意。## 一、索引不是身份過濾以后尤其不是最容易寫的方案是記一個(gè)firstIndex離開頁面時(shí)存起來回來執(zhí)行scrollToIndex(firstIndex)。這種寫法在數(shù)據(jù)不變時(shí)通常能工作換成篩選列表就不可靠了。最極端的情況是篩選前有 36 條篩選后只剩 18 條原先第 27 項(xiàng)甚至可能越界。即便沒有越界原先索引 15 的位置也早已不是同一份文檔。所以需要保存兩個(gè)值滾動位置屬于視圖穩(wěn)定 ID 屬于數(shù)據(jù)。錨點(diǎn)應(yīng)該存成DOC_016需要恢復(fù)時(shí)再到當(dāng)前篩選結(jié)果里計(jì)算它的新索引。這樣的映射邏輯與屏幕寬度、列數(shù)以及列表項(xiàng)樣式都無關(guān)后續(xù)從手機(jī)單欄切到折疊屏雙欄也不必重寫規(guī)則。先做一個(gè)不依賴 ArkUI 的小模塊。它解決的是篩選后從穩(wěn)定 ID 重新定位而不是在業(yè)務(wù)層記錄一個(gè)隨時(shí)可能過期的數(shù)字。arktsinterface DocItem { id: string title: string pending: boolean}function makeDocs(): DocItem[] { const rows: DocItem[] [] for (let i 1; i 36; i) { const id DOC_${String(i).padStart(3, 0)} rows.push({ id, title: i 16 ? 接口聯(lián)調(diào)問題記錄 : 文檔${i}, pending: i % 2 0 }) } return rows}class AnchorKeeper { private anchorId: string remember(id: string): void { if (id.length 0) { this.anchorId id } } resolve(items: DocItem[]): number { return items.findIndex((item: DocItem) item.id this.anchorId) } currentId(): string { return this.anchorId }}findIndex() 返回的是當(dāng)前結(jié)果數(shù)組里的索引不是原始數(shù)組的舊索引。返回 -1 也不是異常如果 DOC_016 已經(jīng)被當(dāng)前篩選條件排除就沒有理由強(qiáng)行把它滾到某個(gè)“差不多”的位置。生產(chǎn)環(huán)境可以設(shè)計(jì)降級方案例如定位到與錨點(diǎn)相鄰的可見項(xiàng)目、清除篩選或提示“當(dāng)前條件下不可見”但必須讓選擇規(guī)則顯式、可解釋。## 二、讓 List 的滾動事件只負(fù)責(zé)觀測位置華為 ArkUI 的 List 可以綁定 Scroller通過 onScrollIndex 獲得列表可見索引也可以調(diào)用 scrollToIndex() 滾動到指定項(xiàng)。這里真正需要警惕的是把觀察與恢復(fù)寫進(jìn)同一個(gè)循環(huán)篩選導(dǎo)致列表重排滾動事件更新錨點(diǎn)隨后恢復(fù)函數(shù)反而找到了一個(gè)已經(jīng)被替換的 ID。因此示例先把待恢復(fù)的 ID 凍結(jié)再替換數(shù)組。下面代碼重點(diǎn)解決兩個(gè)動作的分工onScrollIndex 觀測當(dāng)前可見項(xiàng)applyFilter 計(jì)算新索引恢復(fù)動作由數(shù)據(jù)更新后的顯式按鈕觸發(fā)。為保持文章短而可讀makeDocs() 只是業(yè)務(wù)數(shù)據(jù)準(zhǔn)備函數(shù)在真實(shí)項(xiàng)目中由本地倉儲或分頁接口提供不是平臺 API。arktsEntryComponentstruct SplitLedgerPage { State docs: DocItem[] makeDocs() // 演示數(shù)據(jù)36 項(xiàng) State restoreIndex: number -1 State status: string ‘READY’ private scroller: Scroller new Scroller() private keeper: AnchorKeeper new AnchorKeeper() private savedId: string ‘’ private allDocs: DocItem[] makeDocs() private filtering: boolean false private applyFilter(): void { this.savedId this.keeper.currentId() || ‘DOC_016’ this.filtering true this.docs this.allDocs.filter((doc: DocItem) doc.pending) this.keeper.remember(this.savedId) this.restoreIndex this.keeper.resolve(this.docs) this.status ‘READY’ } private restore(): void { if (this.restoreIndex 0) { this.status ‘NOT_FOUND’ return } this.scroller.scrollToIndex(this.restoreIndex) this.status ‘RESTORED’ this.filtering false } build() { Column({ space: 12 }) { Button(‘篩選待處理’).onClick(() this.applyFilter()) List({ scroller: this.scroller }) { ForEach(this.docs, (item: DocItem) { ListItem() { Text(${item.id} ${item.title}) } }, (item: DocItem) item.id) } .onScrollIndex((first: number) { if (!this.filtering this.docs[first]) { this.keeper.remember(this.docs[first].id) } }) Button(‘恢復(fù)閱讀位置’).onClick(() this.restore()) Text(this.status) } }}這個(gè) Demo 特意把“篩選”和“恢復(fù)”做成兩個(gè)操作避免在修改數(shù)據(jù)數(shù)組的同一個(gè)同步回調(diào)里立刻調(diào)用滾動指令。實(shí)際產(chǎn)品可以在列表完成布局更新后自動恢復(fù)但要根據(jù)所用 ArkUI 版本選擇明確的布局完成時(shí)機(jī)而不是依賴一個(gè)沒有保證的零毫秒延遲。示例也沒有處理分組列表如果目錄引入 ListItemGroup組本身會占據(jù)索引應(yīng)重新設(shè)計(jì)映射關(guān)系。*圖 02DevEco Studio 白色主題風(fēng)格演示圖。左側(cè) SplitLedger 工程樹、中間穩(wěn)定 ID 恢復(fù)代碼、右側(cè)模擬器和底部 HiLog 的數(shù)值均是人工設(shè)定的調(diào)試示意不是實(shí)際運(yùn)行證據(jù)。*## 三、大屏的雙欄和手機(jī)的單欄數(shù)據(jù)錨點(diǎn)應(yīng)該共用平行視界界面很容易把左側(cè)目錄選中項(xiàng)與滾動首項(xiàng)混在一起。其實(shí)二者并不完全相同右側(cè)正文可能停留在 DOC_016左側(cè)用戶又滑動瀏覽到其它條目。用戶再次點(diǎn)擊某個(gè)文檔時(shí)需要更新**閱讀選中項(xiàng)**用戶手動滾動時(shí)需要更新**可見錨點(diǎn)**。如果強(qiáng)行用一個(gè)狀態(tài)變量控制兩者篩選和回退就會互相牽連。我更傾向于把選中項(xiàng)視為業(yè)務(wù)狀態(tài)把閱讀錨點(diǎn)視為界面狀態(tài)。兩欄、單欄可以共享 selectedId但每種形態(tài)的 scrollAnchorId 可以分別保存。本文為了聚焦問題用 DOC_016 同時(shí)展示閱讀項(xiàng)和恢復(fù)錨點(diǎn)正式應(yīng)用應(yīng)該在數(shù)據(jù)模型中保留兩者的區(qū)別。尤其要注意用戶主動觸發(fā)篩選之后與系統(tǒng)因窗口形態(tài)變化觸發(fā)的重排不應(yīng)該無條件使用同一恢復(fù)策略。代碼還需要解釋列表重排時(shí)的額外風(fēng)險(xiǎn)有些業(yè)務(wù)會排序、合并或插入消息?;謴?fù)的目標(biāo)應(yīng)該是**最穩(wěn)定的記錄標(biāo)識**不能由 index、標(biāo)題文案或 UI 節(jié)點(diǎn)生成。下面給出純數(shù)據(jù)層的最小回歸校驗(yàn)解決“篩選之后確實(shí)能把 ID 轉(zhuǎn)換為預(yù)期索引”的問題。這段 JavaScript 只作邏輯示例無需依賴設(shè)備。typescriptconst rows Array.from({ length: 36 }, (_, i) ({ id:DOC_${String(i 1).padStart(3, 0)}, pending: (i 1) % 2 0}));const previousIndex rows.findIndex(row row.id ‘DOC_016’);const filtered rows.filter(row row.pending);const currentIndex filtered.findIndex(row row.id ‘DOC_016’);console.log({ previousIndex, currentIndex, filteredCount: filtered.length });// 預(yù)期{ previousIndex: 15, currentIndex: 7, filteredCount: 18 }這個(gè)檢查的好處是不會摻雜 UI 渲染時(shí)序。如果這里先算錯(cuò)了索引后續(xù)再調(diào)Scroller也不可能得到穩(wěn)定結(jié)果。另一方面純數(shù)據(jù)檢查通過不等于真機(jī)視覺位置完全一致。實(shí)際列表項(xiàng)高度不同、資源未加載完成、動態(tài)字號變化都可能讓錨點(diǎn)落點(diǎn)產(chǎn)生可見偏移必須在目標(biāo)設(shè)備上繼續(xù)驗(yàn)證。## 四、把異常路徑當(dāng)作正式測試的一部分示例截圖展示的最終狀態(tài)統(tǒng)一是RESTORED篩選“待處理18”后定位DOC_016恢復(fù)索引是 7。不能因?yàn)榻貓D是綠色成功狀態(tài)就只覆蓋成功路徑。至少還需要人為構(gòu)造三種反例錨點(diǎn) ID 不在篩選結(jié)果中恢復(fù)期間第二次觸發(fā)篩選用戶在恢復(fù)前先手動滑動。第一種應(yīng)該得到NOT_FOUND不能盲目復(fù)用舊索引第二種需要版本號或取消機(jī)制第三種應(yīng)尊重最新的用戶操作避免自動滾動搶走控制權(quán)。圖 039:16 純屏演示圖以DOC_016、新索引 7 和RESTORED作為一致性檢查點(diǎn)。演示并不表示已經(jīng)完成真機(jī)適配測試。## 五、這套辦法真正解決的邊界這不是一篇完整的平行視界系統(tǒng)能力接入說明而是一個(gè)能在手機(jī)列表和雙欄界面中復(fù)用的滾動恢復(fù)策略。它把“記住我在看什么”和“把列表滾到哪里”拆開不假設(shè)過濾前后的數(shù)組下標(biāo)永遠(yuǎn)相等。Scroller負(fù)責(zé)視圖定位業(yè)務(wù)主鍵負(fù)責(zé)身份判定二者相互配合但職責(zé)不能互換。接入真實(shí)項(xiàng)目時(shí)還應(yīng)檢查長列表虛擬化、列表項(xiàng)異步高度變化以及系統(tǒng)返回后頁面對象是否被重建。對于極長目錄可以在數(shù)據(jù)倉儲層維護(hù) ID 到索引的映射避免每次線性查找但只有性能測量證明它有價(jià)值時(shí)才需要額外緩存。代碼中的示例數(shù)據(jù)和日志均為演示尚未在 DevEco Studio 完成編譯與設(shè)備實(shí)測。資料參考華為開發(fā)者聯(lián)盟《創(chuàng)建列表List》 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-create-list 。