面骨架如何做成組件:ArkUI/ArkTS 實(shí)戰(zhàn)拆解)
文章目錄前言為什么這個(gè)問(wèn)題經(jīng)常被寫(xiě)亂場(chǎng)景資訊列表首屏加載骨架塊設(shè)計(jì)實(shí)操步驟先把頁(yè)面目標(biāo)想清楚完整示例資訊卡片骨架組件把關(guān)鍵代碼一段段拆開(kāi)容易踩坑的點(diǎn)優(yōu)化建議骨架組件要輕但不能失真寫(xiě)在最后前言骨架屏不是灰色占位塊越多越專(zhuān)業(yè)。它應(yīng)該貼近真實(shí)頁(yè)面結(jié)構(gòu)讓用戶(hù)知道內(nèi)容正在加載也讓頁(yè)面從加載到完成時(shí)不會(huì)明顯跳動(dòng)。這篇單獨(dú)聊通用加載這個(gè)場(chǎng)景。重點(diǎn)不是堆 API而是把骨架屏抽成足夠小的 ArkUI 構(gòu)建器讓加載態(tài)貼近真實(shí)頁(yè)面結(jié)構(gòu)。為什么這個(gè)問(wèn)題經(jīng)常被寫(xiě)亂頁(yè)面骨架如何做成組件 這類(lèi)內(nèi)容很容易被寫(xiě)成“代碼能跑就算講完了”但對(duì)初學(xué)者來(lái)說(shuō)這恰恰是最不夠的地方。真正讓人卡住的往往不是某個(gè)組件名記不住而是不知道這段代碼為什么要這樣拆、狀態(tài)為什么要這樣放、以后需求變化時(shí)應(yīng)該從哪里改。所以這篇文章不只想給你一個(gè)能跑的例子更想把背后的判斷過(guò)程講清楚。你只要把這個(gè)判斷過(guò)程吃透后面自己改頁(yè)面、補(bǔ)需求、查問(wèn)題時(shí)心里會(huì)穩(wěn)很多。場(chǎng)景資訊列表首屏加載骨架屏不是把頁(yè)面涂成一片灰色。它應(yīng)該告訴用戶(hù)這里會(huì)有標(biāo)題、這里會(huì)有摘要、這里會(huì)有列表卡片。資訊、課程、訂單、商品列表這類(lèi)頁(yè)面如果加載時(shí)只放一個(gè)轉(zhuǎn)圈用戶(hù)不知道還要等多久如果骨架結(jié)構(gòu)貼近真實(shí)內(nèi)容等待感會(huì)明顯降低。我做骨架組件時(shí)會(huì)控制復(fù)雜度。骨架應(yīng)該是輕量的 UI 片段不應(yīng)該復(fù)制一份完整業(yè)務(wù)卡片。復(fù)制得越像維護(hù)成本越高抽得太粗又失去降低跳動(dòng)的意義。骨架屏不是裝飾它是“頁(yè)面馬上會(huì)長(zhǎng)什么樣”的預(yù)告。骨架塊設(shè)計(jì)骨架塊對(duì)應(yīng)真實(shí)內(nèi)容尺寸建議注意點(diǎn)SkeletonLine標(biāo)題、摘要12 到 22 vp 高寬度不要全滿(mǎn)SkeletonAvatar頭像、圖標(biāo)32 到 48 vp圓角貼近真實(shí)圖形SkeletonCard列表卡片接近真實(shí)卡片高度避免加載完成大跳動(dòng)SkeletonAction按鈕32 到 40 vp 高不要太搶眼實(shí)操步驟先畫(huà)真實(shí)頁(yè)面結(jié)構(gòu)再反推骨架塊而不是先畫(huà)灰塊。抽出最小單元SkeletonLine復(fù)用顏色和圓角。再組合出ArticleSkeletonCard這類(lèi)業(yè)務(wù)骨架。加載態(tài)和內(nèi)容態(tài)用同一塊頁(yè)面區(qū)域切換避免高度差過(guò)大。骨架數(shù)量與首屏可見(jiàn)內(nèi)容接近不要一次渲染幾十個(gè)灰塊。接口失敗時(shí)切到錯(cuò)誤態(tài)不要讓骨架無(wú)限顯示。先把頁(yè)面目標(biāo)想清楚在真正寫(xiě)代碼之前先別急著盯著 API。更有用的做法是先想清楚這個(gè)頁(yè)面到底想解決什么問(wèn)題用戶(hù)最在意的反饋是什么哪些狀態(tài)必須一直保持一致。當(dāng)你先把這條主線想明白再回頭看組件和狀態(tài)設(shè)計(jì)很多選擇都會(huì)順理成章。對(duì)小白來(lái)說(shuō)這一步尤其重要因?yàn)樗軒湍銖摹罢罩甭^(guò)渡到“看得懂、改得動(dòng)”。完整示例資訊卡片骨架組件interfaceArticleItem{id:numbertitle:stringsummary:stringauthor:string}EntryComponentstruct SkeletonComponentPage{Stateloading:booleantrueStatefailed:booleanfalseprivatearticles:ArticleItem[][{id:1,title:ArkUI 列表加載的幾個(gè)細(xì)節(jié),summary:從骨架屏、空態(tài)到錯(cuò)誤兜底整理一個(gè)更完整的加載體驗(yàn)。,author:編輯部},{id:2,title:HarmonyOS7 頁(yè)面狀態(tài)管理實(shí)踐,summary:用明確狀態(tài)減少頁(yè)面閃動(dòng)和重復(fù)判斷。,author:技術(shù)團(tuán)隊(duì)}]BuilderSkeletonLine(widthValue:Length,heightValue:number,radiusValue:number){Blank().width(widthValue).height(heightValue).backgroundColor(#E6EAF0).borderRadius(radiusValue)}BuilderArticleSkeletonCard(){Row({space:12}){this.SkeletonLine(48,48,8)Column({space:10}){this.SkeletonLine(64%,18,4)this.SkeletonLine(92%,14,4)this.SkeletonLine(48%,14,4)}.alignItems(HorizontalAlign.Start).layoutWeight(1)}.padding(14).backgroundColor(#FFFFFF).borderRadius(8)}BuilderArticleCard(item:ArticleItem){Row({space:12}){Text(item.author.substring(0,1)).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FFFFFF).width(48).height(48).textAlign(TextAlign.Center).backgroundColor(#0A59F7).borderRadius(8)Column({space:6}){Text(item.title).fontSize(16).fontWeight(FontWeight.Medium).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})Text(item.summary).fontSize(13).fontColor(#666666).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})Text(item.author).fontSize(12).fontColor(#999999)}.alignItems(HorizontalAlign.Start).layoutWeight(1)}.padding(14).backgroundColor(#FFFFFF).borderRadius(8)}privatereload(){this.loadingtruethis.failedfalse}build(){Column({space:12}){Row(){Text(加載示例).fontSize(22).fontWeight(FontWeight.Bold)Blank()Button(this.loading?顯示內(nèi)容:顯示骨架).onClick(()this.loading!this.loading)}.width(100%)if(this.failed){Column({space:8}){Text(加載失敗).fontSize(18).fontWeight(FontWeight.Medium)Text(網(wǎng)絡(luò)不穩(wěn)定請(qǐng)稍后重試。).fontSize(13).fontColor(#666666)Button(重試).onClick(()this.reload())}.alignItems(HorizontalAlign.Start).padding(14).backgroundColor(#FFFFFF).borderRadius(8)}elseif(this.loading){this.ArticleSkeletonCard()this.ArticleSkeletonCard()this.ArticleSkeletonCard()}else{ForEach(this.articles,(item:ArticleItem){this.ArticleCard(item)},(item:ArticleItem)item.id.toString())}}.padding(16).height(100%).backgroundColor(#F5F7FA)}}把關(guān)鍵代碼一段段拆開(kāi)SkeletonLine()是最小單元只關(guān)心寬、高、圓角。這樣標(biāo)題線、摘要線、頭像占位都能復(fù)用同一套灰色樣式不需要到處復(fù)制backgroundColor。ArticleSkeletonCard()的結(jié)構(gòu)和真實(shí)ArticleCard()接近左側(cè)頭像右側(cè)三行文字。加載完成后高度變化很小頁(yè)面不會(huì)突然跳動(dòng)。failed和loading分開(kāi)處理。骨架只能表示“正在加載”不能拿來(lái)表示“加載失敗”。接口失敗后如果骨架一直轉(zhuǎn)用戶(hù)會(huì)以為還在等。容易踩坑的點(diǎn)骨架和真實(shí)內(nèi)容高度差太大加載完成時(shí)頁(yè)面明顯跳動(dòng)。骨架數(shù)量過(guò)多反而增加首屏渲染壓力。把骨架寫(xiě)成完整業(yè)務(wù)組件的復(fù)制版后續(xù)維護(hù)兩套結(jié)構(gòu)。失敗后仍顯示骨架沒(méi)有錯(cuò)誤兜底。所有灰塊寬度都一樣看起來(lái)不像真實(shí)內(nèi)容。優(yōu)化建議骨架屏適合超過(guò)短暫等待的頁(yè)面。如果接口通常幾十毫秒返回強(qiáng)行閃一下骨架反而打擾用戶(hù)??梢栽O(shè)置一個(gè)很小的延遲策略請(qǐng)求很快完成就不展示骨架請(qǐng)求超過(guò)一定時(shí)間再展示。顏色上不要用太重的灰尤其是在淺色背景里。骨架應(yīng)該低調(diào)地暗示結(jié)構(gòu)不應(yīng)該搶走真實(shí)內(nèi)容的位置感。深色模式下也要單獨(dú)調(diào)整骨架色否則灰塊可能過(guò)亮。骨架組件要輕但不能失真骨架屏的價(jià)值在于降低等待感和布局跳動(dòng)不是復(fù)制一套完整業(yè)務(wù) UI?;覊K要貼近真實(shí)內(nèi)容結(jié)構(gòu)讓用戶(hù)知道頁(yè)面馬上會(huì)出現(xiàn)什么但它又不能和真實(shí)卡片完全同步維護(hù)否則后面改一次業(yè)務(wù)卡片就要改兩份代碼。示例先抽SkeletonLine()作為最小單元再組合成ArticleSkeletonCard()。這樣顏色、圓角和尺寸可以復(fù)用資訊卡片的左圖右文結(jié)構(gòu)也能保留下來(lái)加載完成時(shí)頁(yè)面不會(huì)明顯跳動(dòng)。寫(xiě)在最后失敗態(tài)要和加載態(tài)分開(kāi)。骨架只表示“正在加載”接口失敗后應(yīng)該切到錯(cuò)誤提示和重試入口。如果骨架無(wú)限顯示用戶(hù)會(huì)以為還在等待實(shí)際已經(jīng)沒(méi)有下一步了。