摘要:本文通过 HarmonyOS 的 PullToRefresh 组件,结合 Canvas 绘图技术,实现具有动态小球特效的下拉刷新与上拉加载功能。文章将详细解析动画绘制原理、手势交互逻辑以及性能优化要点。
一、效果预览
实现功能包含:
- 弹性下拉刷新:带有透明度渐变的圆形聚合动画
- 波浪加载动画:三个小球按序弹跳的加载效果
- 数据动态加载:模拟异步数据请求与列表更新
- 流畅交互体验:支持列表惯性滑动与边缘回弹
二、核心实现原理
1. 组件结构设计
2. 动画阶段划分
阶段 | 进度范围 | 动画表现 |
初始下拉 | 0%-33% | 中心圆渐显 |
展开过程 | 33%-75% | 两侧圆点分离 |
最大拉伸 | 75%-100% | 圆点二次扩散 |
加载状态 | - | 三点波浪动画 |
三、关键代码解析
1. 动画参数配置
2. 自定义绘制逻辑
下拉过程绘制:
加载动画实现:
3. 数据加载模拟
四、性能优化技巧
- 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,
})
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.