【开发日志】循环背景斜向滚动
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,外部可传入。决定网格密度与回绕步长 |
| 这些参数可以通过外部传参的形式,或者直接内部定义。 |
- 外部传参,一般就是用在你需要让不同的页面速度有所不同,以及单片大小有所不同,可以传参调整。
- 内部定义,就是你直接自己定义后不做更改,让应用的背景界面统一而不有所差异。
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 偏移量区间
为确保护网始终覆盖视口顶部和左侧,offsetX和 offsetY 被约束在特定区间:
| 轴 | 区间 | 含义 |
|---|---|---|
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);
}
执行流程:
- 每隔 16ms,
offsetX减去speed(网格左移),offsetY加上speed(网格下移) - 检查
offsetX是否 ≤-tileSize,若是则加回tileSize(网格右跳到等效位置) - 检查
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 规律总结
- offsetX / offsetY 的符号决定网格移动方向,间接决定视觉滚动方向(视觉方向与网格移动方向相反)
- 回绕阈值取决于该轴偏移量的增减方向:
- 递减(
-= speed)→ 回绕点<= -tileSize,回绕操作+= tileSize - 递增(
+= speed)→ 回绕点>= 0,回绕操作-= tileSize
- 递减(
- 初始值需使网格在起始位置就覆盖视口:
- 递减轴初始
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 的第一个子元素(即最底层),否则会遮挡上层内容。不能在 Column 或 Row 中直接使用,否则无法铺满全屏。
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,外部可传入。决定网格密度与回绕步长 |
| 这些参数可以通过外部传参的形式,或者直接内部定义。 |
- 外部传参,一般就是用在你需要让不同的页面速度有所不同,以及单片大小有所不同,可以传参调整。
- 内部定义,就是你直接自己定义后不做更改,让应用的背景界面统一而不有所差异。
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 偏移量区间
为确保护网始终覆盖视口顶部和左侧,offsetX和 offsetY 被约束在特定区间:
| 轴 | 区间 | 含义 |
|---|---|---|
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);
}
执行流程:
- 每隔 16ms,
offsetX减去speed(网格左移),offsetY加上speed(网格下移) - 检查
offsetX是否 ≤-tileSize,若是则加回tileSize(网格右跳到等效位置) - 检查
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 规律总结
- offsetX / offsetY 的符号决定网格移动方向,间接决定视觉滚动方向(视觉方向与网格移动方向相反)
- 回绕阈值取决于该轴偏移量的增减方向:
- 递减(
-= speed)→ 回绕点<= -tileSize,回绕操作+= tileSize - 递增(
+= speed)→ 回绕点>= 0,回绕操作-= tileSize
- 递减(
- 初始值需使网格在起始位置就覆盖视口:
- 递减轴初始
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 的第一个子元素(即最底层),否则会遮挡上层内容。不能在 Column 或 Row 中直接使用,否则无法铺满全屏。
7.5 性能考量
| 项目 | 当前实现 | 说明 |
|---|---|---|
| 瓦片数量 | 6 个(3×2) | 固定不变,开销恒定 |
| 每帧操作 | translate 偏移更新 | GPU 加速,极低 CPU 开销 |
| 图片解码 | Image 组件懒加载 | ArkUI 框架负责缓存 |
| 定时器频率 | 60fps | 与屏幕刷新率对齐 |
当前 ForEach + Image 实现在绝大多数场景下性能良好。
更多推荐



所有评论(0)