作者:狼哥
团队:坚果派
团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。
介绍
在移动应用开发中,采用骨架屏(Skeleton Screen)作为加载策略,具有显著的优势。首先,骨架屏能够即时反馈给用户页面正在加载中,有效缓解了因网络延迟或数据处理造成的“白屏”现象,提升了用户体验的流畅度与期待感。它以一种轻量级、占位符的形式预先展示页面结构,让用户对即将呈现的内容有所预期,减少了等待时的焦虑感。
其次,骨架屏设计简洁且高度自定义,能够根据应用风格和页面布局灵活调整,保持界面的一致性和美观性。这种视觉上的连续性不仅增强了应用的品牌形象,也提升了用户对产品的信任度和好感度。
再者,从性能优化的角度来看,骨架屏相比完整的页面内容加载更快,因为它仅包含基础的框架结构和动画效果,减少了初始加载的数据量,有助于提升应用的加载速度和响应能力。这对于提升用户留存率和转化率具有重要意义。
综上所述,骨架屏作为现代移动应用设计中的一种高效加载策略,以其即时反馈、美化等待体验、提升性能及增强品牌一致性等优势,成为提升用户体验不可或缺的一环。
效果预览
知识点
1. 显式动画 (animateTo)
2. 组件内转场 (transition)
3. if/else:条件渲染
4. ForEach:循环渲染
工程目录
├──entry/src/main/ets // 代码区
│ ├──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 // 应用资源目录
具体实现
在HarmonyOS(鸿蒙系统)中实现骨架屏(Skeleton Screen)通常用于提升应用的加载体验,特别是在网络请求数据尚未返回时显示一个大致的页面框架,让用户知道页面正在加载中。下面介绍如何使用if/else
渲染、foreach
渲染、显式动画以及组件内转场等技术在HarmonyOS中实现骨架屏。
骨架屏基础设计
设计你的骨架屏布局。骨架屏通常包括简单的线条、圆形等,以模拟最终加载完成后的页面结构。可以使用在TextAreaBuilder.ets里封装的textArea
Builder来创建。
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视图组成,各自布局和逻辑在各自上实现。
@Entry
@Component
struct Index {
build() {
Column({ space: 20 }) {
SwiperView()
GridView()
ListView()
}
.height('100%')
.width('100%')
}
}
3. Swiper实现
Swiper骨架图
import { textArea } from '../components/TextAreaBuilder'
@Component
export struct SwiperSkeleton {
build() {
textArea('90%', px2vp(460), 16, 0, {top: 11})
}
}
Swiper实现与布局
@State isLoadingSwiperData: boolean = true;
aboutToAppear(): void {
// 模拟异步回调
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实现
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实现与布局
@State isLoadingGridData: boolean = true;
aboutToAppear(): void {
// 模拟异步回调
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实现
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实现与布局
@State isLoadingListData: boolean = true;
aboutToAppear(): void {
// 模拟异步回调
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())
}
总结
通过此案例,可以学习到显式动画知识点,全局animateTo显式动画接口来指定由于闭包代码导致的状态变化插入过渡动效。同属性动画,布局类改变宽高的动画,内容都是直接到终点状态。组件内转场主要通过transition属性配置转场参数,在组件插入和删除时显示过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。if/else条件渲染,可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。ForEach循环渲染,ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为List组件。总之,以后在很多异步返回数据前,都可以先显示骨架屏,让用户知道页面正在加载中,在HarmonyOS中实现骨架屏需要结合布局设计、数据绑定、条件渲染、动画以及可能的组件内状态管理。
约束与限制
1.本示例仅支持标准系统上运行,支持设备:华为手机。
2.HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上。
3.DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。
4.HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。