1. 组件概述

在开发鸿蒙移动应用项目的时候,想让界面变得生动有趣起来,便思考加一个循环滚动的背景如下:

1. 组件概述

在开发鸿蒙移动应用项目的时候,想让界面变得生动有趣起来,便思考加一个循环滚动的背景如下:
循环滚动图示

1.1 用途

这个背景组件是一个用于 ArkUI 页面的斜向循环滚动背景组件。它以一张平铺无缝的背景纹理图片(background.png)作为素材,通过 2D 瓦片网格 + 定时器偏移的方式,在页面上产生背景沿对角线方向无限循环滚动的视觉效果。

1.2 视觉效果

  • 背景图像沿对角线方向(默认左下 → 右上)持续缓慢移动
  • 画面完全连续、无接缝,形成"无限循环"的观感
  • 背景层不响应触摸事件,不影响上层 UI 的正常交互

1.3 使用场景

在应用界面需要的时候例如首页、过渡页、游戏大厅页等等加入,可以营造动态氛围感,避免单调背景,也能够保持视觉的活跃度。

1.4 技术栈

  • 框架:HarmonyOS ArkUI (API 12+)
  • 语言:ArkTS
  • 核心机制:ForEach 网格 + translate 偏移 + setInterval 定时器

2. 参数接口

2.1 参数一览

参数名 类型 默认值 含义 注解
speed number 1.5 滚动速度(像素/帧,约 60fps) @State,外部可传入。值越大越快,推荐范围 0.3 ~ 3.0
tileSize number 500 单个瓦片尺寸(px,宽高相等) @State,外部可传入。决定网格密度与回绕步长
这些参数可以通过外部传参的形式,或者直接内部定义。
  1. 外部传参,一般就是用在你需要让不同的页面速度有所不同,以及单片大小有所不同,可以传参调整。
  2. 内部定义,就是你直接自己定义后不做更改,让应用的背景界面统一而不有所差异。

2.2 内部状态(不可外部传入)

状态 类型 初始值 说明
offsetX number 0 X 轴累计偏移量(单位 px)
offsetY number -tileSize Y 轴累计偏移量(单位 px)
timerId number -1 setInterval 返回的定时器标识

2.3 网格常量

private readonly ROWS: number[] = [0, 1, 2];    // 3 行
private readonly COLS: number[] = [0, 1];       // 2 列

瓦片数量用于排布背景图片,要实现循环而不露馅的话,因情而定,保证不会漏白边即可。若我的背景图片比较小,那么所需的瓦片数量就需要排布渲染更多,如果背景图片比较大,就可以减少。那我这个背景图片设定比较大,因此相应的瓦片数量就少了。
效果如下:
构建示意图

3. 实现原理

3.1 瓦片网格的构建方式

3.1.1 行列数计算

网格使用双层 ForEach 构建:

  • 外层 ForEach(ROWS) 生成 3 行(Row 组件)
  • 内层 ForEach(COLS) 每行生成 2 列(Image 组件)

网格结构示意(每个 [r,c] 代表一个 tileSize × tileSize 的 Image 瓦片):

       col0       col1
row0  [0,0]      [0,1]
row1  [1,0]      [1,1]
row2  [2,0]      [2,1]
3.1.2 覆盖范围

6 个瓦片的总覆盖面积为:

  • 宽度2 × tileSize = 2 × 500 = 1000px
  • 高度3 × tileSize = 3 × 500 = 1500px

网格通过 Column 组件 + Row 组件自然排列,每个 Image 宽高均为 tileSize,最终由外层 Stack.clip(true) 裁切到视口大小。

3.1.3 背景色保障

每个 Row 和整体 Column 均和背景色相同或相近,作为瓦片覆盖区域之外的底色作为保障——若极端场景下瓦片未能完全覆盖视口,底色与背景图主色调保持一致,不会出现突兀的纯白区域。

3.2 偏移与回绕机制

3.2.1 偏移方向

当前方向为左下 → 右上

this.offsetX -= this.speed;   // X 轴向左偏移(负方向)
this.offsetY += this.speed;   // Y 轴向下偏移(正方向)

由于网格通过 .translate({ x: offsetX, y: offsetY }) 平移,offsetX 向左为负、网格整体左移,视觉上内容向右上流动,即"网格向左下方移动 → 视觉滚动方向为右上"。

3.2 偏移与回绕机制

3.2.1 偏移方向

当前方向为左下 → 右上

this.offsetX -= this.speed;   // X 轴向左偏移(负方向)
this.offsetY += this.speed;   // Y 轴向下偏移(正方向)

由于网格通过 .translate({ x: offsetX, y: offsetY }) 平移,offsetX 向左为负、网格整体左移,视觉上内容向右上流动,即"网格向左下方移动 → 视觉滚动方向为右上"。

3.2.2 偏移量区间

为确保护网始终覆盖视口顶部和左侧,offsetXoffsetY 被约束在特定区间:

区间 含义
offsetX [-tileSize, 0) 网格左边界始终 ≤0(不露出右侧白边)
offsetY [-tileSize, 0) 网格上边界始终 ≤0(不露出下方白边)
3.2.3 初始值设定
aboutToAppear(): void {
    this.offsetY = -this.tileSize;   // 初始 -500,网格顶部在视口上方 500px
    this.startScroll();
}
  • offsetX 初始为 0(声明默认值),即网格左边界在视口左边缘
  • offsetY 初始为 -tileSize(-500),即网格顶部在视口上方 500px
3.2.4 回绕逻辑(无缝循环核心)

每帧(~16ms)执行以下回绕检测:

// X 轴:offsetX 向左递减到 ≤ -tileSize 时,加回 tileSize
if (this.offsetX <= -this.tileSize) {
    this.offsetX += this.tileSize;
}

// Y 轴:offsetY 向下递增到 ≥ 0 时,减回 tileSize
if (this.offsetY >= 0) {
    this.offsetY -= this.tileSize;
}

回绕原理图解(以 X 轴为例,Y 轴同理):

时间轴 →

offsetX:  -500  ...  -250  ...  -1    此时 offsetX = -500,触发回绕,加回 500 → 0
                                 ↓
offsetX    0    ...  -250  ...  -500   再次触发回绕...
                                 ↓
offsetX    0    ...  -250  ...         (循环往复)

由于每次回绕的步长恰好为 tileSize(即一个瓦片的尺寸),而网格比视口多出一整列缓冲瓦片,回绕跳变后的画面与回绕前的画面在视觉上完全一致

3.3 定时器与帧率控制

this.timerId = setInterval(() => {
    // 更新 offsetX、offsetY + 回绕检测
}, 16);  // ~60fps (1000ms / 60 ≈ 16.67ms)
项目 说明
帧率 约 60fps(16ms 间隔)
每帧位移 speed px(默认 1.5)
每秒位移 speed × 60 ≈ 90px/s
完整循环周期 tileSize / (speed × 60) 秒(默认 500 / 90 ≈ 5.6 秒)
生命周期管理
生命周期 操作
aboutToAppear() 设置 offsetY 初始负值 + 调用 startScroll() 启动定时器
aboutToDisappear() clearInterval(timerId) 销毁定时器,防止内存泄漏

3.4 触摸穿透

.hitTestBehavior(HitTestMode.None)

设置在最外层 Stack 上,背景层完全不响应任何触摸/点击/滑动手势,所有事件穿透到上层页面内容。

4. 关键代码解析

4.1 滚动定时器 — startScroll()

private startScroll(): void {
    this.timerId = setInterval(() => {
        // 1. 偏移累加(方向:左下→右上)
        this.offsetX -= this.speed;
        this.offsetY += this.speed;

        // 2. X 轴回绕
        if (this.offsetX <= -this.tileSize) {
            this.offsetX += this.tileSize;
        }

        // 3. Y 轴回绕
        if (this.offsetY >= 0) {
            this.offsetY -= this.tileSize;
        }
    }, 16);
}

执行流程

  1. 每隔 16ms,offsetX 减去 speed(网格左移),offsetY 加上 speed(网格下移)
  2. 检查 offsetX 是否 ≤ -tileSize,若是则加回 tileSize(网格右跳到等效位置)
  3. 检查 offsetY 是否 ≥ 0,若是则减回 tileSize(网格上跳到等效位置)

为什么回绕阈值不同?

  • offsetX 方向为递减(负向),回绕点为 ≤ -tileSize
  • offsetY 方向为递增(正向),回绕点为 ≥ 0
    阈值选取的根本原则:在偏移量即将使网格暴露空白区域前,提前回跳到等效位置

4.3 视图构建 — build()

build() {
    Stack() {
        Column() {                                          // 外层列容器(3 行)
            ForEach(this.ROWS, (row: number) => {
                Row() {                                     // 每行(2 列)
                    ForEach(this.COLS, (col: number) => {
                        Image($rawfile('background_animation/background.png'))
                            .width(this.tileSize)
                            .height(this.tileSize)
                    }, (col: number): string => `${row}_${col}`)
                }
                .backgroundColor('#fff9f0')                 // 行底色兜底
            }, (row: number): string => `r${row}`)
        }
        .translate({ x: this.offsetX, y: this.offsetY })   // 偏移变换
        .backgroundColor('#fff9f0')                         // 列底色兜底
    }
    .width('100%')
    .height('100%')
    .clip(true)                                             // 裁切超出视口部分
    .hitTestBehavior(HitTestMode.None)                      // 触摸穿透
}

⚠️注意:

外层Stack主要是用于裁切超出视口部分的内容,超出部分不会被绘制,减少 GPU 绘制面积,节省渲染资源。如果去掉Stack层直接从内部的Column层开始的话,会出现问题:
1、使用clip,那么会先渲染图片,然后再进行位移,就会导致滚动失效
2、不使用clip,那么就会一次性绘制出全部的背景,占用内存,资源开销大

关键属性说明

属性 位置 作用
.translate({ x, y }) Column 将整个网格平移 offsetX/offsetY,实现滚动效果
.clip(true) Stack 裁切超出的瓦片区域,不会撑破布局
.hitTestBehavior(HitTestMode.None) Stack 触摸穿透,事件到达上层页面
backgroundColor('#fff9f0') Row + Column 底色兜底,与背景图主色一致
$rawfile('...') Image entry/src/main/resources/rawfile/ 加载静态资源

5. 滚动方向切换

通过修改 startScroll()offsetX / offsetY 的符号和回绕条件,可以切换四种对角线方向。

5.1 方向总览

方向 offsetX 操作 offsetY 操作 X 回绕条件 Y 回绕条件 offsetX 初始 offsetY 初始
左上 → 右下 -= speed -= speed <= -tileSize → += tileSize <= -tileSize → += tileSize 0 0
左下 → 右上(当前) -= speed += speed <= -tileSize → += tileSize >= 0 → -= tileSize 0 -tileSize
右下 → 左上 += speed -= speed >= 0 → -= tileSize <= -tileSize → += tileSize -tileSize 0
右上 → 左下 += speed += speed >= 0 → -= tileSize >= 0 → -= tileSize -tileSize -tileSize

5.2 规律总结

  1. offsetX / offsetY 的符号决定网格移动方向,间接决定视觉滚动方向(视觉方向与网格移动方向相反)
  2. 回绕阈值取决于该轴偏移量的增减方向:
    • 递减(-= speed)→ 回绕点 <= -tileSize,回绕操作 += tileSize
    • 递增(+= speed)→ 回绕点 >= 0,回绕操作 -= tileSize
  3. 初始值需使网格在起始位置就覆盖视口:
    • 递减轴初始 0(网格左/上边界对齐视口)
    • 递增轴初始 -tileSize(网格在视口上方/左侧,随递增逐渐进入视口)

5.3 各方向完整代码

5.3.1 左上 → 右下
// aboutToAppear: offsetX = 0, offsetY = 0
this.offsetX -= this.speed;
this.offsetY -= this.speed;

if (this.offsetX <= -this.tileSize) this.offsetX += this.tileSize;
if (this.offsetY <= -this.tileSize) this.offsetY += this.tileSize;
5.3.2 左下 → 右上(当前实现)
// aboutToAppear: offsetX = 0, offsetY = -tileSize
this.offsetX -= this.speed;
this.offsetY += this.speed;

if (this.offsetX <= -this.tileSize) this.offsetX += this.tileSize;
if (this.offsetY >= 0) this.offsetY -= this.tileSize;
5.3.3 右下 → 左上
// aboutToAppear: offsetX = -tileSize, offsetY = 0
this.offsetX += this.speed;
this.offsetY -= this.speed;

if (this.offsetX >= 0) this.offsetX -= this.tileSize;
if (this.offsetY <= -this.tileSize) this.offsetY += this.tileSize;
5.3.4 右上 → 左下
// aboutToAppear: offsetX = -tileSize, offsetY = -tileSize
this.offsetX += this.speed;
this.offsetY += this.speed;

if (this.offsetX >= 0) this.offsetX -= this.tileSize;
if (this.offsetY >= 0) this.offsetY -= this.tileSize;

6. 使用方式

6.1 导入组件

import { BackgroundScroll } from '../component/BackgroundScroll';

6.2 页面中使用(Stack 布局)

@Entry
@Component
struct MyPage {
    build() {
        Stack() {
            // ===== 背景层 =====
            BackgroundScroll({ speed: 1.5, tileSize: 500 })

            // ===== 页面内容层 =====
            Column() {
                Text('页面标题')
                    .fontSize(32)
                    .fontColor(Color.White)
                // ... 其他 UI 元素
            }
            .width('100%')
            .height('100%')
        }
    }
}

背景层可以通过zIndex()设置背景层级,或者是在代码中放在其他组件的上方实现背景层叠下载方

6.3 参数调节示例

需求 参数 说明
加快滚动 speed: 3.0 约 180px/s,节奏明快
慢速滚动 speed: 0.5 约 30px/s,舒缓大气
大尺寸纹理 tileSize: 800 纹理图案更大、更稀疏
细密纹理 tileSize: 300 纹理更密、滚动周期更短

6.4 素材要求

素材路径:entry/src/main/resources/rawfile/background_animation/background.png

素材需满足:

  • 无缝平铺:图片上下左右边缘能自然衔接,平铺后无接缝线
  • 无白边:图片内容填满整张画布,边缘不得有白色/透明区域
  • 宽高相等:保持正方形,与 tileSize 正方形假设一致
  • 格式:PNG(推荐,支持透明)或 JPEG

7. 注意事项

7.1 背景图素材不要有白边

如果背景图边缘有白色或与主色不一致的边框,在瓦片拼接时会形成明显的网格线。确保图片边缘与内容自然衔接,做到真正无缝

7.2 触摸穿透

组件已内置 .hitTestBehavior(HitTestMode.None),无需额外处理。禁止在外部包裹再设置触摸事件拦截,否则上层页面将无法响应触摸。

7.3 定时器清理

组件销毁时 aboutToDisappear() 自动执行 clearInterval()。但若页面通过路由跳转而非销毁组件,定时器会继续运行。如有此场景,需在页面 onPageHide() 中手动暂停。

7.4 与 Stack 配合

必须作为 Stack第一个子元素(即最底层),否则会遮挡上层内容。不能在 ColumnRow 中直接使用,否则无法铺满全屏。

7.5 性能考量

项目 当前实现 说明
瓦片数量 6 个(3×2) 固定不变,开销恒定
每帧操作 translate 偏移更新 GPU 加速,极低 CPU 开销
图片解码 Image 组件懒加载 ArkUI 框架负责缓存
定时器频率 60fps 与屏幕刷新率对齐

当前 ForEach + Image 实现在绝大多数场景下性能良好。

1.1 用途

这个背景组件是一个用于 ArkUI 页面的斜向循环滚动背景组件。它以一张平铺无缝的背景纹理图片(background.png)作为素材,通过 2D 瓦片网格 + 定时器偏移的方式,在页面上产生背景沿对角线方向无限循环滚动的视觉效果。

1.2 视觉效果

  • 背景图像沿对角线方向(默认左下 → 右上)持续缓慢移动
  • 画面完全连续、无接缝,形成"无限循环"的观感
  • 背景层不响应触摸事件,不影响上层 UI 的正常交互

1.3 使用场景

在应用界面需要的时候例如首页、过渡页、游戏大厅页等等加入,可以营造动态氛围感,避免单调背景,也能够保持视觉的活跃度。

1.4 技术栈

  • 框架:HarmonyOS ArkUI (API 12+)
  • 语言:ArkTS
  • 核心机制:ForEach 网格 + translate 偏移 + setInterval 定时器

2. 参数接口

2.1 参数一览

参数名 类型 默认值 含义 注解
speed number 1.5 滚动速度(像素/帧,约 60fps) @State,外部可传入。值越大越快,推荐范围 0.3 ~ 3.0
tileSize number 500 单个瓦片尺寸(px,宽高相等) @State,外部可传入。决定网格密度与回绕步长
这些参数可以通过外部传参的形式,或者直接内部定义。
  1. 外部传参,一般就是用在你需要让不同的页面速度有所不同,以及单片大小有所不同,可以传参调整。
  2. 内部定义,就是你直接自己定义后不做更改,让应用的背景界面统一而不有所差异。

2.2 内部状态(不可外部传入)

状态 类型 初始值 说明
offsetX number 0 X 轴累计偏移量(单位 px)
offsetY number -tileSize Y 轴累计偏移量(单位 px)
timerId number -1 setInterval 返回的定时器标识

2.3 网格常量

private readonly ROWS: number[] = [0, 1, 2];    // 3 行
private readonly COLS: number[] = [0, 1];       // 2 列

瓦片数量用于排布背景图片,要实现循环而不露馅的话,因情而定,保证不会漏白边即可。若我的背景图片比较小,那么所需的瓦片数量就需要排布渲染更多,如果背景图片比较大,就可以减少。那我这个背景图片设定比较大,因此相应的瓦片数量就少了。
效果如下:
![[Pasted image 20260730223249.png|250]]

3. 实现原理

3.1 瓦片网格的构建方式

3.1.1 行列数计算

网格使用双层 ForEach 构建:

  • 外层 ForEach(ROWS) 生成 3 行(Row 组件)
  • 内层 ForEach(COLS) 每行生成 2 列(Image 组件)

网格结构示意(每个 [r,c] 代表一个 tileSize × tileSize 的 Image 瓦片):

       col0       col1
row0  [0,0]      [0,1]
row1  [1,0]      [1,1]
row2  [2,0]      [2,1]
3.1.2 覆盖范围

6 个瓦片的总覆盖面积为:

  • 宽度2 × tileSize = 2 × 500 = 1000px
  • 高度3 × tileSize = 3 × 500 = 1500px

网格通过 Column 组件 + Row 组件自然排列,每个 Image 宽高均为 tileSize,最终由外层 Stack.clip(true) 裁切到视口大小。

3.1.3 背景色保障

每个 Row 和整体 Column 均和背景色相同或相近,作为瓦片覆盖区域之外的底色作为保障——若极端场景下瓦片未能完全覆盖视口,底色与背景图主色调保持一致,不会出现突兀的纯白区域。

3.2 偏移与回绕机制

3.2.1 偏移方向

当前方向为左下 → 右上

this.offsetX -= this.speed;   // X 轴向左偏移(负方向)
this.offsetY += this.speed;   // Y 轴向下偏移(正方向)

由于网格通过 .translate({ x: offsetX, y: offsetY }) 平移,offsetX 向左为负、网格整体左移,视觉上内容向右上流动,即"网格向左下方移动 → 视觉滚动方向为右上"。

3.2 偏移与回绕机制

3.2.1 偏移方向

当前方向为左下 → 右上

this.offsetX -= this.speed;   // X 轴向左偏移(负方向)
this.offsetY += this.speed;   // Y 轴向下偏移(正方向)

由于网格通过 .translate({ x: offsetX, y: offsetY }) 平移,offsetX 向左为负、网格整体左移,视觉上内容向右上流动,即"网格向左下方移动 → 视觉滚动方向为右上"。

3.2.2 偏移量区间

为确保护网始终覆盖视口顶部和左侧,offsetXoffsetY 被约束在特定区间:

区间 含义
offsetX [-tileSize, 0) 网格左边界始终 ≤0(不露出右侧白边)
offsetY [-tileSize, 0) 网格上边界始终 ≤0(不露出下方白边)
3.2.3 初始值设定
aboutToAppear(): void {
    this.offsetY = -this.tileSize;   // 初始 -500,网格顶部在视口上方 500px
    this.startScroll();
}
  • offsetX 初始为 0(声明默认值),即网格左边界在视口左边缘
  • offsetY 初始为 -tileSize(-500),即网格顶部在视口上方 500px
3.2.4 回绕逻辑(无缝循环核心)

每帧(~16ms)执行以下回绕检测:

// X 轴:offsetX 向左递减到 ≤ -tileSize 时,加回 tileSize
if (this.offsetX <= -this.tileSize) {
    this.offsetX += this.tileSize;
}

// Y 轴:offsetY 向下递增到 ≥ 0 时,减回 tileSize
if (this.offsetY >= 0) {
    this.offsetY -= this.tileSize;
}

回绕原理图解(以 X 轴为例,Y 轴同理):

时间轴 →

offsetX:  -500  ...  -250  ...  -1    此时 offsetX = -500,触发回绕,加回 500 → 0
                                 ↓
offsetX    0    ...  -250  ...  -500   再次触发回绕...
                                 ↓
offsetX    0    ...  -250  ...         (循环往复)

由于每次回绕的步长恰好为 tileSize(即一个瓦片的尺寸),而网格比视口多出一整列缓冲瓦片,回绕跳变后的画面与回绕前的画面在视觉上完全一致

3.3 定时器与帧率控制

this.timerId = setInterval(() => {
    // 更新 offsetX、offsetY + 回绕检测
}, 16);  // ~60fps (1000ms / 60 ≈ 16.67ms)
项目 说明
帧率 约 60fps(16ms 间隔)
每帧位移 speed px(默认 1.5)
每秒位移 speed × 60 ≈ 90px/s
完整循环周期 tileSize / (speed × 60) 秒(默认 500 / 90 ≈ 5.6 秒)
生命周期管理
生命周期 操作
aboutToAppear() 设置 offsetY 初始负值 + 调用 startScroll() 启动定时器
aboutToDisappear() clearInterval(timerId) 销毁定时器,防止内存泄漏

3.4 触摸穿透

.hitTestBehavior(HitTestMode.None)

设置在最外层 Stack 上,背景层完全不响应任何触摸/点击/滑动手势,所有事件穿透到上层页面内容。

4. 关键代码解析

4.1 滚动定时器 — startScroll()

private startScroll(): void {
    this.timerId = setInterval(() => {
        // 1. 偏移累加(方向:左下→右上)
        this.offsetX -= this.speed;
        this.offsetY += this.speed;

        // 2. X 轴回绕
        if (this.offsetX <= -this.tileSize) {
            this.offsetX += this.tileSize;
        }

        // 3. Y 轴回绕
        if (this.offsetY >= 0) {
            this.offsetY -= this.tileSize;
        }
    }, 16);
}

执行流程

  1. 每隔 16ms,offsetX 减去 speed(网格左移),offsetY 加上 speed(网格下移)
  2. 检查 offsetX 是否 ≤ -tileSize,若是则加回 tileSize(网格右跳到等效位置)
  3. 检查 offsetY 是否 ≥ 0,若是则减回 tileSize(网格上跳到等效位置)

为什么回绕阈值不同?

  • offsetX 方向为递减(负向),回绕点为 ≤ -tileSize
  • offsetY 方向为递增(正向),回绕点为 ≥ 0
    阈值选取的根本原则:在偏移量即将使网格暴露空白区域前,提前回跳到等效位置

4.3 视图构建 — build()

build() {
    Stack() {
        Column() {                                          // 外层列容器(3 行)
            ForEach(this.ROWS, (row: number) => {
                Row() {                                     // 每行(2 列)
                    ForEach(this.COLS, (col: number) => {
                        Image($rawfile('background_animation/background.png'))
                            .width(this.tileSize)
                            .height(this.tileSize)
                    }, (col: number): string => `${row}_${col}`)
                }
                .backgroundColor('#fff9f0')                 // 行底色兜底
            }, (row: number): string => `r${row}`)
        }
        .translate({ x: this.offsetX, y: this.offsetY })   // 偏移变换
        .backgroundColor('#fff9f0')                         // 列底色兜底
    }
    .width('100%')
    .height('100%')
    .clip(true)                                             // 裁切超出视口部分
    .hitTestBehavior(HitTestMode.None)                      // 触摸穿透
}

⚠️注意:

外层Stack主要是用于裁切超出视口部分的内容,超出部分不会被绘制,减少 GPU 绘制面积,节省渲染资源。如果去掉Stack层直接从内部的Column层开始的话,会出现问题:
1、使用clip,那么会先渲染图片,然后再进行位移,就会导致滚动失效
2、不使用clip,那么就会一次性绘制出全部的背景,占用内存,资源开销大

关键属性说明

属性 位置 作用
.translate({ x, y }) Column 将整个网格平移 offsetX/offsetY,实现滚动效果
.clip(true) Stack 裁切超出的瓦片区域,不会撑破布局
.hitTestBehavior(HitTestMode.None) Stack 触摸穿透,事件到达上层页面
backgroundColor('#fff9f0') Row + Column 底色兜底,与背景图主色一致
$rawfile('...') Image entry/src/main/resources/rawfile/ 加载静态资源

5. 滚动方向切换

通过修改 startScroll()offsetX / offsetY 的符号和回绕条件,可以切换四种对角线方向。

5.1 方向总览

方向 offsetX 操作 offsetY 操作 X 回绕条件 Y 回绕条件 offsetX 初始 offsetY 初始
左上 → 右下 -= speed -= speed <= -tileSize → += tileSize <= -tileSize → += tileSize 0 0
左下 → 右上(当前) -= speed += speed <= -tileSize → += tileSize >= 0 → -= tileSize 0 -tileSize
右下 → 左上 += speed -= speed >= 0 → -= tileSize <= -tileSize → += tileSize -tileSize 0
右上 → 左下 += speed += speed >= 0 → -= tileSize >= 0 → -= tileSize -tileSize -tileSize

5.2 规律总结

  1. offsetX / offsetY 的符号决定网格移动方向,间接决定视觉滚动方向(视觉方向与网格移动方向相反)
  2. 回绕阈值取决于该轴偏移量的增减方向:
    • 递减(-= speed)→ 回绕点 <= -tileSize,回绕操作 += tileSize
    • 递增(+= speed)→ 回绕点 >= 0,回绕操作 -= tileSize
  3. 初始值需使网格在起始位置就覆盖视口:
    • 递减轴初始 0(网格左/上边界对齐视口)
    • 递增轴初始 -tileSize(网格在视口上方/左侧,随递增逐渐进入视口)

5.3 各方向完整代码

5.3.1 左上 → 右下
// aboutToAppear: offsetX = 0, offsetY = 0
this.offsetX -= this.speed;
this.offsetY -= this.speed;

if (this.offsetX <= -this.tileSize) this.offsetX += this.tileSize;
if (this.offsetY <= -this.tileSize) this.offsetY += this.tileSize;
5.3.2 左下 → 右上(当前实现)
// aboutToAppear: offsetX = 0, offsetY = -tileSize
this.offsetX -= this.speed;
this.offsetY += this.speed;

if (this.offsetX <= -this.tileSize) this.offsetX += this.tileSize;
if (this.offsetY >= 0) this.offsetY -= this.tileSize;
5.3.3 右下 → 左上
// aboutToAppear: offsetX = -tileSize, offsetY = 0
this.offsetX += this.speed;
this.offsetY -= this.speed;

if (this.offsetX >= 0) this.offsetX -= this.tileSize;
if (this.offsetY <= -this.tileSize) this.offsetY += this.tileSize;
5.3.4 右上 → 左下
// aboutToAppear: offsetX = -tileSize, offsetY = -tileSize
this.offsetX += this.speed;
this.offsetY += this.speed;

if (this.offsetX >= 0) this.offsetX -= this.tileSize;
if (this.offsetY >= 0) this.offsetY -= this.tileSize;

6. 使用方式

6.1 导入组件

import { BackgroundScroll } from '../component/BackgroundScroll';

6.2 页面中使用(Stack 布局)

@Entry
@Component
struct MyPage {
    build() {
        Stack() {
            // ===== 背景层 =====
            BackgroundScroll({ speed: 1.5, tileSize: 500 })

            // ===== 页面内容层 =====
            Column() {
                Text('页面标题')
                    .fontSize(32)
                    .fontColor(Color.White)
                // ... 其他 UI 元素
            }
            .width('100%')
            .height('100%')
        }
    }
}

背景层可以通过zIndex()设置背景层级,或者是在代码中放在其他组件的上方实现背景层叠下载方

6.3 参数调节示例

需求 参数 说明
加快滚动 speed: 3.0 约 180px/s,节奏明快
慢速滚动 speed: 0.5 约 30px/s,舒缓大气
大尺寸纹理 tileSize: 800 纹理图案更大、更稀疏
细密纹理 tileSize: 300 纹理更密、滚动周期更短

6.4 素材要求

素材路径:entry/src/main/resources/rawfile/background_animation/background.png

素材需满足:

  • 无缝平铺:图片上下左右边缘能自然衔接,平铺后无接缝线
  • 无白边:图片内容填满整张画布,边缘不得有白色/透明区域
  • 宽高相等:保持正方形,与 tileSize 正方形假设一致
  • 格式:PNG(推荐,支持透明)或 JPEG

7. 注意事项

7.1 背景图素材不要有白边

如果背景图边缘有白色或与主色不一致的边框,在瓦片拼接时会形成明显的网格线。确保图片边缘与内容自然衔接,做到真正无缝

7.2 触摸穿透

组件已内置 .hitTestBehavior(HitTestMode.None),无需额外处理。禁止在外部包裹再设置触摸事件拦截,否则上层页面将无法响应触摸。

7.3 定时器清理

组件销毁时 aboutToDisappear() 自动执行 clearInterval()。但若页面通过路由跳转而非销毁组件,定时器会继续运行。如有此场景,需在页面 onPageHide() 中手动暂停。

7.4 与 Stack 配合

必须作为 Stack第一个子元素(即最底层),否则会遮挡上层内容。不能在 ColumnRow 中直接使用,否则无法铺满全屏。

7.5 性能考量

项目 当前实现 说明
瓦片数量 6 个(3×2) 固定不变,开销恒定
每帧操作 translate 偏移更新 GPU 加速,极低 CPU 开销
图片解码 Image 组件懒加载 ArkUI 框架负责缓存
定时器频率 60fps 与屏幕刷新率对齐

当前 ForEach + Image 实现在绝大多数场景下性能良好。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐