HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与Body AR的“筑境空间“——PC端沉浸式建筑漫游与设计评审平台
文章目录

每日一句正能量
别因几粒尘埃迷了眼,就错过整个天空的晴朗。
我们要有宏大的视野和格局,不为琐事困扰。
做不了决定的时候,让时间帮你决定。如果还是无法决定,做了再说。宁愿犯错,不留遗憾。早安!
前言
摘要:HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Body AR特性,为建筑设计与空间展示领域提供了全新的交互范式。本文将实战开发一款面向HarmonyOS PC的"筑境空间"应用,展示如何利用
systemMaterialEffect打造沉浸式建筑浏览环境,通过悬浮导航实现多项目快速切换,基于Body AR实现手势操控3D建筑模型漫游,以及基于多窗口架构构建浮动户型详情、材质库和日照分析窗口的协作设计体验。
一、前言:建筑设计的交互革新需求
传统的建筑设计软件往往采用复杂的工具栏和固定的面板布局,在HarmonyOS PC的大屏环境下显得臃肿且缺乏空间感。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation)、沉浸光感(Immersive Light Effects)与Body AR特性,为建筑可视化带来了"自然、沉浸、直观"的交互可能 。
本文核心亮点:
- 场景感知光效:根据当前建筑场景(日间/夜间/四季)动态切换环境光色与氛围
- 悬浮项目导航:底部悬浮页签替代传统项目栏,支持拖拽排序与透明度调节
- Body AR手势操控:通过人体运动捕捉实现手势缩放、旋转、平移3D建筑模型
- 多窗口设计协作:主漫游窗口 + 浮动户型详情 + 材质库 + 日照分析窗口的光效联动
二、核心特性解析与技术选型
2.1 沉浸光感在建筑可视化中的价值
HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为标题栏和导航组件带来细腻的光晕与反射效果 。在建筑漫游场景中,这种材质效果能够:
- 增强空间氛围:玻璃拟态的半透明层让背景光效柔和过渡,模拟真实建筑的光影变化
- 提升空间感知:动态环境光随场景时间变化(清晨暖黄、正午亮白、黄昏橙红、夜晚深蓝),增强空间沉浸感
- 增强操作反馈:通过光效强弱区分窗口焦点状态,多窗口协作时视觉层级更清晰
2.2 Body AR在建筑漫游中的创新应用
HarmonyOS 6的Body AR能力支持实时精确捕捉人体运动信息 ,在建筑漫游中可以:
- 自然手势操控:手掌张开放大模型、握拳抓取旋转、双手开合缩放
- 体感漫游:身体前倾前进、后仰后退、左右侧身平移视角
- 虚拟导览:通过手势触发热点标注、切换楼层、打开门窗动画
三、项目实战:"筑境空间"架构设计
3.1 应用场景与功能规划
面向HarmonyOS PC的建筑设计场景,核心功能包括:
| 功能模块 | 技术实现 | 沉浸光感/Body AR应用 |
|---|---|---|
| 主漫游窗口 | XComponent + 3D引擎渲染 |
背景光效随场景时间变化 |
| 悬浮项目导航 | HdsTabs + systemMaterialEffect |
玻璃拟态页签,选中光晕反馈 |
| Body AR手势操控 | AR Engine + 骨骼跟踪 | 手势操控3D模型 |
| 浮动户型详情窗口 | 子窗口 + HdsNavigation |
户型主题色光效同步 |
| 材质库窗口 | 子窗口 + Grid |
材质预览光效 |
| 日照分析窗口 | 子窗口 + Canvas |
日照角度光效模拟 |
3.2 技术架构图
┌─────────────────────────────────────────────────────────────┐
│ UI Layer (ArkUI) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ImmersiveTitle│ │ BuildingView │ │ FloatTabBar │ │
│ │ Bar (HDS) │ │ (3D+AR) │ │ (HdsTabs) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ AR Engine Layer (Body AR) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Skeleton │ │ Gesture │ │ Pose Control │ │
│ │ Tracking │ │ Recognition │ │ (3D Model) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Window Manager (PC Multi-Window) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Main Window │ │ Layout Win │ │ Material Win │ │
│ │ (FullScreen) │ │ (Floating) │ │ (Floating) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ │
│ │ Sunlight Win │ │
│ │ (Floating) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
四、环境配置与模块依赖
4.1 模块依赖配置
{
"name": "zhujing-space",
"version": "1.0.0",
"description": "Immersive Architecture Walkthrough Platform for HarmonyOS PC",
"dependencies": {
"@kit.AbilityKit": "^6.1.0",
"@kit.ArkUI": "^6.1.0",
"@kit.UIDesignKit": "^6.1.0",
"@kit.BasicServicesKit": "^6.1.0",
"@kit.AREngineKit": "^6.1.0",
"@kit.GraphicsKit": "^6.1.0"
}
}
4.2 权限声明(module.json5)
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "ArchitectureAbility",
"deviceTypes": [
"2in1",
"tablet",
"default"
],
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:permission_camera_reason"
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:permission_internet_reason"
}
]
}
}
五、核心组件实战
5.1 窗口沉浸配置(ArchitectureAbility.ets)
// entry/src/main/ets/ability/ArchitectureAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class ArchitectureAbility extends UIAbility {
private mainWindow: window.Window | null = null;
onWindowStageCreate(windowStage: window.WindowStage): void {
this.initializeArchitectureWindow(windowStage);
}
private async initializeArchitectureWindow(windowStage: window.WindowStage): Promise<void> {
try {
this.mainWindow = windowStage.getMainWindowSync();
await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
await this.mainWindow.setWindowTitleBarEnable(false);
await this.mainWindow.setWindowLayoutFullScreen(true);
await this.mainWindow.setWindowShadowEnabled(true);
await this.mainWindow.setWindowCornerRadius(12);
await this.mainWindow.setWindowBackgroundColor('#00000000');
AppStorage.setOrCreate('main_window', this.mainWindow);
windowStage.loadContent('pages/ArchitecturePage', (err) => {
if (err.code) {
console.error('Failed to load architecture content:', JSON.stringify(err));
return;
}
console.info('Zhujing Space main window initialized');
});
} catch (error) {
console.error('Window initialization failed:', (error as BusinessError).message);
}
}
onWindowStageDestroy(): void {
this.mainWindow = null;
}
}
5.2 沉浸光感标题栏(ImmersiveTitleBar.ets)
// entry/src/main/ets/components/ImmersiveTitleBar.ets
import { HdsNavigation, SystemMaterialEffect } from '@kit.UIDesignKit';
export enum SceneTime {
DAWN = 'dawn', // 清晨
NOON = 'noon', // 正午
DUSK = 'dusk', // 黄昏
NIGHT = 'night' // 夜晚
}
@Component
export struct ImmersiveTitleBar {
@Prop currentProject: string = '云栖别墅';
@Prop currentFloor: string = '一层平面';
@Prop sceneTime: SceneTime = SceneTime.NOON;
@State isWindowFocused: boolean = true;
@State titleBarHeight: number = 48;
// 场景时间主题色映射
private sceneColors: Map<SceneTime, string> = new Map([
[SceneTime.DAWN, '#FFD93D'], // 暖黄-清晨
[SceneTime.NOON, '#FFFFFF'], // 亮白-正午
[SceneTime.DUSK, '#FF6B6B'], // 橙红-黄昏
[SceneTime.NIGHT, '#4A69BD'] // 深蓝-夜晚
]);
aboutToAppear(): void {
AppStorage.watch('window_focused', (focused: boolean) => {
this.isWindowFocused = focused;
});
}
private getThemeColor(): string {
return this.sceneColors.get(this.sceneTime) || '#FFFFFF';
}
private getSceneText(): string {
const texts: Map<SceneTime, string> = new Map([
[SceneTime.DAWN, '清晨'],
[SceneTime.NOON, '正午'],
[SceneTime.DUSK, '黄昏'],
[SceneTime.NIGHT, '夜晚']
]);
return texts.get(this.sceneTime) || '正午';
}
build() {
HdsNavigation({
title: `筑境空间 - ${this.currentProject}`,
subtitle: `${this.currentFloor} · ${this.getSceneText()}`,
systemMaterialEffect: SystemMaterialEffect.IMMERSIVE,
backgroundOpacity: this.isWindowFocused ? 0.85 : 0.55,
height: this.titleBarHeight,
leading: this.buildLeadingActions(),
trailing: this.buildTrailingActions()
})
.width('100%')
.border({
width: { bottom: 1 },
color: this.isWindowFocused
? this.getThemeColor()
: 'rgba(255,255,255,0.1)'
})
.shadow({
radius: this.isWindowFocused ? 15 : 5,
color: this.getThemeColor(),
offsetX: 0,
offsetY: 2
})
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
@Builder
buildLeadingActions(): void {
Row({ space: 12 }) {
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_3d_view'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('view_action', 'toggle_3d');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_floor'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('view_action', 'switch_floor');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_sun'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('view_action', 'cycle_time');
})
}
.padding({ left: 16 })
}
@Builder
buildTrailingActions(): void {
Row({ space: 12 }) {
// AR状态指示
Circle()
.width(8)
.height(8)
.fill(AppStorage.get<boolean>('ar_active') ? '#27C93F' : '#95A5A6')
.shadow({
radius: 8,
color: AppStorage.get<boolean>('ar_active') ? '#27C93F' : 'transparent'
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_layout'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('window_action', 'open_layout');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_material'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('window_action', 'open_material');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_sunlight'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('window_action', 'open_sunlight');
})
}
.padding({ right: 16 })
}
}
5.3 Body AR手势操控组件(BodyARController.ets)
核心创新组件,基于AR Engine实现人体骨骼跟踪与手势识别 。
// entry/src/main/ets/components/BodyARController.ets
import { arEngine } from '@kit.AREngineKit';
import { camera } from '@kit.CameraKit';
export enum GestureType {
NONE = 'none',
OPEN_PALM = 'open_palm', // 张开手掌-放大
CLOSED_FIST = 'closed_fist', // 握拳-旋转
PINCH = 'pinch', // 捏合-缩放
POINT = 'point', // 指向-选择
SWIPE_LEFT = 'swipe_left', // 左滑-平移
SWIPE_RIGHT = 'swipe_right' // 右滑-平移
}
interface BodyPose {
gesture: GestureType;
handPosition: { x: number; y: number };
handDistance: number; // 双手距离(用于缩放)
bodyLean: number; // 身体倾斜角度
confidence: number;
}
@Component
export struct BodyARController {
@State isARActive: boolean = false;
@State currentGesture: GestureType = GestureType.NONE;
@State gestureConfidence: number = 0;
@State trackingStatus: string = '未启动';
private arSession: arEngine.ARSession | null = null;
private bodyTracker: arEngine.BodyTracker | null = null;
private gestureCallback: ((gesture: BodyPose) => void) | null = null;
aboutToAppear(): void {
this.initializeAR();
}
aboutToDisappear(): void {
this.releaseAR();
}
setGestureCallback(callback: (gesture: BodyPose) => void): void {
this.gestureCallback = callback;
}
private async initializeAR(): Promise<void> {
try {
this.arSession = arEngine.createARSession({
mode: arEngine.ARMode.BODY,
cameraConfig: {
cameraFacing: camera.CameraFacing.CAMERA_FACING_FRONT
}
});
this.bodyTracker = this.arSession.createBodyTracker({
maxBodyCount: 1,
enableSkeleton: true,
enableGesture: true
});
await this.arSession.start();
this.isARActive = true;
this.trackingStatus = '跟踪中';
this.startBodyTracking();
console.info('Body AR initialized successfully');
} catch (error) {
console.error('Failed to initialize Body AR:', error);
this.trackingStatus = '初始化失败';
}
}
private startBodyTracking(): void {
if (!this.arSession || !this.bodyTracker) return;
this.arSession.on('frame', (frame: arEngine.ARFrame) => {
const bodies = this.bodyTracker?.track(frame);
if (bodies && bodies.length > 0) {
const body = bodies[0];
const pose = this.analyzeGesture(body);
this.currentGesture = pose.gesture;
this.gestureConfidence = pose.confidence;
if (this.gestureCallback) {
this.gestureCallback(pose);
}
AppStorage.setOrCreate('body_gesture', pose);
}
});
}
private analyzeGesture(body: arEngine.Body): BodyPose {
const skeleton = body.getSkeleton();
const leftHand = skeleton.getJoint(arEngine.BodyJointType.LEFT_HAND);
const rightHand = skeleton.getJoint(arEngine.BodyJointType.RIGHT_HAND);
const leftWrist = skeleton.getJoint(arEngine.BodyJointType.LEFT_WRIST);
const rightWrist = skeleton.getJoint(arEngine.BodyJointType.RIGHT_WRIST);
const nose = skeleton.getJoint(arEngine.BodyJointType.NOSE);
const neck = skeleton.getJoint(arEngine.BodyJointType.NECK);
// 计算双手距离(用于缩放判断)
const handDistance = Math.sqrt(
Math.pow(rightHand.x - leftHand.x, 2) +
Math.pow(rightHand.y - leftHand.y, 2)
);
// 计算身体倾斜(用于漫游判断)
const bodyLean = (nose.x - neck.x) / (neck.confidence || 1);
// 手势识别逻辑
let gesture = GestureType.NONE;
let confidence = 0;
// 判断双手张开(放大)
if (leftHand.confidence > 0.7 && rightHand.confidence > 0.7 && handDistance > 0.5) {
gesture = GestureType.OPEN_PALM;
confidence = (leftHand.confidence + rightHand.confidence) / 2;
}
// 判断单手挥舞(左滑/右滑)
else if (leftWrist.confidence > 0.7 && Math.abs(leftWrist.velocityX) > 0.3) {
gesture = leftWrist.velocityX > 0 ? GestureType.SWIPE_RIGHT : GestureType.SWIPE_LEFT;
confidence = leftWrist.confidence;
}
// 判断指向(选择)
else if (rightHand.confidence > 0.7 && rightWrist.confidence > 0.7) {
const handAngle = Math.atan2(rightHand.y - rightWrist.y, rightHand.x - rightWrist.x);
if (Math.abs(handAngle) < Math.PI / 4) {
gesture = GestureType.POINT;
confidence = rightHand.confidence;
}
}
return {
gesture,
handPosition: { x: rightHand.x, y: rightHand.y },
handDistance,
bodyLean,
confidence
};
}
private releaseAR(): void {
if (this.arSession) {
this.arSession.stop();
this.arSession = null;
}
this.bodyTracker = null;
this.isARActive = false;
this.trackingStatus = '已停止';
}
build() {
Stack() {
// AR预览层
Column() {
if (this.isARActive) {
XComponent({
id: 'body_ar_preview',
type: XComponentType.SURFACE,
controller: new XComponentController()
})
.width('100%')
.height('100%')
.opacity(0.25)
}
}
.width('100%')
.height('100%')
// 手势反馈层
Column() {
this.buildGestureFeedback()
}
.width('100%')
.height('100%')
// AR状态指示器
this.buildARStatusIndicator()
}
.width('100%')
.height('100%')
}
@Builder
buildGestureFeedback(): void {
if (this.currentGesture !== GestureType.NONE) {
Column() {
// 手势图标
Image(this.getGestureIcon())
.width(64)
.height(64)
.fillColor('#4ECDC4')
.opacity(this.gestureConfidence)
Text(this.getGestureText())
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.margin({ top: 8 })
// 置信度进度条
Progress({
value: this.gestureConfidence * 100,
total: 100,
type: ProgressType.Linear
})
.width(120)
.height(4)
.color('#4ECDC4')
.margin({ top: 8 })
}
.width(160)
.height(140)
.backgroundColor('rgba(0,0,0,0.6)')
.borderRadius(16)
.position({ x: '50%', y: '20%' })
.anchor('50%')
.backdropFilter($r('sys.blur.10'))
.animation({
duration: 200,
curve: Curve.EaseOut
})
}
}
@Builder
buildARStatusIndicator(): void {
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.isARActive ? '#27C93F' : '#95A5A6')
.shadow({
radius: 6,
color: this.isARActive ? '#27C93F' : 'transparent'
})
Text(`Body AR · ${this.trackingStatus}`)
.fontSize(11)
.fontColor(this.isARActive ? '#27C93F' : '#95A5A6')
}
.width('auto')
.height(28)
.padding({ left: 10, right: 10 })
.backgroundColor('rgba(0,0,0,0.6)')
.borderRadius(14)
.position({ x: 16, y: 16 })
.backdropFilter($r('sys.blur.10'))
}
private getGestureIcon(): Resource {
const icons: Map<GestureType, Resource> = new Map([
[GestureType.OPEN_PALM, $r('app.media.ic_open_palm')],
[GestureType.CLOSED_FIST, $r('app.media.ic_fist')],
[GestureType.PINCH, $r('app.media.ic_pinch')],
[GestureType.POINT, $r('app.media.ic_point')],
[GestureType.SWIPE_LEFT, $r('app.media.ic_swipe_left')],
[GestureType.SWIPE_RIGHT, $r('app.media.ic_swipe_right')]
]);
return icons.get(this.currentGesture) || $r('app.media.ic_hand');
}
private getGestureText(): string {
const texts: Map<GestureType, string> = new Map([
[GestureType.OPEN_PALM, '放大模型'],
[GestureType.CLOSED_FIST, '旋转模型'],
[GestureType.PINCH, '缩放模型'],
[GestureType.POINT, '选择热点'],
[GestureType.SWIPE_LEFT, '向左平移'],
[GestureType.SWIPE_RIGHT, '向右平移']
]);
return texts.get(this.currentGesture) || '未知手势';
}
}
5.4 3D建筑漫游视图(BuildingViewer.ets)
// entry/src/main/ets/components/BuildingViewer.ets
import { BodyARController, GestureType, BodyPose } from './BodyARController';
@Component
export struct BuildingViewer {
@Prop sceneTime: string = 'noon';
@Prop themeColor: string = '#FFFFFF';
@State modelScale: number = 1.0;
@State modelRotationY: number = 0;
@State modelOffsetX: number = 0;
@State modelOffsetY: number = 0;
@State currentFloor: number = 1;
@State showHotspots: boolean = true;
private arController: BodyARController | null = null;
aboutToAppear(): void {
// 监听Body AR手势
AppStorage.watch('body_gesture', (pose: BodyPose) => {
this.handleGesture(pose);
});
}
private handleGesture(pose: BodyPose): void {
switch (pose.gesture) {
case GestureType.OPEN_PALM:
// 双手张开-放大模型
this.modelScale = Math.min(2.0, this.modelScale + 0.05);
break;
case GestureType.PINCH:
// 捏合-缩小模型
this.modelScale = Math.max(0.5, this.modelScale - 0.05);
break;
case GestureType.SWIPE_LEFT:
// 左滑-向左平移
this.modelOffsetX -= 10;
break;
case GestureType.SWIPE_RIGHT:
// 右滑-向右平移
this.modelOffsetX += 10;
break;
case GestureType.CLOSED_FIST:
// 握拳-旋转模型
this.modelRotationY += 2;
break;
case GestureType.POINT:
// 指向-切换楼层
if (pose.handPosition.y < 0.3) {
this.currentFloor = Math.min(3, this.currentFloor + 1);
} else if (pose.handPosition.y > 0.7) {
this.currentFloor = Math.max(1, this.currentFloor - 1);
}
break;
}
}
build() {
Stack() {
// 场景光效背景
this.buildSceneLightBackground()
// 3D建筑模型层
Column() {
// 建筑轮廓示意(实际项目中使用3D引擎渲染)
this.buildBuildingModel()
// 热点标注
if (this.showHotspots) {
this.buildHotspots()
}
}
.width('100%')
.height('100%')
.scale({ x: this.modelScale, y: this.modelScale })
.translate({ x: this.modelOffsetX, y: this.modelOffsetY })
.rotate({ x: 0, y: 1, z: 0, angle: this.modelRotationY })
.animation({
duration: 300,
curve: Curve.EaseInOut
})
// 楼层指示器
this.buildFloorIndicator()
// Body AR控制层
BodyARController()
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
.width('100%')
.height('100%')
}
@Builder
buildSceneLightBackground(): void {
Column() {
// 天空光效
Column()
.width('100%')
.height('60%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: this.getSkyColors()
})
// 地面反射
Column()
.width('100%')
.height('40%')
.backgroundColor('rgba(0,0,0,0.3)')
.linearGradient({
direction: GradientDirection.Top,
colors: [
['rgba(255,255,255,0.05)', 0.0],
['transparent', 1.0]
]
})
// 环境光晕
Column()
.width(800)
.height(800)
.backgroundColor(this.themeColor)
.blur(200)
.opacity(0.06)
.position({ x: '50%', y: '30%' })
.anchor('50%')
.animation({
duration: 10000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
}
.width('100%')
.height('100%')
.backgroundColor('#0a0a0f')
}
private getSkyColors(): Array<[string, number]> {
const colors: Map<string, Array<[string, number]>> = new Map([
['dawn', [['#FF9A76', 0.0], ['#FFD93D', 0.5], ['#87CEEB', 1.0]]],
['noon', [['#87CEEB', 0.0], ['#E0F7FA', 0.5], ['#FFFFFF', 1.0]]],
['dusk', [['#4A69BD', 0.0], ['#FF6B6B', 0.5], ['#FFD93D', 1.0]]],
['night', [['#0C1445', 0.0], ['#1E3A5F', 0.5], ['#2C3E50', 1.0]]]
]);
return colors.get(this.sceneTime) || [['#87CEEB', 0.0], ['#FFFFFF', 1.0]];
}
@Builder
buildBuildingModel(): void {
Column() {
// 建筑主体(简化示意)
Column() {
// 屋顶
Triangle()
.width(200)
.height(100)
.fill('#8B7355')
.position({ x: '50%', y: 0 })
.anchor('50%')
// 楼层
ForEach([1, 2, 3], (floor) => {
Column() {
Row({ space: 8 }) {
ForEach([1, 2, 3, 4], (window) => {
Rectangle()
.width(30)
.height(40)
.fill(floor === this.currentFloor ? '#FFEAA7' : '#4A5568')
.borderRadius(4)
.shadow({
radius: floor === this.currentFloor ? 8 : 0,
color: '#FFEAA7'
})
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Text(`F${floor}`)
.fontSize(10)
.fontColor('#666666')
.margin({ top: 4 })
}
.width(220)
.height(100)
.backgroundColor(floor === this.currentFloor ? 'rgba(255,234,167,0.1)' : 'rgba(74,85,104,0.3)')
.borderRadius(8)
.margin({ top: 4 })
.border({
width: floor === this.currentFloor ? 2 : 0,
color: '#FFEAA7'
})
})
}
.width(240)
.height(380)
.position({ x: '50%', y: '50%' })
.anchor('50%')
}
.width('100%')
.height('100%')
}
@Builder
buildHotspots(): void {
// 热点标注点
ForEach([
{ x: 120, y: 150, label: '主卧', info: '32㎡ · 南向' },
{ x: 180, y: 200, label: '客厅', info: '45㎡ · 落地窗' },
{ x: 80, y: 250, label: '厨房', info: '15㎡ · 开放式' }
], (spot) => {
Column() {
Circle()
.width(12)
.height(12)
.fill('#FF6B6B')
.shadow({
radius: 8,
color: '#FF6B6B'
})
.animation({
duration: 1500,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
.scale({ x: 1.2, y: 1.2 })
Text(spot.label)
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor('rgba(0,0,0,0.6)')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ top: 4 })
}
.position({ x: spot.x, y: spot.y })
.onClick(() => {
AppStorage.setOrCreate('selected_hotspot', spot);
})
})
}
@Builder
buildFloorIndicator(): void {
Column({ space: 8 }) {
ForEach([3, 2, 1], (floor) => {
Button(`F${floor}`)
.fontSize(12)
.fontColor(floor === this.currentFloor ? '#FFFFFF' : '#666666')
.backgroundColor(floor === this.currentFloor ? '#4ECDC4' : 'rgba(255,255,255,0.1)')
.width(40)
.height(40)
.borderRadius(20)
.onClick(() => {
this.currentFloor = floor;
})
})
}
.width('auto')
.height('auto')
.padding(8)
.backgroundColor('rgba(0,0,0,0.4)')
.borderRadius(12)
.position({ x: '95%', y: '50%' })
.anchor('100% 50%')
.backdropFilter($r('sys.blur.10'))
}
}
5.5 悬浮项目导航页签(FloatTabNavigation.ets)
// entry/src/main/ets/components/FloatTabNavigation.ets
import { window } from '@kit.ArkUI';
import { HdsTabs, SystemMaterialEffect } from '@kit.UIDesignKit';
export enum TransparencyLevel {
STRONG = 0.85,
BALANCED = 0.70,
WEAK = 0.55
}
interface ProjectTab {
id: string;
name: string;
theme: string;
type: string;
area: string;
icon: Resource;
}
@Component
export struct FloatTabNavigation {
@Prop currentIndex: number = 0;
@State navTransparency: number = TransparencyLevel.BALANCED;
@State isExpanded: boolean = false;
@State bottomAvoidHeight: number = 0;
@State tabs: ProjectTab[] = [
{ id: '1', name: '云栖别墅', theme: '#4ECDC4', type: '别墅', area: '380㎡', icon: $r('app.media.ic_villa') },
{ id: '2', name: '湖畔公寓', theme: '#FF6B6B', type: '公寓', area: '120㎡', icon: $r('app.media.ic_apartment') },
{ id: '3', name: '商业综合体', theme: '#FFEAA7', type: '商业', area: '5000㎡', icon: $r('app.media.ic_commercial') },
{ id: '4', name: '文创园区', theme: '#96CEB4', type: '园区', area: '12000㎡', icon: $r('app.media.ic_park') }
];
aboutToAppear(): void {
this.getBottomAvoidArea();
}
private async getBottomAvoidArea(): Promise<void> {
try {
const mainWindow = await window.getLastWindow();
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
this.bottomAvoidHeight = avoidArea.bottomRect.height;
} catch (error) {
console.error('Failed to get avoid area:', error);
}
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
this.contentBuilder()
}
.padding({ bottom: this.bottomAvoidHeight + 88 })
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.backgroundBlurStyle(BlurStyle.REGULAR)
.opacity(this.navTransparency)
.backdropFilter($r('sys.blur.20'))
Column()
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Top,
colors: [
['rgba(255,255,255,0.15)', 0.0],
['rgba(255,255,255,0.05)', 1.0]
]
})
}
.width('100%')
.height('100%')
.borderRadius(20)
.shadow({
radius: 20,
color: 'rgba(0,0,0,0.2)',
offsetX: 0,
offsetY: -4
})
Row() {
ForEach(this.tabs, (tab: ProjectTab, index: number) => {
this.buildProjectTab(tab, index)
}, (tab: ProjectTab) => tab.id)
}
.width('100%')
.height(64)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.Start)
if (this.isExpanded) {
this.buildTransparencyPanel()
}
}
.width('96%')
.height(this.isExpanded ? 108 : 64)
.margin({
bottom: this.bottomAvoidHeight + 12,
left: '2%',
right: '2%'
})
.animation({
duration: 300,
curve: Curve.Spring,
iterations: 1
})
.gesture(
LongPressGesture({ duration: 500 })
.onAction(() => {
this.isExpanded = !this.isExpanded;
})
)
}
.width('100%')
.height('100%')
}
@Builder
buildProjectTab(tab: ProjectTab, index: number): void {
Row({ space: 6 }) {
Image(tab.icon)
.width(16)
.height(16)
.fillColor(tab.theme)
Text(tab.name)
.fontSize(13)
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentIndex === index ? '#FFFFFF' : '#AAAAAA')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(80)
Text(`${tab.type} · ${tab.area}`)
.fontSize(10)
.fontColor('#666666')
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
if (this.currentIndex === index) {
Button({ type: ButtonType.Circle }) {
Text('×')
.fontSize(14)
.fontColor('#AAAAAA')
}
.width(20)
.height(20)
.backgroundColor('transparent')
.onClick(() => {
this.closeTab(index);
})
}
}
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor(this.currentIndex === index
? 'rgba(255,255,255,0.15)'
: 'transparent')
.borderRadius(12)
.border({
width: this.currentIndex === index ? 1 : 0,
color: tab.theme
})
.onClick(() => {
this.currentIndex = index;
AppStorage.setOrCreate('current_project', tab.name);
AppStorage.setOrCreate('current_theme', tab.theme);
})
}
@Builder
buildTransparencyPanel(): void {
Row({ space: 12 }) {
Text('透明度')
.fontSize(12)
.fontColor('#AAAAAA')
Slider({
value: this.navTransparency * 100,
min: 55,
max: 85,
step: 15,
style: SliderStyle.InSet
})
.width(120)
.onChange((value: number) => {
this.navTransparency = value / 100;
})
Text(`${Math.round(this.navTransparency * 100)}%`)
.fontSize(12)
.fontColor('#AAAAAA')
Button('强')
.fontSize(11)
.backgroundColor(this.navTransparency === TransparencyLevel.STRONG
? '#4ECDC4'
: 'rgba(255,255,255,0.1)')
.onClick(() => { this.navTransparency = TransparencyLevel.STRONG; })
Button('平衡')
.fontSize(11)
.backgroundColor(this.navTransparency === TransparencyLevel.BALANCED
? '#4ECDC4'
: 'rgba(255,255,255,0.1)')
.onClick(() => { this.navTransparency = TransparencyLevel.BALANCED; })
Button('弱')
.fontSize(11)
.backgroundColor(this.navTransparency === TransparencyLevel.WEAK
? '#4ECDC4'
: 'rgba(255,255,255,0.1)')
.onClick(() => { this.navTransparency = TransparencyLevel.WEAK; })
}
.width('100%')
.height(44)
.justifyContent(FlexAlign.Center)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius({ topLeft: 12, topRight: 12 })
}
private closeTab(index: number): void {
if (this.tabs.length <= 1) return;
this.tabs.splice(index, 1);
if (this.currentIndex >= index && this.currentIndex > 0) {
this.currentIndex--;
}
}
@BuilderParam contentBuilder: () => void = this.defaultContentBuilder;
@Builder
defaultContentBuilder(): void {
Column() {
Text('建筑漫游区域')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
5.6 多窗口光效同步管理器(WindowManager.ets)
// entry/src/main/ets/utils/WindowManager.ets
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export interface ToolWindowConfig {
name: string;
title: string;
width: number;
height: number;
x?: number;
y?: number;
followMainWindow?: boolean;
themeColor?: string;
}
export class WindowManager {
private static instance: WindowManager;
private mainWindow: window.Window | null = null;
private subWindows: Map<string, window.Window> = new Map();
static getInstance(): WindowManager {
if (!WindowManager.instance) {
WindowManager.instance = new WindowManager();
}
return WindowManager.instance;
}
async initializeMainWindow(windowStage: window.WindowStage): Promise<void> {
this.mainWindow = windowStage.getMainWindowSync();
await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
await this.mainWindow.setWindowTitleBarEnable(false);
await this.mainWindow.setWindowShadowEnabled(true);
await this.mainWindow.setWindowCornerRadius(12);
await this.mainWindow.setWindowBackgroundColor('#00000000');
this.mainWindow.on('windowFocusChange', (isFocused: boolean) => {
AppStorage.setOrCreate('window_focused', isFocused);
if (isFocused) {
this.syncGlobalLightEffect(AppStorage.get<string>('global_theme_color') || '#4ECDC4');
}
});
console.info('Main window initialized for Zhujing Space');
}
async createToolWindow(config: ToolWindowConfig): Promise<window.Window | null> {
try {
if (!this.mainWindow) {
throw new Error('Main window not initialized');
}
const subWindow = await this.mainWindow.createSubWindow(config.name);
await subWindow.setWindowSizeType(window.WindowSizeType.FREE);
await subWindow.moveWindowTo({ x: config.x ?? 100, y: config.y ?? 100 });
await subWindow.resize(config.width, config.height);
await subWindow.setWindowBackgroundColor('#00000000');
await subWindow.setWindowShadowEnabled(true);
await subWindow.setWindowCornerRadius(16);
await subWindow.setWindowTopmost(true);
this.subWindows.set(config.name, subWindow);
await subWindow.setUIContent(`pages/${config.name}`);
await subWindow.showWindow();
if (config.followMainWindow) {
this.setupWindowFollow(subWindow, config);
}
this.syncSubWindowLightEffect(subWindow, config.name, config.themeColor);
return subWindow;
} catch (error) {
console.error(`Failed to create tool window:`, (error as BusinessError).message);
return null;
}
}
private setupWindowFollow(subWindow: window.Window, config: ToolWindowConfig): void {
this.mainWindow?.on('windowRectChange', (data: window.RectChangeOptions) => {
if (data.rectChangeReason === window.RectChangeReason.MOVE) {
const mainRect = this.mainWindow?.getWindowProperties().windowRect;
if (mainRect) {
subWindow.moveWindowTo({
x: mainRect.left + (config.x ?? 100),
y: mainRect.top + (config.y ?? 100)
});
}
}
});
}
private syncSubWindowLightEffect(subWindow: window.Window, name: string, themeColor?: string): void {
subWindow.on('windowFocusChange', (isFocused: boolean) => {
AppStorage.setOrCreate(`window_${name}_focused`, isFocused);
if (isFocused && themeColor) {
AppStorage.setOrCreate('global_theme_color', themeColor);
}
});
AppStorage.watch('global_theme_color', (color: string) => {
console.info(`Syncing theme color ${color} to window ${name}`);
});
}
async syncGlobalLightEffect(color: string): Promise<void> {
AppStorage.setOrCreate('global_theme_color', color);
}
async openLayoutWindow(): Promise<void> {
await this.createToolWindow({
name: 'LayoutWindow',
title: '户型详情',
width: 400,
height: 600,
x: 1000,
y: 100,
themeColor: '#4ECDC4'
});
}
async openMaterialWindow(): Promise<void> {
await this.createToolWindow({
name: 'MaterialWindow',
title: '材质库',
width: 350,
height: 500,
x: 200,
y: 100,
themeColor: '#FFEAA7'
});
}
async openSunlightWindow(): Promise<void> {
await this.createToolWindow({
name: 'SunlightWindow',
title: '日照分析',
width: 500,
height: 400,
x: 50,
y: 500,
themeColor: '#FF6B6B'
});
}
async closeToolWindow(name: string): Promise<void> {
const subWindow = this.subWindows.get(name);
if (subWindow) {
await subWindow.destroyWindow();
this.subWindows.delete(name);
}
}
}
5.7 浮动户型详情窗口(LayoutWindow.ets)
// entry/src/main/ets/pages/LayoutWindow.ets
@Entry
@Component
struct LayoutWindow {
@State layoutInfo: string = '户型:四室两厅三卫\n建筑面积:380㎡\n套内面积:320㎡\n得房率:84.2%\n\n功能分区:\n- 一层:客厅、餐厅、厨房、老人房\n- 二层:主卧套房、儿童房、书房\n- 三层:露台、健身房、储藏室';
@State isFocused: boolean = false;
@State themeColor: string = '#4ECDC4';
aboutToAppear(): void {
AppStorage.watch('window_LayoutWindow_focused', (focused: boolean) => {
this.isFocused = focused;
});
AppStorage.watch('current_theme', (color: string) => {
this.themeColor = color;
});
}
build() {
Stack() {
Column() {
Column()
.width(400)
.height(400)
.backgroundColor(this.themeColor)
.blur(150)
.opacity(this.isFocused ? 0.1 : 0.05)
.position({ x: '50%', y: '30%' })
.anchor('50%')
}
.width('100%')
.height('100%')
.backgroundColor('#0f0f1a')
Column() {
Row() {
Text('户型详情')
.fontSize(14)
.fontColor(this.themeColor)
.fontWeight(FontWeight.Bold)
Row({ space: 8 }) {
Circle().width(12).height(12).fill('#FF5F56')
Circle().width(12).height(12).fill('#FFBD2E')
Circle().width(12).height(12).fill('#27C93F')
}
}
.width('100%')
.height(36)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(`rgba(${this.hexToRgb(this.themeColor)},0.05)`)
Scroll() {
Text(this.layoutInfo)
.fontSize(14)
.fontColor('#E0E0E0')
.lineHeight(22)
.width('100%')
}
.width('100%')
.layoutWeight(1)
.padding(16)
Row({ space: 12 }) {
Button('导出CAD')
.type(ButtonType.Capsule)
.fontSize(13)
.backgroundColor(this.themeColor)
.width(120)
Button('VR漫游')
.type(ButtonType.Capsule)
.fontSize(13)
.backgroundColor('rgba(255,255,255,0.1)')
.width(120)
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
private hexToRgb(hex: string): string {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ?
`${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}`
: '78,205,196';
}
}
5.8 主页面集成(ArchitecturePage.ets)
// entry/src/main/ets/pages/ArchitecturePage.ets
import { ImmersiveTitleBar, SceneTime } from '../components/ImmersiveTitleBar';
import { FloatTabNavigation } from '../components/FloatTabNavigation';
import { BuildingViewer } from '../components/BuildingViewer';
import { WindowManager } from '../utils/WindowManager';
@Entry
@Component
struct ArchitecturePage {
@State currentProject: number = 0;
@State currentScene: SceneTime = SceneTime.NOON;
@State currentTheme: string = '#4ECDC4';
@State currentFloor: string = '一层平面';
@State useBodyAR: boolean = true;
@State lightIntensity: number = 0.6;
private sceneCycle: SceneTime[] = [SceneTime.DAWN, SceneTime.NOON, SceneTime.DUSK, SceneTime.NIGHT];
aboutToAppear(): void {
AppStorage.watch('current_project', (project: string) => {
// 项目切换逻辑
});
AppStorage.watch('view_action', (action: string) => {
if (action === 'cycle_time') {
this.cycleSceneTime();
} else if (action === 'switch_floor') {
// 切换楼层逻辑
}
});
AppStorage.watch('window_action', (action: string) => {
if (action === 'open_layout') {
WindowManager.getInstance().openLayoutWindow();
} else if (action === 'open_material') {
WindowManager.getInstance().openMaterialWindow();
} else if (action === 'open_sunlight') {
WindowManager.getInstance().openSunlightWindow();
}
});
}
private cycleSceneTime(): void {
const currentIndex = this.sceneCycle.indexOf(this.currentScene);
const nextIndex = (currentIndex + 1) % this.sceneCycle.length;
this.currentScene = this.sceneCycle[nextIndex];
this.currentTheme = this.getSceneColor(this.currentScene);
WindowManager.getInstance().syncGlobalLightEffect(this.currentTheme);
}
private getSceneColor(scene: SceneTime): string {
const colors: Map<SceneTime, string> = new Map([
[SceneTime.DAWN, '#FFD93D'],
[SceneTime.NOON, '#FFFFFF'],
[SceneTime.DUSK, '#FF6B6B'],
[SceneTime.NIGHT, '#4A69BD']
]);
return colors.get(scene) || '#FFFFFF';
}
build() {
Stack() {
this.buildAmbientLightLayer()
Column() {
ImmersiveTitleBar({
currentProject: this.getProjectName(this.currentProject),
currentFloor: this.currentFloor,
sceneTime: this.currentScene
})
BuildingViewer({
sceneTime: this.currentScene,
themeColor: this.currentTheme
})
.layoutWeight(1)
}
.width('100%')
.height('100%')
FloatTabNavigation({
currentIndex: this.currentProject,
onTabChange: (index: number) => {
this.currentProject = index;
this.currentTheme = this.getProjectTheme(index);
WindowManager.getInstance().syncGlobalLightEffect(this.currentTheme);
},
contentBuilder: () => {}
})
}
.width('100%')
.height('100%')
.backgroundColor('#0a0a0f')
.expandSafeArea(
[SafeAreaType.SYSTEM],
[SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
)
}
private getProjectName(index: number): string {
const names = ['云栖别墅', '湖畔公寓', '商业综合体', '文创园区'];
return names[index] || '云栖别墅';
}
private getProjectTheme(index: number): string {
const themes = ['#4ECDC4', '#FF6B6B', '#FFEAA7', '#96CEB4'];
return themes[index] || '#4ECDC4';
}
@Builder
buildAmbientLightLayer(): void {
Column() {
Column()
.width(600)
.height(600)
.backgroundColor(this.currentTheme)
.blur(180)
.opacity(this.lightIntensity * 0.3)
.position({ x: '50%', y: '25%' })
.anchor('50%')
.animation({
duration: 7000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
.scale({ x: 1.4, y: 1.4 })
Column()
.width('100%')
.height(250)
.backgroundColor(this.currentTheme)
.opacity(this.lightIntensity * 0.08)
.blur(120)
.position({ x: 0, y: '75%' })
.linearGradient({
direction: GradientDirection.Top,
colors: [
[this.currentTheme, 0.0],
['transparent', 1.0]
]
})
}
.width('100%')
.height('100%')
.backgroundColor('#050508')
}
}
六、关键技术总结
6.1 沉浸光感实现清单
| 技术点 | API/方法 | 应用场景 |
|---|---|---|
| 系统材质效果 | systemMaterialEffect: SystemMaterialEffect.IMMERSIVE |
HdsNavigation标题栏 |
| 背景模糊 | backgroundBlurStyle(BlurStyle.REGULAR) |
悬浮导航玻璃拟态 |
| 背景滤镜 | backdropFilter($r('sys.blur.20')) |
精细模糊控制 |
| 安全区扩展 | expandSafeArea([SafeAreaType.SYSTEM], [...]) |
全屏沉浸布局 |
| 窗口沉浸 | setWindowLayoutFullScreen(true) |
无边框模式 |
| 光效动画 | animation({ duration, iterations: -1 }) |
呼吸灯背景 |
| 动态透明度 | backgroundOpacity |
焦点感知降级 |
6.2 Body AR实现要点
| 技术点 | API/方法 | 说明 |
|---|---|---|
| AR会话创建 | arEngine.createARSession({ mode: ARMode.BODY }) |
启用人体模式 |
| 人体跟踪器 | session.createBodyTracker({ enableSkeleton: true }) |
启用骨骼跟踪 |
| 骨骼关节获取 | skeleton.getJoint(BodyJointType.LEFT_HAND) |
获取手部位置 |
| 手势识别 | 分析关节位置与速度 | 识别张开/握拳/滑动等 |
| 3D模型操控 | 绑定手势到模型变换属性 | 实时同步 |
6.3 PC端多窗口光效协同
- 主窗口:全屏沉浸,环境光背景延伸至所有安全区边缘
- 浮动工具窗口:置顶、圆角、阴影,跟随主窗口移动
- 光效同步:通过
AppStorage全局状态实现跨窗口主题色联动 - 焦点感知:窗口激活时边缘发光增强,失活时自动降低光效强度
七、调试与性能优化
7.1 真机调试建议
- Body AR效果:需要支持AR Engine的真机设备,确保摄像头视野覆盖全身
- 手势识别校准:不同用户体型差异可能需要调整识别阈值
- 3D渲染性能:建议使用
XComponent配合原生3D引擎进行高性能渲染
7.2 性能优化策略
// 1. AR性能优化
private optimizeARPerformance(): void {
if (this.arSession) {
this.arSession.setCameraConfig({
fps: 15,
resolution: { width: 640, height: 480 }
});
}
}
// 2. 3D渲染优化
aboutToDisappear(): void {
this.pauseRendering = true;
}
// 3. 窗口创建优化
private async lazyLoadToolWindows(): Promise<void> {
if (!this.layoutWindow) {
this.layoutWindow = await WindowManager.getInstance().openLayoutWindow();
}
}
八、总结与展望
本文基于HarmonyOS 6(API 23)的悬浮导航、沉浸光感与Body AR特性,完整实战了一款面向PC端的"筑境空间"沉浸式建筑漫游与设计评审平台。核心创新点总结:
-
场景时间感知光效系统:根据建筑场景时间(清晨/正午/黄昏/夜晚)动态切换主题色,从天空渐变到环境光晕形成统一的空间氛围
-
Body AR手势操控:基于AR Engine人体骨骼跟踪,实现张开手掌放大、握拳旋转、身体倾斜漫游等自然交互,让设计师"身临其境"地操控建筑模型
-
悬浮项目导航:底部悬浮页签替代传统项目栏,玻璃拟态设计+三档透明度调节,在保持导航可达性的同时最大化漫游区域
-
PC级多窗口设计协作:主漫游窗口 + 浮动户型详情 + 材质库 + 日照分析的四层架构,通过
WindowManager实现跨窗口光效联动与焦点感知
未来扩展方向:
- 接入分布式软总线,实现跨设备协同设计(手机扫码查看、平板手绘批注、PC主控漫游)
- AI辅助设计:基于当前建筑模型,AI推荐优化方案并以光效形式提示改进区域
- VR/AR融合:支持VR头显接入,实现完全沉浸式的建筑漫游体验
- 数字孪生:接入IoT传感器数据,实时展示建筑能耗、温湿度等运营指标
转载自:https://blog.csdn.net/u014727709/article/details/137143865
欢迎 👍点赞✍评论⭐收藏,欢迎指正
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)