摘要:本文通过 HarmonyOS 的 PullToRefresh 组件,结合 Canvas 绘图技术,实现具有动态小球特效的下拉刷新与上拉加载功能。文章将详细解析动画绘制原理、手势交互逻辑以及性能优化要点。
一、效果预览
实现功能包含:
- 弹性下拉刷新:带有透明度渐变的圆形聚合动画
- 波浪加载动画:三个小球按序弹跳的加载效果
- 数据动态加载:模拟异步数据请求与列表更新
- 流畅交互体验:支持列表惯性滑动与边缘回弹
二、核心实现原理
1. 组件结构设计
PullToRefresh({
// 数据绑定与配置
onRefresh: () {}, // 下拉回调
onLoadMore: () {}, // 上拉回调
onAnimPullDown: () {}, // 下拉动画绘制
onAnimRefreshing: () {}// 加载动画绘制
})
1.2.3.4.5.6.7.
复制
2. 动画阶段划分
阶段 | 进度范围 | 动画表现 |
---|---|---|
初始下拉 | 0%-33% | 中心圆渐显 |
展开过程 | 33%-75% | 两侧圆点分离 |
最大拉伸 | 75%-100% | 圆点二次扩散 |
加载状态 | - | 三点波浪动画 |
三、关键代码解析
1. 动画参数配置
// 设置最大下拉距离为130vp
.setMaxTranslate(130)
// 设置刷新动画时长为500ms
.setRefreshAnimDuration(500)
1.2.3.4.
复制
2. 自定义绘制逻辑
下拉过程绘制:
if (value <= 0.33) {
// 绘制中心圆(透明度渐变)
} else if (value <= 0.75) {
// 两点对称分离
} else {
// 两点二次扩散
}
1.2.3.4.5.6.7.
复制
加载动画实现:
// 使用队列实现动画延迟效果
if (this.value1.length === 7) {
this.drawPoint(/*...*/)
this.value1 = this.value1.splice(1)
}
1.2.3.4.5.
复制
3. 数据加载模拟
// 下拉刷新
setTimeout(() => {
this.data = this.numbers; // 重置数据
}, 2000);
// 上拉加载
this.data.push(''+(this.data.length+1));
1.2.3.4.5.6.7.
复制
四、性能优化技巧
- Canvas 绘制优化:
- 使用
clearRect
清空画布代替重新创建 - 限制绘制频率(本例使用进度值驱动)
- 列表渲染优化:
- 设置
.clip(true)
避免溢出渲染 - 使用
ForEach
进行数据驱动更新
- 动画参数调优:
- 调整 pointSpace 控制圆点间距
- 修改 pointJumpHeight 改变弹跳幅度
五、完整代码
import { PullToRefresh, PullToRefreshConfigurator } from '@ohos/pulltorefresh'
import util from '@ohos.util';
// 设置点间距与跳动高度
const pointSpace = 35;
const pointJumpHeight = 10;
@Entry
@Component
struct Index {
private numbers: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
@State data: string[] = this.numbers;
@State transparency: number = 1;
private value1: number[] = [];
private value2: number[] = [];
private scroller: Scroller = new Scroller();
private configurator: PullToRefreshConfigurator = new PullToRefreshConfigurator();
private setting: RenderingContextSettings = new RenderingContextSettings(true);
private refresh: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.setting);
len:number=0;
// 属性设置
aboutToAppear() {
this.configurator
// 下拉时间与下拉距离设置
.setRefreshAnimDuration(500)
.setMaxTranslate(130)
}
// 自定义图形绘画
private drawPoint(x: number, y: number): void {
this.refresh.beginPath();
this.refresh.arc(x,y,12,0,Math.PI*2);
this.refresh.fill();
}
@Builder
private customRefreshView() {
Column(){
Canvas(this.refresh)
.width('100%')
.height('75%')
.opacity(this.transparency)
Text('Loading...')
.fontSize(14)
.fontColor(Color.Black)
.margin({ top: 5 });
}
.width('100%')
.height('100%')
.clip(true)
}
@Builder
private getListView() {
List({ space: 15, scroller: this.scroller }) {
ForEach(this.data, (item: string) => {
ListItem() {
Row(){
Text(util.format('模块 %s',item))
.width('100%')
.height(130)
.fontSize(28)
.textAlign(TextAlign.Center)
.fontWeight(FontWeight.Bold)
.backgroundColor('#ffffff')
.borderRadius(15)
}
.width('100%')
.padding({right:10,left:10})
.alignItems(VerticalAlign.Center)
}
})
}
.backgroundColor('#eeeeee')
.padding({top:15,bottom:15})
.divider({ strokeWidth: 1, color: '#e0e0e0' })
// 设置列表为滑动到边缘无效果
.edgeEffect(EdgeEffect.None)
}
build() {
Column() {
PullToRefresh({
data: $data,
scroller: this.scroller,
customList: () => {
this.getListView();
},
refreshConfigurator: this.configurator,
// 下拉刷新
onRefresh: () => {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
resolve('Success');
this.data = this.numbers;
}, 2000);
});
},
// 上拉加载
onLoadMore: () => {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
resolve('');
this.len=this.data.length+1
this.data.push(''+this.len);
}, 2000);
});
},
// 自定义刷新动画
customRefresh: () => {
this.customRefreshView();
},
// 下拉回调
onAnimPullDown: (value, width, height) => {
if (value !== undefined && width !== undefined && height !== undefined) {
this.refresh.clearRect(0, 0, width, height);
if (value <= 0.33) {
this.transparency = value * 2;
this.drawPoint(width / 2, height / 2);
} else if (value <= 0.75) {
this.transparency = 1;
this.drawPoint(width / 2 - (pointSpace / 2 * (value - 0.33) / (0.75 - 0.33)), height / 2);
this.drawPoint(width / 2 + (pointSpace / 2 * (value - 0.33) / (0.75 - 0.33)), height / 2);
} else {
this.drawPoint(width / 2, height / 2);
this.drawPoint(width / 2 - pointSpace / 2 - (pointSpace / 2 * (value - 0.75) / (1 - 0.75)), height / 2);
this.drawPoint(width / 2 + pointSpace / 2 + (pointSpace / 2 * (value - 0.75) / (1 - 0.75)), height / 2);
}
}
},
// 刷新回调
onAnimRefreshing: (value, width, height) => {
if (value !== undefined && width !== undefined && height !== undefined) {
this.refresh.clearRect(0, 0, width, height);
value = Math.abs(value * 2 - 1) * 2 - 1;
// 绘制第1个点
this.drawPoint(width / 2 - pointSpace, height / 2 + pointJumpHeight * value);
// 绘制第2个点
if (this.value1.length === 7) {
this.drawPoint(width / 2, height / 2 + pointJumpHeight * this.value1[0]);
this.value1 = this.value1.splice(1, this.value1.length);
} else {
this.drawPoint(width / 2, height / 2 + pointJumpHeight);
}
this.value1.push(value);
// 绘制第3个点
if (this.value2.length === 14) {
this.drawPoint(width / 2 + pointSpace, height / 2 + pointJumpHeight * this.value2[0]);
this.value2 = this.value2.splice(1, this.value2.length);
} else {
this.drawPoint(width / 2 + pointSpace, height / 2 + pointJumpHeight);
}
this.value2.push(value);
}
},
customLoad: null,
})
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。