《骨架图》应用实战 原创

一路向北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()
      }
    }
  }
}

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐