山海万灵 HarmonyOS 文化知识设计续篇(10):地图缩放与区域选中态协同方案
一、文化地图需要把视口和区域语义同时保留下来
山海世界的地域不是普通列表项。读者在昆仑、青丘或海外三经之间切换时,既需要看清地图上的空间关系,也需要在进入展厅、查看神兽详情后回到此前的区域。区域选择已经承担“当前探索位置”的语义;缩放能力应当只负责视口,而不能覆盖这个语义。
本方案将地图交互拆成两份可独立恢复的数据:selectedRegionId表示读者正在探索的地域,viewport表示地图画布当前的比例和偏移。前者参与展厅、导览和进度计算,后者只服务于阅读视野。两个状态在同一次手势中可以一起变化,但它们的来源、保存频率和失效条件不同。
| 状态 | 解决的问题 | 生命周期 |
|---|---|---|
selectedRegionId | 确定高亮地域、展厅入口和探索上下文 | 跨页面保留 |
viewport.scale | 控制地图阅读颗粒度 | 当前地图会话优先 |
viewport.offsetX/Y | 保留读者观察的位置 | 经节流后保存 |
gesturePhase | 防止点击与双指缩放互相抢占 | 单次手势结束即清理 |
ArkUI 的可观察状态应直接驱动选中样式和画布变换;关于状态更新边界可参考 ArkTS 状态管理概述。
二、先划清地图视口的能力边界
缩放的目标是帮助读者阅读地域分布、连接线和图例,不是提供无限画布,也不承担地理测量。为避免小屏设备上出现“放大后找不回内容”的问题,视口状态必须具备最小比例、最大比例和边界回弹。区域卡片仍是主要的无障碍选择入口;手势不可用时,读者可以通过卡片完成同样的地域切换。
| 纳入范围 | 设计处理 |
|---|---|
| 双指缩放与双击复位 | 将比例限制在 0.85 至 2.40,复位回到默认视图 |
| 单击地域 | 写入选中地域,再将视口平滑聚焦到该地域锚点 |
| 返回地图 | 先恢复有效地域,再恢复合法的视口比例和偏移 |
| 字体放大或窗口变化 | 重新计算地图边界,保留选中地域而非旧坐标 |
| 不纳入范围 | 原因 |
|---|---|
| 无限制缩放 | 会造成浮点误差、内容丢失与命中区域失真 |
| 手势直接修改探索进度 | 视口移动不代表完成探索行为 |
| 以屏幕坐标持久化地域 | 横竖屏和大屏布局变化后坐标没有语义 |
三、用一个显式模型承接手势、选中和恢复
地图页只保存渲染所需的即时状态,领域层保存能够跨页面恢复的语义状态。regionId无法在最新地域集合中找到时,恢复流程回退到默认地域;比例或偏移非法时,只重置视口,不丢弃已经选择的地域。
interface MapViewport { scale: number offsetX: number offsetY: number } interface MapInteractionState { selectedRegionId: string viewport: MapViewport gesturePhase: 'IDLE' | 'PINCHING' | 'PANNING' } const DEFAULT_VIEWPORT: MapViewport = { scale: 1, offsetX: 0, offsetY: 0 }比例和位移必须经过统一的归一化函数。这样可以避免一次异常手势把画布带到不可见区域,也让恢复逻辑和实时渲染共享同一套约束。
function normalizeViewport(input: MapViewport, bounds: MapBounds): MapViewport { const scale = Math.max(0.85, Math.min(input.scale, 2.40)) const maxX = Math.max(0, bounds.contentWidth * scale - bounds.viewportWidth) / 2 const maxY = Math.max(0, bounds.contentHeight * scale - bounds.viewportHeight) / 2 return { scale, offsetX: Math.max(-maxX, Math.min(input.offsetX, maxX)), offsetY: Math.max(-maxY, Math.min(input.offsetY, maxY)) } }四、让缩放手势和区域点击有确定的优先级
同一张地图里既有可点击地域,又有缩放、拖拽和双击复位。事件调度需要先判断手势是否已经跨过移动阈值:双指缩放或明显拖动发生后,本次触点不再触发地域选择;单击在阈值内结束时,才交给地域命中测试。这条规则能避免读者放大地图时误入某个展厅。
function reduceGesture(state: MapInteractionState, event: MapGesture): MapInteractionState { if (event.type === 'PINCH') { return { ...state, gesturePhase: 'PINCHING', viewport: normalizeViewport({ scale: state.viewport.scale * event.scaleDelta, offsetX: state.viewport.offsetX + event.focusDeltaX, offsetY: state.viewport.offsetY + event.focusDeltaY }, event.bounds) } } if (event.type === 'TAP' && state.gesturePhase === 'IDLE' && event.regionId) { return { ...state, selectedRegionId: event.regionId } } return { ...state, gesturePhase: 'IDLE' } }选中地域发生变化后,页面不直接推导展厅和导览结果。它只发出领域命令,由探索状态协调器校验地域与展厅的对应关系,并将可恢复的选择写入本地进度。这样,卡片选择、地图点击和深链进入可以复用同一个入口。
五、把“选中地域”接入已有探索位置,而不是另存一份影子状态
已有探索位置由地域标识和展厅标识组成。地图方案继续使用这一语义主键,并增加独立的视口快照。保存顺序是“选择先确认、视口后节流”:区域切换必须立刻可恢复,连续缩放只在手势结束或短暂静止后写入,避免 Preferences 被高频写入拖慢界面。
async function commitRegionSelection(regionId: string): Promise<void> { const region = regionCatalog.find((item: RegionItem) => item.id === regionId) if (region === undefined) { return } state.selectedRegionId = region.id await explorationStore.savePosition({ selectedRegionId: region.id, selectedHallId: region.hallId }) } async function commitViewportAfterGesture(viewport: MapViewport): Promise<void> { const safeViewport = normalizeViewport(viewport, currentBounds) await mapViewStore.saveViewport(safeViewport) }恢复时按“数据有效性优先于视觉连续性”处理。地域目录更新、窗口尺寸变化或资源包替换都可能让旧视口不再适用;此时保留可识别的地域,重算画布边界,再使用默认比例。与轻量设置有关的持久化能力可按 Preferences 开发指导实现。
六、为异常路径准备可理解的降级行为
| 失败场景 | 页面行为 | 数据处理 |
|---|---|---|
| 地域目录尚未加载 | 显示骨架和不可点击地图,不写入选择 | 等目录就绪后恢复默认地域 |
| 保存视口失败 | 继续显示当前视口,并提示下次可能从默认视图进入 | 不影响已确认的地域选择 |
| 旧地域已下线 | 回退到默认地域,展示可选择的替代地域 | 清理失效地域标识 |
| 大屏尺寸变化 | 重新计算可移动边界并保留选中高亮 | 仅覆盖超出边界的偏移 |
| 手势能力不可用 | 保留地域卡片、复位按钮和键盘焦点路径 | 不依赖双指操作完成探索 |
网络内容不可用时,地图可以展示本地已知地域和当前选择,但不能把临时视口误当作探索进度。读者能够继续浏览已加载内容,待数据恢复后再同步可用地域集合。
七、实施顺序从纯状态开始,再接入画布手势
第一步建立MapViewport、边界归一化和恢复函数,并用固定输入验证比例钳制、偏移回弹和失效地域回退。第二步让地域卡片与地图命中区域共用选择命令,确保两种入口产生同一份探索位置。第三步接入缩放与拖拽手势,最后把节流后的视口快照写入本地存储。
| 阶段 | 交付物 | 验收条件 |
|---|---|---|
| 状态层 | 视口模型、归一化函数、恢复策略 | 非法比例与越界偏移被稳定收敛 |
| 选择层 | 地图和卡片共用的地域命令 | 两个入口切换到同一展厅与高亮 |
| 手势层 | 缩放、拖拽、双击复位 | 缩放时不触发误选,复位可预测 |
| 持久化层 | 节流视口快照与地域位置 | 重进地图后恢复合法的选择与视野 |
验收动作包括:选择青丘后进入展厅再返回;在放大状态拖动到边界后复位;旋转或切换宽窗口后确认高亮仍指向同一地域;清除无效快照后确认地图回到默认视图。每个动作都应回读选中地域、比例、偏移和最终可见区域,而不是只检查按钮文字。
八、方案收束:语义状态稳定,视口状态可控
地图缩放的可用性取决于它是否服从探索语义。选中地域继续作为跨页面的主状态,视口作为受边界保护的阅读状态;手势、卡片和恢复流程通过统一命令协作。这样的分层让后续接入多设备布局、离线目录和更复杂的地图标记时仍能保持明确的失败路径与验收边界。
