摘要:本文通过 HarmonyOS 的 PullToRefresh 组件,结合 Canvas 绘图技术,实现具有动态小球特效的下拉刷新与上拉加载功能。文章将详细解析动画绘制原理、手势交互逻辑以及性能优化要点。

一、效果预览

实现功能包含:

  1. 弹性下拉刷新:带有透明度渐变的圆形聚合动画
  2. 波浪加载动画:三个小球按序弹跳的加载效果
  3. 数据动态加载:模拟异步数据请求与列表更新
  4. 流畅交互体验:支持列表惯性滑动与边缘回弹

二、核心实现原理

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.

复制

四、性能优化技巧

  1. Canvas 绘制优化
  • 使用clearRect 清空画布代替重新创建
  • 限制绘制频率(本例使用进度值驱动)
  1. 列表渲染优化
  • 设置.clip(true) 避免溢出渲染
  • 使用ForEach 进行数据驱动更新
  1. 动画参数调优
  • 调整 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 声望0 粉丝