作者:狼哥
團(tuán)隊(duì):堅(jiān)果派
團(tuán)隊(duì)介紹:堅(jiān)果派由堅(jiān)果等人創(chuàng)建,團(tuán)隊(duì)擁有12個華為HDE帶領(lǐng)熱愛HarmonyOS/OpenHarmony的開發(fā)者,以及若干其他領(lǐng)域的三十余位萬粉博主運(yùn)營。專注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服務(wù)、倉頡。團(tuán)隊(duì)成員聚集在北京,上海,南京,深圳,廣州,寧夏等地,目前已開發(fā)鴻蒙原生應(yīng)用,三方庫60+,歡迎交流。
介紹
在移動應(yīng)用開發(fā)中,采用骨架屏(Skeleton Screen)作為加載策略,具有顯著的優(yōu)勢。首先,骨架屏能夠即時反饋給用戶頁面正在加載中,有效緩解了因網(wǎng)絡(luò)延遲或數(shù)據(jù)處理造成的“白屏”現(xiàn)象,提升了用戶體驗(yàn)的流暢度與期待感。它以一種輕量級、占位符的形式預(yù)先展示頁面結(jié)構(gòu),讓用戶對即將呈現(xiàn)的內(nèi)容有所預(yù)期,減少了等待時的焦慮感。
其次,骨架屏設(shè)計(jì)簡潔且高度自定義,能夠根據(jù)應(yīng)用風(fēng)格和頁面布局靈活調(diào)整,保持界面的一致性和美觀性。這種視覺上的連續(xù)性不僅增強(qiáng)了應(yīng)用的品牌形象,也提升了用戶對產(chǎn)品的信任度和好感度。
再者,從性能優(yōu)化的角度來看,骨架屏相比完整的頁面內(nèi)容加載更快,因?yàn)樗鼉H包含基礎(chǔ)的框架結(jié)構(gòu)和動畫效果,減少了初始加載的數(shù)據(jù)量,有助于提升應(yīng)用的加載速度和響應(yīng)能力。這對于提升用戶留存率和轉(zhuǎn)化率具有重要意義。
綜上所述,骨架屏作為現(xiàn)代移動應(yīng)用設(shè)計(jì)中的一種高效加載策略,以其即時反饋、美化等待體驗(yàn)、提升性能及增強(qiáng)品牌一致性等優(yōu)勢,成為提升用戶體驗(yàn)不可或缺的一環(huán)。
效果預(yù)覽

1723867551658.gif

image.png

image.png

image.png

image.png

image.png

image.png

image.png
知識點(diǎn)
1. 顯式動畫 (animateTo)
2. 組件內(nèi)轉(zhuǎn)場 (transition)
3. if/else:條件渲染
4. ForEach:循環(huán)渲染
工程目錄
├──entry/src/main/ets // 代碼區(qū)
│ ├──components
│ │ └──TextAreaBuilder.ets // 骨架屏占位組件
│ ├──entryability
│ │ └──EntryAbility.ets
│ ├──pages
│ │ └──Index.ets // 首頁
│ └──view
│ ├──GridSkeleton.ets // Grid骨架圖
│ ├──GridView.ets // Grid布局圖
│ ├──ListSkeleton.ets // List骨架圖
│ └──ListView.ets // List布局圖
│ ├──SwiperSkeleton.ets // Swiper骨架圖
│ └──SwiperView.ets // Swiper布局圖
└──entry/src/main/resources // 應(yīng)用資源目錄
具體實(shí)現(xiàn)
在HarmonyOS(鴻蒙系統(tǒng))中實(shí)現(xiàn)骨架屏(Skeleton Screen)通常用于提升應(yīng)用的加載體驗(yàn),特別是在網(wǎng)絡(luò)請求數(shù)據(jù)尚未返回時顯示一個大致的頁面框架,讓用戶知道頁面正在加載中。下面介紹如何使用`if/else`渲染、`foreach`渲染、顯式動畫以及組件內(nèi)轉(zhuǎn)場等技術(shù)在HarmonyOS中實(shí)現(xiàn)骨架屏。
**骨架屏基礎(chǔ)設(shè)計(jì)**
設(shè)計(jì)你的骨架屏布局。骨架屏通常包括簡單的線條、圓形等,以模擬最終加載完成后的頁面結(jié)構(gòu)??梢允褂迷赥extAreaBuilder.ets里封裝的`textArea` Builder來創(chuàng)建。
1. 骨架屏占位組件
@Builder
export function textArea(
width: number | Resource | string = '100%',
height: number | Resource | string = '100%',
borderRadius: Length | BorderRadiuses | LocalizedBorderRadiuses = 0,
padding: Length | Padding | LocalizedPadding = 0,
margin: Length | Padding | LocalizedPadding = 0) {
Row()
.width(width)
.height(height)
.backgroundColor('#FFF2F3F4')
.borderRadius(borderRadius)
.padding(padding)
.margin(margin)
}
2. 首頁
首頁由Swiper視圖、Grid視圖、List視圖組成,各自布局和邏輯在各自上實(shí)現(xiàn)。
@Entry
@Component
struct Index {
build() {
Column({ space: 20 }) {
SwiperView()
GridView()
ListView()
}
.height('100%')
.width('100%')
}
}
3. Swiper實(shí)現(xiàn)
Swiper骨架圖
import { textArea } from '../components/TextAreaBuilder'
@Component
export struct SwiperSkeleton {
build() {
textArea('90%', px2vp(460), 16, 0, {top: 11})
}
}
Swiper實(shí)現(xiàn)與布局
@State isLoadingSwiperData: boolean = true;
aboutToAppear(): void {
// 模擬異步回調(diào)
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.isLoadingSwiperData = false;
})
}, 4000)
}
Swiper() {
ForEach(this.bannerList, (item: BannerClass) => {
if (this.isLoadingSwiperData) {
SwiperSkeleton()
} else {
Image($r(item.imageSrc))
.objectFit(ImageFit.Contain)
.width('100%')
.borderRadius(16)
.transition({ type: TransitionType.Insert, translate: { x: 700, y: 0 } })
}
}, (item: BannerClass) => item.id.toString())
}
4. Grid實(shí)現(xiàn)
Grid骨架圖
import { textArea } from '../components/TextAreaBuilder'
@Component
export struct GridSkeleton {
build() {
Column({space: 15}) {
textArea('100%', '60%', { topLeft: 16, topRight: 16 })
Column({space: 15}) {
textArea('60%', '15%')
textArea('90%', '25%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding({left: 15})
}
.height('100%')
.width('100%')
.alignItems(HorizontalAlign.Start)
}
}
Grid實(shí)現(xiàn)與布局
@State isLoadingGridData: boolean = true;
aboutToAppear(): void {
// 模擬異步回調(diào)
setTimeout(() => {
animateTo({ duration: 3000 }, () => {
this.isLoadingGridData = false;
})
}, 6000)
}
Grid() {
ForEach(this.enablementList, (item: ArticleClass) => {
GridItem() {
if (this.isLoadingGridData) {
GridSkeleton()
} else {
EnablementItem({ enablementItem: item })
.transition({ type: TransitionType.Insert, translate: { x: -700, y: 0 } })
}
}
.width(160)
}, (item: ArticleClass) => item.id.toString())
}
5. List實(shí)現(xiàn)
List骨架圖
import { textArea } from '../components/TextAreaBuilder'
@Component
export struct ListSkeleton {
build() {
Row({space: 10}) {
Column({space: 10}) {
textArea('60%', '20%')
textArea('90%', '70%')
}
.width('70%')
.alignItems(HorizontalAlign.Start)
textArea('30%', '100%', 16)
}
.height(88)
.margin({ bottom: 10 })
.justifyContent(FlexAlign.SpaceBetween)
}
}
List實(shí)現(xiàn)與布局
@State isLoadingListData: boolean = true;
aboutToAppear(): void {
// 模擬異步回調(diào)
setTimeout(() => {
animateTo({ duration: 3000 }, () => {
this.isLoadingListData = false;
})
}, 8000)
}
List({ space: 12 }) {
ForEach(this.tutorialList, (item: ArticleClass) => {
ListItem() {
if (this.isLoadingListData) {
ListSkeleton()
} else {
TutorialItem({ tutorialItem: item })
.transition({ type: TransitionType.Insert, translate: { x: 0, y: 500 } })
}
}
}, (item: ArticleClass) => item.id.toString())
}
總結(jié)
通過此案例,可以學(xué)習(xí)到顯式動畫知識點(diǎn),全局animateTo顯式動畫接口來指定由于閉包代碼導(dǎo)致的狀態(tài)變化插入過渡動效。同屬性動畫,布局類改變寬高的動畫,內(nèi)容都是直接到終點(diǎn)狀態(tài)。組件內(nèi)轉(zhuǎn)場主要通過transition屬性配置轉(zhuǎn)場參數(shù),在組件插入和刪除時顯示過渡動效,主要用于容器組件中的子組件插入和刪除時,提升用戶體驗(yàn)。if/else條件渲染,可根據(jù)應(yīng)用的不同狀態(tài),使用if、else和else if渲染對應(yīng)狀態(tài)下的UI內(nèi)容。ForEach循環(huán)渲染,F(xiàn)orEach接口基于數(shù)組類型數(shù)據(jù)來進(jìn)行循環(huán)渲染,需要與容器組件配合使用,且接口返回的組件應(yīng)當(dāng)是允許包含在ForEach父容器組件中的子組件。例如,ListItem組件要求ForEach的父容器組件必須為List組件??傊?,以后在很多異步返回?cái)?shù)據(jù)前,都可以先顯示骨架屏,讓用戶知道頁面正在加載中,在HarmonyOS中實(shí)現(xiàn)骨架屏需要結(jié)合布局設(shè)計(jì)、數(shù)據(jù)綁定、條件渲染、動畫以及可能的組件內(nèi)狀態(tài)管理。
約束與限制
1.本示例僅支持標(biāo)準(zhǔn)系統(tǒng)上運(yùn)行,支持設(shè)備:華為手機(jī)。
2.HarmonyOS系統(tǒng):HarmonyOS NEXT Developer Beta1及以上。
3.DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。
4.HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上。