回复
《骨架图》应用实战 原创
一路向北545
发布于 2024-12-8 17:07
浏览
0收藏
一、介绍
很多体验好的app在进入列表页时会有列表轮廓为形态的骨架图闪烁动画,从而让用户感知新页面在加载运行的动态过程,让体验更加流程。
二、实现思路
骨架图用于在页面数据加载完成前,先给用户展示出页面的大致结构(通常以灰色或其他浅色系的占位图形式呈现),待接口数据加载完成后,再渲染出实际页面内容并替换掉骨架屏。 通过网络接口返回的状态改变
跳转的新页面有加载中、加载成功、加载失败、加载数据为空四种状态。通过 loadingStatus 值,动态切换页面内容。
三、具体实现
(1)通过setTimeout方法来模拟网络请求,延迟3秒返回数据
aboutToAppear(): void {
setTimeout(() => {
for (let index = 0; index < 10; index++) {
this.list.push("item" + index)
}
this.loadingStatus = LoadingStatus.SUCCESS
}, 3000)
}
(2)定义页面的状态枚举类
export enum LoadingStatus {
LOADING = "loading",
FAILED = "failed",
SUCCESS = "success",
EMPTY = "empty"
}
(3)显示动画来控制骨架图的闪烁动画
// 骨架图的闪烁动画
startAnimation(): void {
// TODO: 知识点:显式动画animateTo(value: AnimateParam, event: () => void): void接口可以给状态变化添加过渡动画。
animateTo({
duration: 400, // 动画持续时间,为400毫秒。
tempo: 0.6, // 动画播放速度,值越大动画播放越快,值越小播放越慢,为0时无动画效果。
curve: Curve.EaseInOut, // 动画曲线,动画以低速开始和结束。
delay: 200, // 动画延迟播放时间,为200毫秒
iterations: -1, // 动画播放次数,设置为-1时表示无限次播放。
playMode: PlayMode.Alternate // 动画播放模式,动画在奇数次(1、3、5...)正向播放,在偶数次(2、4、6...)反向播放。
}, () => {
// 动态修改骨架屏的透明度
this.listOpacity = 0.5;
});
}
(4)根据LoadingStatus的值渲染不同的页面内容:当LoadingStatus为loading时,显示骨架图(LoadingSkeletonView);若LoadingStatus为success,则显示列表页;若数据为空,则显示无数据页面;若LoadingStatus为FAILED,则显示加载失败页面。
build() {
Column() {
if (this.loadingStatus === LoadingStatus.LOADING) {
LoadingSkeletonView()
} else if (this.loadingStatus === LoadingStatus.SUCCESS) {
this.loadingSuccessBuilder()
} else if (this.loadingStatus === LoadingStatus.FAILED) {
Text("加载出错,点击重试")
.width("100%")
.height("100%")
.textAlign(TextAlign.Center)
} else if (this.loadingStatus === LoadingStatus.EMPTY) {
Text("暂无数据,请稍后再试")
.width("100%")
.height("100%")
.textAlign(TextAlign.Center)
}
}.height("100%")
}
四、完整代码
index.ets
import { LoadingSkeletonView } from '../LoadingSkeletonView'
import { LoadingStatus } from './LoadingStatus'
@Entry
@Component
struct Index {
@State list: string[] = []
@State loadingStatus: string = LoadingStatus.LOADING
aboutToAppear(): void {
setTimeout(() => {
for (let index = 0; index < 10; index++) {
this.list.push("item" + index)
}
this.loadingStatus = LoadingStatus.SUCCESS
}, 3000)
}
build() {
Column() {
if (this.loadingStatus === LoadingStatus.LOADING) {
LoadingSkeletonView()
} else if (this.loadingStatus === LoadingStatus.SUCCESS) {
this.loadingSuccessBuilder()
} else if (this.loadingStatus === LoadingStatus.FAILED) {
Text("加载出错,点击重试")
.width("100%")
.height("100%")
.textAlign(TextAlign.Center)
} else if (this.loadingStatus === LoadingStatus.EMPTY) {
Text("暂无数据,请稍后再试")
.width("100%")
.height("100%")
.textAlign(TextAlign.Center)
}
}.height("100%")
}
@Builder
loadingSuccessBuilder() {
List() {
ForEach(this.list, (e: string) => {
ListItem() {
Row() {
Image($r("app.media.app_icon"))
.width(100)
.height(80)
Column() {
Text(e)
.fontWeight(FontWeight.Bold)
Text("this is content")
.margin({ top: 10 })
}.margin({ left: 10 }).alignItems(HorizontalAlign.Start)
.height(80)
.justifyContent(FlexAlign.SpaceEvenly)
}.padding(15)
}
})
}.divider({ strokeWidth: 1, startMargin: 15, endMargin: 15 })
}
}
骨架图控件LoadingSkeletonView
import { SkeletonItemView } from "./SkeletonItemView"
import { SkeletonType } from "./SkeletonType"
@Component
export struct LoadingSkeletonView {
@State list: SkeletonType[] = [{}, {}, {}, {}, {}, {}, {}, {}]
@State listOpacity: number = 1;
// 骨架图的闪烁动画
startAnimation(): void {
// TODO: 知识点:显式动画animateTo(value: AnimateParam, event: () => void): void接口可以给状态变化添加过渡动画。
animateTo({
duration: 400, // 动画持续时间,为400毫秒。
tempo: 0.6, // 动画播放速度,值越大动画播放越快,值越小播放越慢,为0时无动画效果。
curve: Curve.EaseInOut, // 动画曲线,动画以低速开始和结束。
delay: 200, // 动画延迟播放时间,为200毫秒
iterations: -1, // 动画播放次数,设置为-1时表示无限次播放。
playMode: PlayMode.Alternate // 动画播放模式,动画在奇数次(1、3、5...)正向播放,在偶数次(2、4、6...)反向播放。
}, () => {
// 动态修改骨架屏的透明度
this.listOpacity = 0.5;
});
}
@Builder
itemBuilder() {
Row() {
Text()
.width(100)
.height(80)
.borderRadius(8)
.backgroundColor("#f3f3f3")
Column() {
Text()
.width(100)
.height(20)
.backgroundColor("#f3f3f3")
Text()
.width(150)
.height(20)
.backgroundColor("#f3f3f3")
.margin({ top: 10 })
}.margin({ left: 10 }).alignItems(HorizontalAlign.Start)
.height(80)
.justifyContent(FlexAlign.SpaceEvenly)
}.padding(15)
}
build() {
Column() {
List() {
ForEach(this.list, (e: SkeletonType) => {
ListItem() {
SkeletonItemView({
customItemView: () => {
this.itemBuilder()
}
})
}
})
}.divider({ strokeWidth: 1, startMargin: 15, endMargin: 15 })
.scrollBar(BarState.Off)
.enabled(false) //骨架图,可以禁止掉滑动
}.opacity(this.listOpacity)
// 组件挂载显示后触发此回调,调用动画接口给组件添加动画。
.onAppear(() => {
this.startAnimation();
})
.height("100%")
}
}
export class SkeletonType {
type?: number = 0
}
骨架图的自定义item
@Component
export struct SkeletonItemView {
@BuilderParam customItemView: () => void
build() {
Row() {
if (this.customItemView) {
this.customItemView()
}
}
}
}
©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
赞
收藏
回复
相关推荐