项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前言

在现代移动应用开发中,视差滚动(Parallax Scrolling)作为一种极具视觉冲击力的 UI 交互技术,被广泛应用于相册浏览、故事阅读、引导页面等场景。通过让不同层级的元素以不同速度滚动,视差效果能够营造出强烈的空间纵深感,给用户带来沉浸式的交互体验。

HarmonyOS NEXT 作为华为推出的全新一代操作系统,其 ArkUI 声明式开发框架为开发者提供了强大而灵活的 UI 构建能力。本文将深入探讨如何利用 ArkUI 的 Scroll 组件配合 .translate() 属性,从零开始实现一个多层级的视差滚动效果。无论你是 ArkTS 的初学者还是有一定经验的开发者,都能从本文中学到实用的开发技巧和最佳实践。


第一章:视差效果原理与视觉心理学

1.1 什么是视差效果

视差(Parallax)源于希腊语 “parallaxos”,意为"改变位置"或"差异"。在视觉领域,视差指的是当观察者移动时,前景物体相对于背景物体产生的明显位置变化。这一现象是人类感知深度的重要线索之一。

视差效果的核心原理:

  • 当你向前移动时,离你较近的物体在视野中移动得更快、更远
  • 较远的物体则移动得更慢、更近
  • 这种速度差异被大脑解读为深度差异

1.2 视差效果在移动应用中的应用场景

视差滚动技术已成为现代移动应用设计的标配,以下是几个典型的应用场景:

应用场景 描述 案例
启动引导页 用户首次打开应用时,通过视差效果展示产品特色 各大 App 的新手引导
内容浏览 让背景元素与内容产生互动,提升阅读体验 Medium、Instagram
相册展示 图片以视差方式呈现,增强立体感 苹果相册、Google Photos
地图导航 道路与建筑以不同层次呈现 高德地图、Google Maps
游戏界面 角色与背景分离滚动,增强游戏感 横版过关游戏、卡牌游戏

1.3 多层视差的实现模型

一个完整的视差系统通常由以下几层构成:

┌─────────────────────────────────────────┐
│  前景层 (Foreground Layer)               │  ← 移动速度 > 1.0x
│  - 主要交互元素                          │  - 可以快速滑过,营造"飞跃"感
├─────────────────────────────────────────┤
│  内容层 (Content Layer)                  │  ← 移动速度 = 1.0x
│  - 用户可滚动的内容                      │  - 正常滚动速度
├─────────────────────────────────────────┤
│  中景层 (Midground Layer)                │  ← 移动速度 = 0.5~0.8x
│  - 装饰性图形或次要元素                  │  - 较慢速度跟随
├─────────────────────────────────────────┤
│  远景层 (Background Layer)               │  ← 移动速度 = 0.2~0.4x
│  - 模糊或淡化的背景                      │  - 最慢速度移动
├─────────────────────────────────────────┤
│  背景层 (Base Layer)                     │  ← 移动速度 = 0x
│  - 固定不动的底色或渐变                  │  - 完全静止
└─────────────────────────────────────────┘

1.4 速度系数的数学关系

视差效果的本质是通过速度系数(Speed Factor)来控制不同层的移动速度:

层的实际位移 = 滚动偏移量 × 速度系数

速度系数选择原则:

  • 前景层:速度系数 > 1.0(通常 1.2~2.0),营造快速移动的感觉
  • 内容层:速度系数 = 1.0,作为基准参考
  • 中景层:速度系数 = 0.4~0.8,缓慢跟随
  • 远景层:速度系数 = 0.1~0.3,几乎不动
  • 背景层:速度系数 = 0,完全静止

速度系数对照表:

层级名称 推荐速度系数 视觉效果 典型内容
前景层 1.5 ~ 2.5 快速飞过 飞鸟、飘落物、光线
中前景层 1.1 ~ 1.4 略快于内容 近景装饰、侧边栏
内容层 1.0 正常滚动 文章、卡片、列表
中后景层 0.5 ~ 0.9 缓慢跟随 建筑、树木
远景层 0.1 ~ 0.4 几乎不动 山脉、云朵、星空
固定层 0.0 完全静止 渐变背景、底色

第二章:HarmonyOS NEXT 开发环境准备

2.1 系统要求

在开始之前,请确保你的开发环境满足以下要求:

硬件要求:

  • CPU:Intel i5 / AMD Ryzen 5 及以上
  • 内存:至少 16GB RAM(推荐 32GB)
  • 硬盘:至少 50GB 可用空间

操作系统:

  • Windows 10 64位 / Windows 11 64位
  • macOS 10.15 及以上(支持 Apple Silicon)

开发工具版本:

  • DevEco Studio 5.0 Release 及以上
  • SDK API 24 及以上(HarmonyOS NEXT)
  • Node.js 16.x 及以上

2.2 创建 HarmonyOS NEXT 项目

步骤一:启动 DevEco Studio

打开 DevEco Studio,在欢迎界面选择 “Create Project”。

步骤二:选择项目模板

在项目模板选择界面,选择 “Application” → “Empty Ability”(空白能力),点击 “Next”。

步骤三:配置项目参数
参数 说明
Project name ParallaxDemo 项目名称
Bundle name com.example.parallaxdemo 应用标识
Save location 自定义路径 项目保存位置
Compile SDK HarmonyOS NEXT (API 24) 目标 SDK 版本
Model Stage model 应用模型
步骤四:完成项目创建

点击 “Finish” 完成项目创建,DevEco Studio 将自动下载依赖并初始化项目结构。

2.3 项目目录结构说明

创建完成后,项目结构如下:

ParallaxDemo/
├── entry/                          # 应用模块
│   └── src/
│       └── main/
│           ├── ets/
│           │   ├── entryability/
│           │   │   └── EntryAbility.ets    # 入口 Ability
│           │   └── pages/
│           │       └── Index.ets           # 主页面(我们的目标文件)
│           ├── resources/                  # 资源文件
│           │   ├── base/
│           │   │   ├── element/            # 颜色、尺寸等
│           │   │   ├── media/              # 图片资源
│           │   │   └── profile/            # 快捷方式图标
│           │   └── zh_CN/                  # 中文国际化资源
│           └── module.json5               # 模块配置
├── build-profile.json5
├── hvigle.ts
├── oh-package.json5                       # 依赖管理
└── AppScope/
    ├── app.json5                          # 应用级配置
    └── resources/                         # 应用级资源

2.4 配置 SDK 和签名

配置 SDK 路径
  1. 打开 “File” → “Settings”(Windows)或 “DevEco Studio” → “Preferences”(macOS)
  2. 找到 “SDK” 选项
  3. 确认已安装 HarmonyOS NEXT (API 24) SDK
  4. 如果未安装,点击 “Apply” 后自动下载
配置应用签名
  1. 打开 “File” → “Project Structure”
  2. 选择 “Signing Configs”
  3. 勾选 “Automatically generate signature”
  4. 点击 “OK” 保存配置

2.5 运行第一个应用

在模拟器或真机上运行应用,确认项目配置正确:

  1. 点击工具栏的运行按钮 ▶️
  2. 选择模拟器设备(如没有,点击 “Device Manager” 创建)
  3. 等待应用启动,看到 “Hello World” 即表示环境就绪

第三章:核心组件与 API 详解

在开始编写视差效果代码之前,我们需要深入了解 ArkUI 中实现视差效果所涉及的核心组件和 API。

3.1 Stack 组件:多层布局的基础

Stack 是 ArkUI 中最基础的堆叠容器,所有子组件按照添加顺序层叠放置。

基础用法
Stack() {
  // 子组件1(最底层)
  Text('底层')
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Blue)
  
  // 子组件2(中层)
  Text('中层')
    .width('50%')
    .height('50%')
    .backgroundColor(Color.Red)
  
  // 子组件3(最顶层)
  Text('顶层')
    .width('30%')
    .height('30%')
    .backgroundColor(Color.Green)
}
.width('100%')
.height('100%')
关键属性
属性 类型 说明
alignContent Alignment 设置子组件默认对齐方式
clipContent boolean 是否裁剪超出 Stack 边界的子组件
hitTestBehavior HitTestMode 命中测试模式
zIndex 控制层级

通过 .zIndex(number) 属性可以精确控制子组件的层叠顺序:

Stack() {
  Text('zIndex=1')
    .zIndex(1)  // 层级最低
  
  Text('zIndex=3')
    .zIndex(3)  // 层级最高
  
  Text('zIndex=2')
    .zIndex(2)  // 中间层级
}

在视差场景中的应用: 我们使用 Stack 作为根容器,将前景、内容、中景、背景四层分别放在不同的 zIndex 上。

3.2 Scroll 组件:视差的动力源泉

Scroll 是实现视差效果的核心组件,它负责处理用户的滚动手势并触发回调。

基础用法
Scroll() {
  Column() {
    // 内容区域,高度超过 Scroll 时可滚动
    Text('内容1')
    Text('内容2')
    Text('内容3')
    // ... 更多内容
  }
}
.width('100%')
.height('100%')
.scrollable(ScrollDirection.Vertical)  // 设置滚动方向
关键属性
属性 类型 说明
scrollable ScrollDirection 滚动方向(Vertical/Horizontal)
scrollBar BarState 滚动条显示状态(Auto/On/Off/Idle)
edgeEffect EdgeEffect 边缘效果(Spring/Fade/None)
scrollWidth Length 滚动条宽度
scrollColor ResourceColor 滚动条颜色
滚动事件回调

Scroll 组件提供了多个滚动相关的事件回调:

onScroll:滚动过程回调(高频触发)

Scroll() {
  // ...
}
.onScroll((xOffset: number, yOffset: number) => {
  // xOffset: 水平滚动增量(每帧)
  // yOffset: 垂直滚动增量(每帧)
  // 注意:这是增量值,不是绝对位置!
  console.log('滚动增量:', xOffset, yOffset)
})

onWillScroll:滚动前回调

Scroll() {
  // ...
}
.onWillScroll((xOffset: number, yOffset: number) => {
  // 在滚动开始前触发,可以用于拦截或修改滚动行为
})

onScrollFrameBegin:滚动帧开始回调

Scroll() {
  // ...
}
.onScrollFrameBegin((xOffset: number, yOffset: number) => {
  // 返回值可以调整实际滚动量
  // 返回 { x: 0, y: 0 } 可以阻止滚动
  return { x: xOffset, y: yOffset }
})

onDidScroll:滚动完成回调

Scroll() {
  // ...
}
.onDidScroll(() => {
  // 滚动动画结束后触发
})
Scroller 控制器

ScrollerScroll 的控制器,用于程序化控制滚动位置:

// 定义 Scroller 实例
private scroller: Scroller = new Scroller()

build() {
  Column() {
    // 绑定 Scroller 到 Scroll
    Scroll(this.scroller) {
      // ...
    }
    
    // 控制按钮
    Button('滚动到顶部')
      .onClick(() => {
        this.scroller.scrollTo({ xOffset: 0, yOffset: 0 })
      })
    
    Button('滚动到底部')
      .onClick(() => {
        this.scroller.scrollEdge(Edge.Bottom)
      })
  }
}

获取当前滚动位置:

Button('获取位置')
  .onClick(() => {
    const offset = this.scroller.currentOffset()
    console.log('当前位置:', offset.xOffset, offset.yOffset)
  })

3.3 .translate():实现位移的关键

.translate() 是 ArkUI 的通用属性方法,用于改变组件的位置而不影响其布局空间。

基础用法
Text('移动我')
  .translate({ x: 100, y: 50 })  // 向右移动100vp,向下移动50vp
参数说明
参数 类型 说明
x number 水平方向偏移量(vp 单位),正数向右,负数向左
y number 垂直方向偏移量(vp 单位),正数向下,负数向上
z number Z轴方向偏移量(用于 3D 变换)
在视差中的应用

Scroll 的滚动偏移量应用到 .translate() 上,即可实现视差效果:

// 远景层 - 慢速移动
Column() {
  // ...
}
.translate({ x: 0, y: -scrollOffset * 0.3 })  // 0.3倍速

// 内容层 - 正常速度
Scroll() {
  // ...
}
// 内容层本身在滚动,不需要额外 translate

// 前景层 - 快速移动
Column() {
  // ...
}
.translate({ x: 0, y: -scrollOffset * 1.5 })  // 1.5倍速

3.4 @State:响应式状态管理

@State 装饰器是 ArkUI 声明式 UI 的核心,用于声明组件内部的响应式状态变量。

基础用法
@Component
struct MyComponent {
  // 声明状态变量
  @State scrollOffset: number = 0
  
  build() {
    Column() {
      Text(`偏移量: ${this.scrollOffset}`)
      
      Scroll() {
        // ...
      }
      .onScroll((x, y) => {
        // 修改状态变量,触发 UI 更新
        this.scrollOffset += y
      })
    }
  }
}
响应式机制
用户滚动 → Scroll.onScroll 触发 → 修改 @State 变量 → UI 重新渲染 → .translate() 更新位置

注意事项:

  1. @State 只能在组件内部使用
  2. 修改 @State 变量会同步触发组件的 build() 方法重新执行
  3. 避免在 onScroll 中执行耗时操作,以免影响滚动流畅度

3.5 .hitTestBehavior():解决触摸穿透问题

当多层元素叠加时,上层元素可能会阻挡下层元素的触摸事件。使用 .hitTestBehavior() 可以解决这个问题。

可选值
说明
Default 默认行为:响应触摸事件,不穿透
Block 阻止触摸事件传递
Transparent 透明模式:本身不响应触摸,事件穿透到下层
None 完全不参与命中测试
视差场景的应用
Stack() {
  // 背景层 - 需要穿透触摸
  Column() {
    // ...
  }
  .hitTestBehavior(HitTestMode.Transparent)
  
  // 内容层 - 需要响应触摸
  Scroll() {
    // ...
  }
  
  // 前景层 - 需要穿透触摸
  Column() {
    // ...
  }
  .hitTestBehavior(HitTestMode.Transparent)
}

3.6 .zIndex():控制层叠顺序

.zIndex() 控制子组件在 Stack 中的层叠顺序。数值越大,显示越靠前。

Stack() {
  // zIndex 越小越靠下
  BackgroundLayer().zIndex(1)  // 最底层
  MidLayer().zIndex(2)        // 中景层
  ContentLayer().zIndex(3)    // 内容层
  ForegroundLayer().zIndex(4) // 最顶层
}

3.7 .linearGradient():渐变背景

用于创建渐变色背景,为视差效果增添视觉层次。

Stack() {
  // ...
}
.width('100%')
.height('100%')
.linearGradient({
  angle: 180,              // 渐变角度(0-360)
  colors: [                // 颜色节点数组
    ['#87CEEB', 0],        // [颜色, 位置比例]
    ['#E0F6FF', 0.4],
    ['#FFE4B5', 0.7],
    ['#98D8C8', 1]
  ],
  direction: GradientDirection.Bottom  // 渐变方向
})

第四章:完整代码实现

现在让我们开始编写完整的视差效果代码。

4.1 代码结构规划

@Entry
@Component
struct ParallaxDemo {
  // 1. 状态声明
  @State scrollOffset: number = 0
  private scroller: Scroller = new Scroller()
  
  // 2. 主布局
  build() {
    Stack() {
      // 背景层(固定)
      // 远景层(0.3x 速度)
      // 中景层(0.6x 速度)
      // 内容层(1.0x 速度 - Scroll)
      // 前景层(1.5x 速度)
    }
    .linearGradient(...)
  }
  
  // 3. 自定义组件构建器
  @Builder
  ContentCardGroup(...) { }
  
  @Builder
  ContentCard(...) { }
}

4.2 完整代码清单

将以下代码写入 Index.ets 文件:

// ============================================================
// 文件: Index.ets
// 功能: 多层视差滚动效果演示
// 适用 API: 24 (HarmonyOS NEXT)
// ============================================================

@Entry
@Component
struct Index {
  // 视差核心状态:存储滚动累计偏移量
  @State scrollOffset: number = 0;
  
  // Scroll 组件控制器
  private scroller: Scroller = new Scroller();

  build() {
    // 根容器:Stack 用于多层叠加
    Stack() {
      // ========== 第1层:远景层 ==========
      // 速度系数 0.3x,最慢,zIndex 最低
      Column() {
        Text('🏔️')
          .fontSize(200)
          .margin({ top: 50 })
        Text('⛰️')
          .fontSize(180)
          .margin({ top: -30, left: 100 })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Start)
      .alignItems(HorizontalAlign.Start)
      .translate({ x: 0, y: -this.scrollOffset * 0.3 })
      .zIndex(1)
      .opacity(0.6)
      .hitTestBehavior(HitTestMode.Transparent)

      // ========== 第2层:中景层 ==========
      // 速度系数 0.6x,中速
      Column() {
        Row() {
          Text('🌲').fontSize(120)
          Text('🌳').fontSize(100).margin({ left: 50 })
          Text('🌲').fontSize(130).margin({ left: 80 })
        }
        .margin({ top: 200 })

        Row() {
          Text('🏢').fontSize(80)
          Text('🏠').fontSize(100).margin({ left: 60 })
          Text('🏬').fontSize(90).margin({ left: 40 })
        }
        .margin({ top: 100 })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Start)
      .alignItems(HorizontalAlign.Start)
      .translate({ x: 0, y: -this.scrollOffset * 0.6 })
      .zIndex(2)
      .hitTestBehavior(HitTestMode.Transparent)

      // ========== 第3层:内容层 ==========
      // 速度系数 1.0x,Scroll 组件本身在滚动
      Scroll(this.scroller) {
        Column({ space: 20 }) {
          Text('视差效果演示')
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
            .width('100%')
            .margin({ top: 100, bottom: 50 })

          Text('使用 Scroll + .translate() 实现多层视差')
            .fontSize(16)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
            .width('100%')
            .margin({ bottom: 100 })

          this.ContentCardGroup('自然景观', 
            ['🌅 日出', '🌄 日落', '🌊 海浪', '🌲 森林'])
          this.ContentCardGroup('城市建筑', 
            ['🏛️ 博物馆', '🏢 摩天楼', '🏗️ 工地', '🗼 铁塔'])
          this.ContentCardGroup('美食佳肴', 
            ['🍕 披萨', '🍔 汉堡', '🍜 面条', '🍣 寿司'])
          this.ContentCardGroup('动物世界', 
            ['🦁 狮子', '🐘 大象', '🦒 长颈鹿', '🦋 蝴蝶'])

          Blank().height(200)
        }
        .width('100%')
        .padding({ left: 20, right: 20 })
      }
      .width('100%')
      .height('100%')
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.Spring)
      .onScroll((xOffset: number, yOffset: number) => {
        this.scrollOffset += yOffset;
      })
      .zIndex(3)

      // ========== 第4层:前景层 ==========
      // 速度系数 1.5x,最快,zIndex 最高
      Column() {
        Row() {
          Text('🕊️').fontSize(40)
          Text('🦅').fontSize(50).margin({ left: 150, top: 30 })
        }
        .margin({ top: 80 })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Start)
      .alignItems(HorizontalAlign.Start)
      .translate({ x: 0, y: -this.scrollOffset * 1.5 })
      .zIndex(4)
      .hitTestBehavior(HitTestMode.Transparent)
    }
    .width('100%')
    .height('100%')
    .linearGradient({
      direction: GradientDirection.Bottom,
      colors: [
        ['#87CEEB', 0],
        ['#E0F6FF', 0.4],
        ['#FFE4B5', 0.7],
        ['#98D8C8', 1]
      ]
    })
  }

  // ========== 自定义组件 ==========
  
  @Builder
  ContentCardGroup(title: string, items: string[]) {
    Column() {
      Text(title)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 15, left: 10 })

      Grid() {
        ForEach(items, (item: string, index: number) => {
          GridItem() {
            this.ContentCard(item, index)
          }
        }, (item: string) => item)
      }
      .columnsTemplate('1fr 1fr')
      .rowsGap(15)
      .columnsGap(15)
      .width('100%')
      .height(200)
    }
    .width('100%')
    .padding(15)
    .backgroundColor('rgba(255, 255, 255, 0.85)')
    .borderRadius(20)
    .shadow({ radius: 10, color: 'rgba(0,0,0,0.15)', offsetX: 0, offsetY: 5 })
    .margin({ bottom: 10 })
  }

  @Builder
  ContentCard(content: string, index: number) {
    Column() {
      Text(content)
        .fontSize(40)
        .margin({ bottom: 8 })
      Text(`卡片 ${index + 1}`)
        .fontSize(14)
        .fontColor('#666666')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F5F5F5')
    .borderRadius(15)
  }
}

4.3 逐行解析

1. 状态声明部分:

@State scrollOffset: number = 0;

这是整个视差效果的核心。@State 装饰器声明了一个响应式状态变量,初始值为 0。当这个值改变时,所有使用它的 UI 元素会自动更新。

2. onScroll 回调部分:

.onScroll((xOffset: number, yOffset: number) => {
  this.scrollOffset += yOffset;
})

这里有两个关键点:

  • onScroll 的参数是增量值,不是绝对位置
  • 必须使用 += 累加,而不是 = 赋值

3. .translate() 绑定部分:

.translate({ x: 0, y: -this.scrollOffset * 0.3 })

负号表示向上移动,0.3 是速度系数。将状态变量绑定到 translate 上,实现了滚动驱动的位移效果。

4. .hitTestBehavior() 设置:

.hitTestBehavior(HitTestMode.Transparent)

确保非 Scroll 层不会拦截触摸事件,让用户能够正常滚动内容。

4.4 运行效果说明

运行应用后,你将看到:

  1. 屏幕展示从天空到地面的渐变色背景
  2. 远景的山脉图标缓慢移动(0.3x 速度)
  3. 中景的树木和建筑较快移动(0.6x 速度)
  4. 前景的飞鸟快速飞过(1.5x 速度)
  5. 中间的内容卡片正常滚动(1.0x 速度)

上下滑动屏幕,可以感受到明显的空间纵深感,仿佛在一个三维场景中穿行。


第五章:关键技术点深度分析

5.1 Scroll.onScroll 的增量特性

这是实现视差效果最容易踩坑的地方。

onScroll 的参数含义:

.onScroll((xOffset: number, yOffset: number) => {
  // 很多人误以为这是绝对滚动位置
  // 实际上是"本次滚动帧的增量"
})

错误写法:

.onScroll((xOffset: number, yOffset: number) => {
  // 错误:直接赋值,每次只能得到很小的增量
  this.scrollOffset = yOffset;
})

正确写法:

.onScroll((xOffset: number, yOffset: number) => {
  // 正确:累加到之前的偏移量
  this.scrollOffset += yOffset;
})

使用 Scroller 获取绝对位置:

// 绑定 Scroller 到 Scroll
Scroll(this.scroller) {
  // ...
}

// 在回调中获取绝对位置
.onDidScroll(() => {
  const offset = this.scroller.currentOffset();
  this.scrollOffset = offset.yOffset;
})

两种方式对比:

方式 优点 缺点
+= 累加增量 实时性好,不依赖 Scroller 需要注意边界重置
Scroller.currentOffset() 直接获取绝对值,无需累加 需要 Scroller 实例,实时性稍差

5.2 负号的意义:滚动方向与位移方向

在代码中我们使用了负号:

.translate({ x: 0, y: -this.scrollOffset * 0.3 })

为什么需要负号?

当用户向下滑动时:

  1. Scroll 内容向上移动(视觉上)
  2. scrollOffset 持续增加(正值)
  3. 为了让背景元素也向上移动,需要给 .translate() 传入负的 y 值
  4. 所以最终是 -this.scrollOffset * 速度系数

方向对照表:

用户手势 scrollOffset 变化 .translate() 需要的值
向上滑(内容向下) 正值增加 负 y 值(向上移动)
向下滑(内容向上) 负值减少 正 y 值(向下移动)
弹回顶部 趋近于 0 趋近于 0

5.3 hitTestBehavior 的穿透机制

当我们使用 Stack 叠加多层时,默认行为是上层组件会拦截所有触摸事件。

没有穿透时的触摸路径:

用户触摸 → 前景层(拦截)→ 事件结束,Scroll 收不到事件

添加穿透后的触摸路径:

用户触摸 → 前景层(Transparent,穿透)→ 中景层(Transparent,穿透)→ 内容层(Scroll,接收事件)

四种模式详细对比:

模式 响应触摸 穿透到下层 典型应用场景
Default 需要响应用户交互的按钮、文本等
Block 弹窗遮罩层,阻止触摸穿透
Transparent 视差背景层、装饰性元素
None 是(完全不参与) 纯视觉效果,不需要任何触摸

5.4 zIndex 与渲染顺序

Stack 中的渲染顺序由 zIndex 决定:

Stack() {
  Layer1().zIndex(1)  // 最先渲染(最底层)
  Layer2().zIndex(3)  // 第三个渲染
  Layer3().zIndex(2)  // 第二个渲染
  Layer4().zIndex(4)  // 最后渲染(最顶层)
}

在视差场景中的 zIndex 分配原则:

层级 zIndex 推荐值 说明
背景渐变 0 或无 最底层的底色
远景层 1 需要被中景遮挡
中景层 2 需要被内容遮挡
内容层 3 核心交互层
前景层 4 不遮挡内容但显示在最前

层级越高,zIndex 值越大,显示越靠前。

5.5 速度系数的选择策略

速度系数直接影响视差效果的自然度:

推荐配置方案 A(经典电影效果):

远景层: 0.15  // 几乎不动
中景层: 0.4   // 缓慢移动
内容层: 1.0   // 正常速度
前景层: 1.8   // 快速飞过

推荐配置方案 B(柔和舒缓):

远景层: 0.25
中景层: 0.55
内容层: 1.0
前景层: 1.3

推荐配置方案 C(动感强烈):

远景层: 0.1
中景层: 0.35
内容层: 1.0
前景层: 2.5

速度系数设计原则:

  1. 相邻层之间的系数差建议在 0.2~0.5 之间
  2. 前景层与远景层的系数差越大,视差效果越明显
  3. 避免系数差过小(< 0.1),否则效果不明显
  4. 避免系数差过大(> 1.0),否则会产生眩晕感

5.6 状态更新与 UI 渲染

onScroll 在滚动过程中每帧都会触发(通常 60 次/秒),这意味着 @State 变量也会被高频更新。

性能优化建议:

// ✅ 好的写法
.onScroll((x, y) => {
  this.scrollOffset += y;  // 简单累加
})

// ❌ 不好的写法
.onScroll((x, y) => {
  this.scrollOffset += y;
  this.updateSomeState();      // 额外的状态更新
  console.log(this.scrollOffset);  // 日志也有开销
  this.doHeavyCalculation();   // 耗时计算
})

性能优化清单:

  • onScroll 回调只做必要的计算
  • 避免在回调中创建新对象或数组
  • 避免在回调中触发其他状态更新
  • 使用 renderGroup 为复杂子树启用 GPU 合成
  • 将非实时性操作延迟到 onDidScroll 执行

第六章:常见问题与解决方案

6.1 问题一:背景层不随滚动移动

现象描述: 远景层和中景层完全静止,只有内容在滚动。

可能原因:

  1. @State 变量没有正确绑定
  2. .translate() 没有使用状态变量
  3. onScroll 没有正确累加
  4. 变量名拼写错误

解决步骤:

// 检查清单
// 1. @State 声明是否正确
@State scrollOffset: number = 0;

// 2. onScroll 是否正确累加
.onScroll((x, y) => {
  this.scrollOffset += y;  // 必须用 += 累加
})

// 3. .translate() 是否使用了状态变量
.translate({ x: 0, y: -this.scrollOffset * 0.3 })
//                                        ^^^^^^^^^^ 必须引用 this.scrollOffset

6.2 问题二:背景层移动方向错误

现象描述: 背景元素向错误方向移动,或者移动速度不对。

可能原因:

  1. 忘记加负号
  2. 速度系数配置错误

解决方案:

// 错误:没有负号
.translate({ x: 0, y: this.scrollOffset * 0.3 })

// 正确:加上负号
.translate({ x: 0, y: -this.scrollOffset * 0.3 })

6.3 问题三:无法触摸滚动内容

现象描述: 手指触摸屏幕时,Scroll 不响应滚动。

可能原因:

  1. 前景层或中景层没有设置 .hitTestBehavior(HitTestMode.Transparent)
  2. 上层组件拦截了触摸事件

解决方案:

// 所有非 Scroll 层都需要设置透明触摸
ForegroundLayer()
  .hitTestBehavior(HitTestMode.Transparent)  // 加上这行!

MidgroundLayer()
  .hitTestBehavior(HitTestMode.Transparent)  // 加上这行!

// 只有 Scroll 层保留默认行为
Scroll() { ... }
// 不需要设置 hitTestBehavior,默认即可响应触摸

6.4 问题四:滚动不流畅、卡顿

现象描述: 滚动过程中有明显的掉帧和卡顿。

可能原因:

  1. onScroll 回调中有耗时操作
  2. 层级太多,每帧重绘压力大
  3. 没有启用 GPU 合成
  4. 子组件过于复杂

解决方案:

// 1. 简化 onScroll 回调
.onScroll((x, y) => {
  this.scrollOffset += y;  // 只做这一件事
})

// 2. 使用 renderGroup 启用 GPU 合成
ForegroundLayer()
  .renderGroup(true)

// 3. 控制层级数量(建议不超过 5 层)

// 4. 简化子组件,避免过于复杂的布局

6.5 问题五:滚到顶部时位置错乱

现象描述: 弹回顶部时,背景层位置不正确。

可能原因: 滚动过边界时 yOffset 会产生异常值。

解决方案:

// 限制 scrollOffset 的范围
.onScroll((x, y) => {
  let newOffset = this.scrollOffset + y;
  newOffset = Math.max(0, Math.min(newOffset, this.maxScrollRange));
  this.scrollOffset = newOffset;
})

6.6 问题六:模拟器与真机表现不同

现象描述: 在模拟器上正常,真机上有问题。

可能原因:

  1. 模拟器使用软件渲染,真机使用硬件加速
  2. 不同设备的性能差异
  3. Emoji 在不同设备上的渲染差异

解决方案:

  • 尽量使用图片资源代替 Emoji
  • 在多个真机上测试
  • 适当降低速度系数,让动画更柔和
  • 使用 .renderGroup(true) 增强渲染性能

6.7 问题七:如何实现横向视差

解决方案:

// 1. 设置 Scroll 为水平滚动
Scroll() {
  // ...
}
.scrollable(ScrollDirection.Horizontal)

// 2. 使用 xOffset 而不是 yOffset
.onScroll((xOffset, yOffset) => {
  this.scrollXOffset += xOffset;
})

// 3. 在 .translate() 中使用 x 属性
.translate({ x: -this.scrollXOffset * 0.3, y: 0 })

6.8 问题八:如何实现循环视差背景

解决方案:

@State scrollOffset: number = 0;
private readonly BG_WIDTH: number = 375;  // 单张背景宽度
private readonly LOOP_COUNT: number = 3;   // 循环次数

build() {
  Stack() {
    ForEach([0, 1, 2], (index: number) => {
      Column() {
        // 背景内容
      }
      .translate({ 
        x: (index * BG_WIDTH) - (this.scrollOffset * 0.3 % (BG_WIDTH * LOOP_COUNT)),
        y: 0 
      })
    }, (index: number) => `${index}`)
  }
  .width('100%')
  .height('100%')
}

第七章:性能优化与最佳实践

7.1 性能分析工具

HarmonyOS NEXT 提供了强大的性能分析工具:

工具 用途 获取方式
DevEco Profiler CPU、内存、帧率分析 DevEco Studio → View → Tool Windows → Profiler
帧率监视器 实时查看帧率 设置 → 开发者选项 → 性能监控 → 帧刷新率
HiLog 日志输出 代码中使用 hilog 模块
SmartPerf 功耗分析 DevEco Studio → Tools → SmartPerf

7.2 视差效果性能优化清单

7.2.1 减少层级数量
层级数 渲染压力 推荐场景
2-3 层 简单页面、低端设备
4-5 层 大多数应用(推荐)
6+ 层 复杂特效、游戏场景
7.2.2 合理使用 renderGroup

.renderGroup(true) 可以将一组子组件作为整体提交给 GPU 渲染:

Column() {
  // 多个子组件...
}
.renderGroup(true)  // 启用 GPU 合成

何时使用 renderGroup:

  • 子树包含多个需要一起移动的组件
  • 子树有明显的边界
  • 频繁重绘导致卡顿
7.2.3 优化 onScroll 回调

黄金法则:onScroll 回调越短越好

// ❌ 性能差:回调中做太多事
.onScroll((x, y) => {
  this.scrollOffset += y;
  this.updateBackgroundOpacity();
  this.checkIfShouldLoadMore();
  console.log('scroll:', this.scrollOffset);
  this.triggerSomeAnimation();
})

// ✅ 性能好:只做必要的事
.onScroll((x, y) => {
  this.scrollOffset += y;
})

// 将其他逻辑延迟处理
.onDidScroll(() => {
  this.checkIfShouldLoadMore();
})
7.2.4 图片资源优化

如果使用图片代替 Emoji:

Image($r('app.media.parallax_bg'))
  .width('100%')
  .height('100%')
  // 优化解码尺寸
  .decodeWidth(1024)
  .decodeHeight(1024)
7.2.5 使用合适的图片格式
格式 适用场景 压缩率 透明支持
WebP 通用场景
PNG 需要透明
JPEG 照片

7.3 内存管理注意事项

7.3.1 避免内存泄漏
  • 在组件销毁时清理定时器和监听器
  • 避免循环引用
  • 使用 WeakRef 存储外部引用
7.3.2 大图处理
// 不要加载过大的图片
Image($r('app.media.huge_image'))
  .decodeWidth(screenWidth)   // 只解码屏幕大小
  .decodeHeight(screenHeight)

7.4 无障碍与暗色模式

7.4.1 无障碍支持
Text('内容描述')
  .accessibilityText('这是可滚动的内容卡片')
7.4.2 暗色模式适配
// 使用 ResourceColor 支持主题切换
.backgroundColor($r('app.color.parallax_bg'))

7.5 多设备适配

7.5.1 响应式布局
.width('100%')        // 宽度自适应
.height('auto')       // 高度自适应
.padding({ left: 20 })  // 使用 vp 单位
7.5.2 不同屏幕尺寸
@media (min-width: 600vp) {
  // 平板布局
}

@media (max-width: 600vp) {
  // 手机布局
}

第八章:实战拓展案例

8.1 案例一:图片视差滚动

将 Emoji 替换为真实图片,打造精致的相册视差效果:

@Entry
@Component
struct ImageParallax {
  @State scrollOffset: number = 0;
  private scroller: Scroller = new Scroller();

  build() {
    Stack() {
      // 远景层:模糊背景图
      Image($r('app.media.mountain_bg'))
        .width('100%')
        .height('150%')
        .translate({ x: 0, y: -this.scrollOffset * 0.2 })
        .blur(20)
        .opacity(0.7)
        .zIndex(1)
        .hitTestBehavior(HitTestMode.Transparent)

      // 中景层:森林图片
      Image($r('app.media.forest_mid'))
        .width('120%')
        .height('120%')
        .translate({ x: 0, y: -this.scrollOffset * 0.5 })
        .zIndex(2)
        .hitTestBehavior(HitTestMode.Transparent)

      // 内容层
      Scroll(this.scroller) {
        Column() {
          Image($r('app.media.main_content'))
            .width('100%')
            .height(400)
            .objectFit(ImageFit.Cover)
          
          Column() {
            Text('标题')
              .fontSize(32)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 16 })
            
            Text('详细内容...')
              .fontSize(18)
              .lineHeight(28)
          }
          .padding(20)
        }
      }
      .onScroll((x, y) => {
        this.scrollOffset += y;
      })
      .zIndex(3)

      // 前景层:飞鸟剪影
      Image($r('app.media.birds_foreground'))
        .width(150)
        .height(80)
        .translate({ x: 50, y: 100 - this.scrollOffset * 1.5 })
        .zIndex(4)
        .hitTestBehavior(HitTestMode.Transparent)
    }
    .width('100%')
    .height('100%')
  }
}

8.2 案例二:卡片视差堆叠效果

实现类似 iOS Card Stack 的视差效果:

@Entry
@Component
struct CardStackParallax {
  @State scrollOffset: number = 0;
  private readonly CARD_HEIGHT: number = 200;

  build() {
    Stack() {
      ForEach([0, 1, 2, 3, 4], (index: number) => {
        this.CardItem(index)
      }, (index: number) => `${index}`)
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  CardItem(index: number) {
    Column() {
      Text(`卡片 ${index + 1}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
    }
    .width('90%')
    .height(this.CARD_HEIGHT)
    .justifyContent(FlexAlign.Center)
    .backgroundColor(this.getCardColor(index))
    .borderRadius(16)
    .shadow({ radius: 10, offsetY: 5 })
    .margin({ left: '5%', top: 20 })
    .translate({ 
      x: 0, 
      y: -this.scrollOffset * (0.3 + index * 0.1) 
    })
    .zIndex(10 - index)
  }

  private getCardColor(index: number): ResourceColor {
    const colors: ResourceColor[] = [
      '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'
    ];
    return colors[index % colors.length];
  }
}

8.3 案例三:3D 翻转视差

结合 3D 变换实现更丰富的效果:

@Entry
@Component
struct Parallax3D {
  @State scrollOffset: number = 0;

  build() {
    Stack() {
      // 背景层
      Column() {
        Text('🗻').fontSize(200)
      }
      .width('100%')
      .height('100%')
      .translate({ x: 0, y: -this.scrollOffset * 0.3 })
      .zIndex(1)
      .hitTestBehavior(HitTestMode.Transparent)

      // 3D 旋转的内容层
      Scroll() {
        Column() {
          ForEach([0, 1, 2, 3], (index: number) => {
            this.RotatingCard(index)
          }, (index: number) => `${index}`)
        }
      }
      .onScroll((x, y) => {
        this.scrollOffset += y;
      })
      .zIndex(2)
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  RotatingCard(index: number) {
    Column() {
      Text(`3D 卡片 ${index + 1}`)
        .fontSize(28)
        .fontColor(Color.White)
    }
    .width('80%')
    .height(180)
    .margin({ left: '10%', top: 30 })
    .justifyContent(FlexAlign.Center)
    .backgroundColor(this.getCardColor(index))
    .borderRadius(20)
    .rotate({
      z: (this.scrollOffset % 360) * 0.5,
      centerX: '50%',
      centerY: '50%'
    })
    .scale({
      x: 0.9 + (this.scrollOffset % 100) / 1000,
      y: 0.9 + (this.scrollOffset % 100) / 1000
    })
  }

  private getCardColor(index: number): string {
    const colors: string[] = ['#667eea', '#764ba2', '#f093fb', '#f5576c'];
    return colors[index % colors.length];
  }
}

8.4 案例四:视差与动画结合

结合 Spring 动画和视差效果:

@Entry
@Component
struct AnimatedParallax {
  @State scrollOffset: number = 0;
  @State isExpanded: boolean = false;

  build() {
    Stack() {
      // 动态背景层
      Column() {
        ForEach([0, 1, 2, 3], (i: number) => {
          Text(this.getEmoji(i))
            .fontSize(60 + i * 20)
            .margin({ top: 50 + i * 80, left: 50 + i * 100 })
        }, (i: number) => `${i}`)
      }
      .width('100%')
      .height('100%')
      .translate({ 
        x: this.isExpanded ? 50 : 0,
        y: -this.scrollOffset * 0.3 
      })
      .animation({
        duration: 500,
        curve: Curve.Spring,
        delay: 100
      })
      .zIndex(1)
      .hitTestBehavior(HitTestMode.Transparent)

      // 交互层
      Column() {
        Button(this.isExpanded ? '收起' : '展开')
          .onClick(() => {
            animateTo({
              duration: 300,
              curve: Curve.EaseInOut
            }, () => {
              this.isExpanded = !this.isExpanded;
            })
          })
          .margin({ top: 100 })

        Scroll() {
          Column() {
            ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
              Column() {
                Text(`${i + 1} 项内容`)
                  .fontSize(20)
              }
              .width('90%')
              .height(100)
              .backgroundColor('#F0F0F0')
              .margin({ top: 20 })
              .borderRadius(10)
            }, (i: number) => `${i}`)
          }
          .width('100%')
          .padding({ top: 50 })
        }
        .width('100%')
        .layoutWeight(1)
        .onScroll((x, y) => {
          this.scrollOffset += y;
        })
      }
      .width('100%')
      .height('100%')
      .zIndex(2)
    }
    .width('100%')
    .height('100%')
  }

  private getEmoji(index: number): string {
    const emojis: string[] = ['🌈', '⭐', '🌙', '☀️'];
    return emojis[index % emojis.length];
  }
}

第九章:总结与展望

9.1 技术回顾

本文详细介绍了如何在 HarmonyOS NEXT 中使用 Scroll 组件配合 .translate() 属性实现多层视差效果。让我们回顾一下核心要点:

实现视差效果的三大要素:

  1. Scroll 组件:负责捕获用户的滚动手势,通过 onScroll 回调输出滚动增量

  2. @State 状态变量:存储滚动偏移量,并在变化时触发 UI 重新渲染

  3. .translate() 属性:将滚动偏移量乘以不同的速度系数,应用到各层组件上

关键技术细节总结:

技术点 说明 常见错误
增量 vs 绝对值 onScroll 输出增量,需要累加 忘记累加,直接赋值
负号的作用 滚动方向与位移方向相反 忘记加负号
hitTestBehavior 背景层设为 Transparent 忘记设置导致无法滚动
zIndex 分配 底层小、顶层大 层级混乱导致遮挡问题
速度系数 前景 > 1.0x,远景 < 1.0x 系数配置导致效果不自然

9.2 视差效果在实际项目中的价值

提升用户体验:

  • 增强页面的视觉吸引力
  • 让内容浏览更有趣味性
  • 帮助用户建立空间感知

打造品牌特色:

  • 让应用看起来更精致、更专业
  • 建立独特的视觉风格
  • 提升品牌辨识度

适用场景总结:

视差效果特别适合以下场景:
├── 启动引导页(Onboarding)
├── 故事/小说阅读界面
├── 产品展示页面
├── 相册/画廊应用
├── 游戏菜单界面
├── 营销活动页面
└── 个人主页背景

9.3 进阶学习路线

继续深入学习的方向:

方向 相关技术 难度等级
复杂动画 animateTo, keyframeAnimation ⭐⭐
3D 变换 .rotate3D, .perspective ⭐⭐⭐
GPU 渲染 renderGroup, OffscreenCanvas ⭐⭐⭐
手势交互 Gesture, PanGesture ⭐⭐
性能优化 Profiler, ArkTools ⭐⭐⭐

推荐学习资源:

  • HarmonyOS NEXT 官方文档:developer.huawei.com
  • ArkUI 组件示例:DevEco Studio → New Project → Sample
  • 华为开发者论坛:forums.developer.huawei.com
  • GitHub 上的 HarmonyOS 开源项目

9.4 未来展望

HarmonyOS NEXT 的 ArkUI 框架正在快速发展,未来视差效果可能的演进方向:

  1. 声明式动画系统增强:更流畅的 GPU 加速动画,更好的开发体验
  2. 3D 视差原生支持:框架层面的 3D 空间布局,降低开发者难度
  3. 物理引擎集成:真实的物理碰撞和运动模拟,更自然的交互
  4. AI 辅助设计:自动生成视差效果的智能工具,降低设计门槛
  5. 分布式视差:跨设备的协同视差效果,多屏联动

9.5 写在最后

视差效果虽然原理简单,但要实现自然流畅、性能优良的效果,需要开发者对 ArkUI 的渲染机制、状态管理和事件系统有深入的理解。希望本文能够帮助你掌握这项技术,并在实际项目中灵活运用。

记住:好的视差效果应该让用户"感觉不到"它的存在——它只是让内容浏览变得更有深度和乐趣。

感谢阅读,祝开发愉快!🚀


附录

附录 A:完整的 API 参考

Scroll 组件 API
API 说明 参数
Scroll(scroller?: Scroller) 创建 Scroll 实例 可选的 Scroller 控制器
.scrollable(direction) 设置滚动方向 ScrollDirection.Vertical/Horizontal
.scrollBar(state) 设置滚动条状态 BarState.Auto/On/Off/Idle
.edgeEffect(effect) 设置边缘效果 EdgeEffect.Spring/Fade/None
.onScroll(callback) 滚动事件回调 (xOffset, yOffset) => void
.onWillScroll(callback) 滚动前回调 (xOffset, yOffset) => void
.onDidScroll(callback) 滚动完成回调 () => void
.onScrollFrameBegin(callback) 帧开始回调 (xOffset, yOffset) => {x, y}
.translate() API
参数 类型 默认值 说明
x number 0 水平偏移量(vp),正数向右
y number 0 垂直偏移量(vp),正数向下
z number 0 Z 轴偏移量(vp),用于 3D
.hitTestBehavior() API
响应触摸 穿透 说明
Default 默认行为
Block 阻止传递
Transparent 透明模式
None 完全不参与
.zIndex() API
参数 类型 说明
value number 层级值,越大越靠前

附录 B:速度系数速查表

层级类型 推荐速度系数 与内容层的系数差 视觉效果描述
前景层(快速) 1.5 ~ 2.5 +0.5 ~ +1.5 快速飞过,最具动感
内容层(基准) 1.0 0 用户正常阅读的内容
中后景层 0.4 ~ 0.8 -0.2 ~ -0.6 缓慢跟随,不抢注意力
远景层 0.1 ~ 0.3 -0.7 ~ -0.9 几乎不动,营造深度
固定背景 0 -1.0 完全静止的底色

附录 C:调试技巧

使用 HiLog 输出调试信息
import hilog from '@ohos.hilog';

// 在代码中输出日志
hilog.info(0x0001, 'ParallaxDemo', 'scrollOffset: %{public}d', this.scrollOffset);
使用布局检查器
  1. 运行应用
  2. 在 DevEco Studio 中打开 Tools → Layout Inspector
  3. 实时查看组件层级和属性
使用性能分析器
  1. 打开 View → Tool Windows → Profiler
  2. 选择目标设备和应用
  3. 配置性能分析任务
  4. 开始分析并查看帧率、CPU、内存等数据

版权声明: 本文为 HarmonyOS NEXT 技术学习资料,仅用于学习和参考。

技术支持: 如有问题,请访问华为开发者论坛或提交 Issue。

Logo

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

更多推荐