HarmonyOS Next-- 实现炫酷下拉刷新与上拉加载

整岛铁盒
发布于 2025-3-21 20:13
浏览
0收藏

摘要:本文通过 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.
  • 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.

收藏
回复
举报
回复
    相关推荐