接入与生命周期
本页解决“组件放进页面后如何长期稳定工作”的问题。核心只有一句话:一个可见的 Viewer 绑定一个稳定 Controller;导入串行进行;页面永久离开才 dispose。 如果只是查字段,请跳到 VoxelViewer API 参考。
Controller 生命周期
一个 VoxelViewer 对应一个稳定的 VoxelViewerController:
创建 Options → 创建 Controller → 配置资源/渲染参数 → 挂载 Viewer → 串行 load* → dispose
| 阶段 | 必须做 | 不要做 |
|---|---|---|
| 页面初始化 | 创建 Options 与 Controller。 | 在 build() 创建。 |
| 首次加载前 | 设置 unitGridTexture、背景、Overlay 策略和 Tuning。 | 先加载再补纹理,导致回退描边。 |
| 导入期间 | 等待当前 Promise 结束。 | 并发调用两个 load*()。 |
| 页面可见 | 把同一 Controller 传给同一 Viewer。 | 外部调用 bindStateListener()。 |
| 永久退出 | 调用 dispose()。 | 释放后继续复用 Controller。 |
dispose() 会取消仍未完成的边线任务、立即收束载入动画并清除当前 Scene 引用。再次展示预览必须新建 Controller 和 Viewer。
常见生命周期写法
@Entry
@Component
struct AssetDetailPage {
private readonly previewOptions = new VoxelViewerOptions();
private readonly previewController = new VoxelViewerController(this.previewOptions);
aboutToAppear(): void {
this.previewOptions.unitGridTexture = $rawfile('voxel/voxel_grid.png');
this.previewController.loadUri(this.assetUri, this.assetName)
.catch((error: Error) => console.error(error.message));
}
build() {
VoxelViewer({ controller: this.previewController })
.width('100%')
.height(520)
}
aboutToDisappear(): void {
this.previewController.dispose();
}
}
如果页面只是被覆盖又会返回,不要仅为短暂不可见而销毁/重建 Controller;是否在离开时释放由页面自身的生存期决定。永久离开或确定不再复用时才调用 dispose()。调用后重新 load*() 不属于支持用法。
内置手势
| 手势 | 行为 |
|---|---|
| 单指拖拽 | 以模型 Pivot 为中心的球面旋转;横向环绕,纵向俯仰。 |
| 双指平移 | 相机目标点平移。 |
| 双指捏合 | 缩放。 |
| 载入动画期间 | 手势暂时忽略,避免临时动画 Geometry 与相机状态错位。 |
双指平移和捏合会在同一事件轮次合并相机更新。宿主不应额外给 Viewer 套一层会修改相机或争夺同类手势的组件。
手势范围与产品提示
- 用户不能通过公开 API 设置相机初始坐标、固定某个旋转角,或脚本化推进相机。
- Viewer 不是地图控件:两指是平移与缩放,而不是第二套旋转手势。
- 载入动画时有意锁定输入,防止临时动画几何与相机状态错位;
Promise成功不等于动画结束,但 UI 会在动画结束时自动恢复触控。 - 希望用户快速理解操作时,在 Viewer 外部放一条“单指旋转,双指平移/缩放”的辅助文案即可,不要覆盖一个透明的手势层。
Options 与动态更新
const options = this.controller.getOptions();
options.darkBackground = true;
options.orbitSensitivity = 1.2;
options.panSensitivity = 0.9;
this.controller.setOptions(options);
setOptions() 立即更新背景、当前地面网格和当前场景动画时长;动画模式和是否播放动画由下一次加载读取。autoRotate 是保留字段,当前版本无效果。
为避免配置失效,修改同一对象后仍需提交:
const options = this.controller.getOptions();
options.darkBackground = true;
options.showGroundGrid = false;
this.controller.setOptions(options);
如果只改网格,使用 setGroundGridVisible(false) 更直接;它会同步写入 Options 并立即操作当前场景。
背景四选一
| 模式 | 何时使用 | 宿主代码 |
|---|---|---|
BUILTIN | 要默认工作室视觉。 | 仅切换 darkBackground。 |
BUILTIN_CUSTOM | 要品牌渐变或一张图片。 | 设置六段颜色,或 backgroundImage。 |
BUILDER | 要在组件内部完全替换背景。 | 传 backgroundBuilder。 |
EXTERNAL | 页面已有全屏背景。 | 在父 Stack 绘制背景。 |
图片背景必须由宿主创建 ArkUI Resource,例如 $rawfile('images/background.jpg');HAR 不接收路径或 URI,也不管理图片权限/缓存。
背景 Builder 示例
@Builder
function BrandBackground(state: VoxelBackgroundState) {
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: state.darkBackground
? [['#10243A', 0], ['#05070C', 1]]
: [['#FBFCFF', 0], ['#DCEBFF', 1]]
})
}
// 配置:options.backgroundMode = VoxelBackgroundMode.BUILDER;
VoxelViewer({ controller: this.controller, backgroundBuilder: BrandBackground })
BUILDER 是组件内部背景;EXTERNAL 是页面外部背景。前者随 Viewer 尺寸裁切,后者可与标题、按钮、说明共享同一张整页背景。
Loading UI 四选一
| 模式 | HAR 渲染 | 宿主做什么 |
|---|---|---|
BUILTIN | 遮罩、状态卡片、错误提示全部内置。 | 无。 |
BUILTIN_CUSTOM | 保留内置结构。 | 改 VoxelLoadOverlayStyle 文案/颜色/圆角。 |
BUILDER | HAR 管理遮罩可见性。 | 用 loadOverlayBuilder 替换提示卡片。 |
EXTERNAL | 不画任何遮罩或错误。 | 监听 onLoadOverlayStateChanged。 |
EXTERNAL 示例:
options.loadOverlayMode = VoxelLoadOverlayMode.EXTERNAL;
options.onLoadOverlayStateChanged = (state: VoxelLoadOverlayState): void => {
// 将 state.phase / state.visible / state.message 写入宿主 @State。
};
controller.setOptions(options);
阶段枚举为 IDLE、IMPORTING、PREPARING、ANIMATING、ERROR、HIDDEN。回调仅表达 UI 生命周期,不表示读取字节百分比或 TaskPool 实际进度。
外置 Loading 的推荐模式
@State private overlayVisible: boolean = false;
@State private overlayMessage: string = '';
private configureOverlay(): void {
this.options.loadOverlayMode = VoxelLoadOverlayMode.EXTERNAL;
this.options.onLoadOverlayStateChanged = (state: VoxelLoadOverlayState): void => {
this.overlayVisible = state.visible;
this.overlayMessage = state.errorMessage.length > 0 ? state.errorMessage : state.message;
};
this.controller.setOptions(this.options);
}
宿主使用 visible 控制自己的遮罩,使用 phase 决定是显示进度样式、动画样式还是错误样式。不要根据 message 文本做条件判断;文案是面向用户的内容,可能随版本优化。
切换模型时的规则
- 等待当前
load*()Promise 完成(无论成功或失败)。 - 使用同一个 Controller 发起下一次
load*()。 - 组件会取消前一场景尚未完成的延迟描边,替换为新场景。
- 不要在切换时先
dispose()再继续load*();这会把生命周期变成未定义状态。
在支持“重新导入”按钮的产品中,按钮应在导入期间禁用,或让业务层维护一个串行队列;不要让用户的连续点击并发进入两个导入调用。