本文介绍一个基于 HarmonyOS ArkTS 实现的开源 UI 组件库 SweepLoading,提供带扫光动效的下拉刷新、全屏加载遮罩、上拉加载更多三个组件,支持高度自定义。


一、效果预览

在这里插入图片描述


二、实现原理

2.1 扫光动效原理

ArkUI 提供了 shaderStyle 修饰符,可以对 Text 组件应用线性渐变着色器:

Text('Loading...')
  .shaderStyle({
    angle: 90,
    colors: [
      ['#9A9078', 0.0],   // 基础色(左)
      ['#FFE294', 0.5],   // 高亮色(中心)
      ['#9A9078', 1.0],   // 基础色(右)
    ]
  } as LinearGradientOptions)

通过 setInterval 每 16ms 更新一个 shimmerPos 状态值(约 60fps),让高亮色的位置从 0 移动到 1.4 后循环,形成光从左向右扫过的视觉效果:

// 每帧推进扫光位置
this.shimmerTimer = setInterval(() => {
  this.shimmerPos = (this.shimmerPos + 0.008) % 1.4;
}, 16);

// 渐变色随 shimmerPos 动态变化
colors: [
  [baseColor,      Math.max(0, shimmerPos - 0.2)],
  [highlightColor, Math.min(1, shimmerPos)],
  [baseColor,      Math.min(1, shimmerPos + 0.2)]
]

shimmerPos - 0.2shimmerPos + 0.2 形成一个宽度为 0.4 的高亮窗口,窗口随时间平移,产生扫光效果。


2.2 下拉刷新原理(SweepRefresh)

基于 ArkUI 原生 Refresh 组件封装,通过 builder 参数注入自定义下拉头部:

Refresh({ refreshing: $$this.isRefreshing, builder: this.refreshBuilder }) {
  this.content()  // 列表内容通过 @BuilderParam 注入
}
.onRefreshing(() => {
  this.onRefreshing(); // 回调给外部
})

关键点:

  • $$this.isRefreshing 双向绑定,系统可以自动将其置为 true
  • builder 传函数引用(不加括号),避免立即执行导致渲染异常
  • @BuilderParam content 让外部传入任意列表内容,组件本身不耦合业务

2.3 全屏遮罩原理(SweepOverlay)

使用 Stack 作为根节点(ArkTS 要求 build() 根节点必须是容器,不能是 if),内部条件渲染遮罩层:

build() {
  Stack() {
    if (this.isVisible) {
      Column() { /* 动画 + 文字 */ }
        .width('100%')
        .height('100%')
        .backgroundColor(overlayColor)
    }
  }
  .width('100%')
  .height('100%')
  // isVisible=false 时事件穿透,不阻断下层交互
  .hitTestBehavior(this.isVisible ? HitTestMode.Default : HitTestMode.Transparent)
}

hitTestBehavior(HitTestMode.Transparent) 确保遮罩隐藏后不会拦截触摸事件。


2.4 上拉加载更多原理(SweepLoadMore)

作为 List 的最后一个 ListItem 放置,监听 List.onReachEnd 触发加载:

// 组件内部:isLoading=false 且 hasMore=false 时显示"无更多"
Row() {
  if (this.isLoading) {
    Lottie(...) // 动画
    Text(loadingHint).shaderStyle(...) // 扫光文字
  } else if (this.config.hasMore === false) {
    Text(noMoreText).fontColor('#666666')
  }
}

Timer 优化:通过 onAppear/onDisAppear 控制 timer 生命周期,避免组件不可见时仍在消耗资源。


2.5 架构设计

sweeploading/
├── SweepConfig.ets      // 统一配置接口(所有自定义项)
├── SweepDefaults.ets    // 所有默认常量(单一维护点)
├── SweepRefresh.ets     // 下拉刷新组件
├── SweepOverlay.ets     // 全屏加载遮罩
└── SweepLoadMore.ets    // 上拉加载更多

所有默认值集中在 SweepDefaults.ets,三个组件共享,避免重复定义。SweepConfig 接口覆盖所有可自定义项,不传则全部走默认值,做到零配置开箱即用


三、安装使用

oh-package.json5 中添加依赖:

{
  "dependencies": {
    "sweeploading": "^1.0.0"
  }
}

四、组件使用说明

4.1 SweepRefresh 下拉刷新

属性 类型 必填 说明
isRefreshing boolean 双向绑定刷新状态($ 前缀)
content () => void 列表内容 Builder
onRefreshing () => void 触发刷新的回调
config SweepConfig 自定义配置
import { SweepRefresh } from 'sweeploading';

SweepRefresh({
  isRefreshing: $isRefreshing,
  onRefreshing: () => {
    fetchData().then(() => { this.isRefreshing = false; });
  },
  content: this.listBuilder.bind(this)
})

4.2 SweepOverlay 全屏加载遮罩

属性 类型 必填 说明
isVisible boolean 是否显示遮罩
config SweepConfig 自定义配置
import { SweepOverlay } from 'sweeploading';

// 必须放在 Stack 内,叠在内容上方
Stack() {
  MyPageContent()
  SweepOverlay({ isVisible: this.isLoading })
}
.width('100%').height('100%')

4.3 SweepLoadMore 上拉加载更多

属性 类型 必填 说明
isLoading boolean 是否正在加载
config SweepConfig hasMore / noMoreText
import { SweepLoadMore } from 'sweeploading';

List() {
  ForEach(this.items, ...)
  ListItem() {
    SweepLoadMore({
      isLoading: this.isLoadingMore,
      config: { hasMore: this.hasMore, noMoreText: '没有更多了' }
    })
  }
}
.onReachEnd(() => {
  if (this.isLoadingMore || !this.hasMore) return;
  this.isLoadingMore = true;
  loadMore().then(data => {
    if (data.length === 0) this.hasMore = false;
    else this.items = [...this.items, ...data];
    this.isLoadingMore = false;
  });
})

五、SweepConfig 完整配置项

属性 类型 默认值 适用组件 说明
lottieAnimationPath string 内置 lego 动画 全部 rawfile 路径,与 imageRes 二选一
imageRes Resource 全部 图片资源,与 lottieAnimationPath 二选一
shimmerBaseColor string #9A9078 全部 扫光基础色
shimmerHighlightColor string #FFE294 全部 扫光高亮色
shimmerSpeed number 0.008 全部 扫光速度,值越大越快
shimmerAngle number 90 全部 扫光角度(度)
loadingText string 随机文案 全部 加载提示文字
iconSize number 40 全部 图标尺寸(vp)
overlayColor string #121317 SweepOverlay 遮罩背景色
hasMore boolean true SweepLoadMore 是否还有更多数据
noMoreText string No more data SweepLoadMore 无更多数据时的文字

自定义示例

import { SweepConfig } from 'sweeploading';

// 蓝色扫光主题
const blueConfig: SweepConfig = {
  shimmerBaseColor: '#1a3a5c',
  shimmerHighlightColor: '#4FC3F7',
  shimmerSpeed: 0.012,
  loadingText: '加载中...',
};

// 替换为自定义 Lottie 动画
const lottieConfig: SweepConfig = {
  lottieAnimationPath: 'lottie/my_animation.json',
};

// 替换为图片资源
const imageConfig: SweepConfig = {
  imageRes: $r('app.media.loading'),
  iconSize: 56,
};

六、完整示例

import { SweepRefresh, SweepOverlay, SweepLoadMore } from 'sweeploading';

@Entry
@Component
struct DemoPage {
  @State isRefreshing: boolean = false;
  @State isLoadingMore: boolean = false;
  @State isPageLoading: boolean = true;
  @State items: string[] = [];
  @State hasMore: boolean = true;

  aboutToAppear() {
    fetchInitialData().then(data => {
      this.items = data;
      this.isPageLoading = false;
    });
  }

  build() {
    Stack() {
      SweepRefresh({
        isRefreshing: $isRefreshing,
        onRefreshing: () => {
          refresh().then(data => {
            this.items = data;
            this.isRefreshing = false;
          });
        },
        content: this.listContent.bind(this)
      })
      SweepOverlay({ isVisible: this.isPageLoading })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  listContent() {
    List() {
      ForEach(this.items, (item: string) => {
        ListItem() { Text(item) }
      })
      ListItem() {
        SweepLoadMore({
          isLoading: this.isLoadingMore,
          config: { hasMore: this.hasMore, noMoreText: '没有更多了' }
        })
      }
    }
    .onReachEnd(() => {
      if (this.isLoadingMore || !this.hasMore) return;
      this.isLoadingMore = true;
      loadMore().then(data => {
        if (data.length === 0) this.hasMore = false;
        else this.items = [...this.items, ...data];
        this.isLoadingMore = false;
      });
    })
  }
}

七、注意事项

  1. SweepOverlay 必须放在 Stack,且作为最后一个子节点,确保层级在内容之上
  2. SweepRefreshcontent 参数需要用 .bind(this) 绑定上下文
  3. 自定义 Lottie 动画需将 .json 文件放在模块的 src/main/resources/rawfile/ 目录下
  4. lottieAnimationPathimageRes 二选一,同时设置时 imageRes 优先

源码地址:gitcode- SweepLoading
欢迎 Star & Issue

Logo

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

更多推荐