题外话:什么是OOP

直言:面向对象是一种思想。

封装是为了将接口和实现分离,其实就是为了解耦。

继承就是代码的复用 ,子类继承父类的方法。

多态呢,多态稍微复杂一点,多态的话,可能就是说使用至不同的内部结构的对象,

可以共享相同的外部接口,其实在我们的理解就是这个, 相同的方法名,相同的返回值,
返回值定义,然后他的参数不一样。

其实对外暴露的时候,方法名都是一个,参数不一样,不同场景用的同一个方法。


正题:骨架屏实现

先看效果:

仿开眼logo 侵权可删

定义enum页面加载状态

/**
 *  页面加载状态 Loading
 */
export enum LoadingStatus {
  OFF = 'off', //关
  LOADING = 'loading', //加载中
  SUCCESS = 'success', //加载成功
  FAILED = 'failed' //加载失败
}

设置一点全局方法+动画实现

//常用参数
export class CommonConstants {
  static readonly FULL_SCREEN = '100%'

  // 骨架动画
  static readonly SKELETON_ANIMATION: AnimateParam = {
    duration: 400, //动画持续时间,单位为毫秒
    tempo: 0.6, //动画播放速度。数值越大动画播放速度越快,数值越小动画播放速度越慢。值为0表示没有动画
    curve: Curve.EaseInOut, //动画曲线:表示动画以慢速开始和结束
    delay: 200, //动画延迟时间,单位为毫秒
    iterations: -1, //动画的迭代。当设置为-1时,动画重复播放。取值范围大于等于-1
    //playMode: 动画播放模式。默认情况下,动画在播放完成后从头开始播放
    playMode: PlayMode.Alternate //PlayMode.Alternate: 动画在奇数(1,3,7…)上向前播放,在偶数(2,4,6…)上向后播放
  }
}

//全局方法(demo偷懒用全局方法, 实际开发中,应该用资源文件存储 )
export function skeletonColor2(): string {
  return '#FFF2F3F4'. //骨架屏色(狠浅)
}

export function skeletonColor(): string {
  return '#ECECEC'. //骨架屏色(偏浅)
}

export function skeletonColorDeep(): string {
  return '1A000000' //骨架屏色 (偏深)
}

export function vpConversion(number: number): string {
  return number + 'vp' //无须多言 vp
}

骨架屏View

@Preview //方便在预览器看效果 开发完后删除该装饰器
@ComponentV2
export struct HomeSkeletonView {
  //不透明度
  @Local columnOpacity: number = 1;
  //开始动画
  startAnimation(): void {
    animateTo(CommonConstants.SKELETON_ANIMATION, () => {
      this.columnOpacity = 0.5;
    })
  }

  build() {
    Column() {
      //tab
      Row({ space: vpConversion(8) }) {
        ForEach([7, 7, 7, 7, 7, 7, 7], () => {
          Row()
            .width(vpConversion(44))
            .height(vpConversion(24))
            .backgroundColor(skeletonColor())
            .padding({
              left: vpConversion(12)
            })
            .borderRadius(vpConversion(5))
        })
      }
      .margin({ top: vpConversion(10) })
      .width(CommonConstants.FULL_SCREEN)

      ForEach([3, 3, 3], () => {
        Column({ space: vpConversion(8) }) {
          Row()
            .backgroundColor(skeletonColorDeep())
            .width(CommonConstants.FULL_SCREEN)
            .height(vpConversion(150))
            .borderRadius(vpConversion(10))
            .margin({ top: vpConversion(10), bottom: vpConversion(6) })

          Row()
            .backgroundColor(skeletonColor())
            .width('84%')
            .height(vpConversion(12))
            .borderRadius(vpConversion(10))

          Row()
            .backgroundColor(skeletonColor())
            .width('84%')
            .height(vpConversion(12))
            .borderRadius(vpConversion(10))


          Row({ space: vpConversion(12) }) {
            Row()
              .backgroundColor(skeletonColorDeep())
              .width(vpConversion(30))
              .height(vpConversion(30))
              .borderRadius(vpConversion(50))
            Column() {
              Row()
                .backgroundColor(skeletonColor())
                .width('35%')
                .height(vpConversion(10))
                .borderRadius(vpConversion(10))
              Row()
                .backgroundColor(skeletonColor())
                .width('26%')
                .height(vpConversion(10))
                .borderRadius(vpConversion(10))
            }
            .height(vpConversion(30))
            .width(CommonConstants.FULL_SCREEN)
            .alignItems(HorizontalAlign.Start)
            .justifyContent(FlexAlign.SpaceAround)

          }

          Line()
            .height(vpConversion(1))
            .width('97%')
            .margin({ left: vpConversion(3) })
            .backgroundColor('#E0E0E0')

        }
        .width(CommonConstants.FULL_SCREEN)
        .alignItems(HorizontalAlign.Start)

      })
    }
    .backgroundColor($r('app.color.skeletonColor2'))
    .padding({ left: 12, right: 12 })
    .width(CommonConstants.FULL_SCREEN)
    .height(CommonConstants.FULL_SCREEN)
    .opacity(this.columnOpacity) //动态控制不透明度
    .onAppear(() => {
      this.startAnimation() //当ui挂载显示器时触发的回调-> 执行(开始动画)方法
    })
  }
}

目前我们的骨架屏已经实现了,但是还是需要完善一下逻辑。

骨架屏使用

import { LoadingStatus } from "common";
import { HomeSkeletonView } from "../../../components/HomeSkeletonView"
import { VideoTypeEntity } from "../../../model/VideoTypeEntity";
import { videoTypeService } from "../../../service/VideoTypeService";
import { RecommendTitleBar } from "./recommend/RecommendTitleBar";HttpCodezH

@ComponentV2
export struct RecommendView {
  //骨架屏 状态
  @Local loadingStatus: LoadingStatus = LoadingStatus.OFF //默认关闭
  //data
  @Local videoTypes: Array<VideoTypeEntity> = []

  aboutToAppear(): void {
    this.loadData()
  }

  loadData(): void {
    // 进入页面,状态设置为loading,也就是骨架屏
    this.loadingStatus = LoadingStatus.LOADING
    // 模拟一个网络请求,自行替换
    videoTypeService.getVideoTypes().then((result) => {
      //成功
      if (result.code == 200) {
        this.videoTypes = result.data //替换data数据
        //修改页面状态为success
        this.loadingStatus = LoadingStatus.SUCCESS
      } else {
        // 修改页面状态为falled,没有获取到数据
        this.loadingStatus = LoadingStatus.FAILED
      }
    })
  }

  build() {
    Column() {
      if (this.loadingStatus === LoadingStatus.LOADING) {
        HomeSkeletonView() //展示骨架屏view
      } else if (this.loadingStatus === LoadingStatus.SUCCESS) {
        RecommendTitleBar() //展示数据view
      } else if (this.loadingStatus === LoadingStatus.FAILED) {
        //加载 无网络视图 TODO
        FalledView() //展示失败view
      }
    }

  }
}

原理刨析

业务逻辑:

通过页面状态loading,在数据回来之前,把骨架屏展示出来

核心技术点: 

动画实现:SKELETON_ANIMATION(请看代码注解)  

通过透明度 0.5 - 1 之间的不断变换,实现了骨架屏若隐若现

通过@Local装饰器实时更新透明度

通过根布局属性onAppear 页面挂载时触发动画

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐