HarmonyOS 开源库 | SweepLoading:扫光风格下拉刷新 / 全屏加载 / 上拉加载更多
本文介绍了一个基于HarmonyOS ArkTS实现的开源UI组件库SweepLoading,包含三个核心组件:带扫光动效的下拉刷新(SweepRefresh)、全屏加载遮罩(SweepOverlay)和上拉加载更多(SweepLoadMore)。组件通过shaderStyle修饰符实现扫光效果,利用动态渐变和定时器更新产生光效动画。采用模块化设计,统一配置接口和默认值,支持高度自定义。使用简单,
本文介绍一个基于 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.2 到 shimmerPos + 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双向绑定,系统可以自动将其置为 truebuilder传函数引用(不加括号),避免立即执行导致渲染异常@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;
});
})
}
}
七、注意事项
SweepOverlay必须放在Stack内,且作为最后一个子节点,确保层级在内容之上SweepRefresh的content参数需要用.bind(this)绑定上下文- 自定义 Lottie 动画需将
.json文件放在模块的src/main/resources/rawfile/目录下 lottieAnimationPath和imageRes二选一,同时设置时imageRes优先
源码地址:gitcode- SweepLoading
欢迎 Star & Issue
更多推荐

所有评论(0)