Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/apps/mobile/harmonyos/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ source scripts/ohos-env.sh
## Visual reference fidelity

- Before drawing a system glyph, text approximation, or new bitmap, search the existing HarmonyOS media resources and the approved desktop reference images. Reuse the established asset when one exists.
- Conversation header controls must use the approved `remote_ref_back` and `remote_ref_more` assets. Do not replace them with a system chevron or text such as `...` / bullet characters.
- Conversation header controls must use the approved `remote_ref_back` and `remote_ref_more` assets. Do not replace them with a system chevron or text such as `...` / bullet characters. Compact top-left open-sidebar uses `CompactMenuButton` (`gpt_home_menu_glyph`). Wide restore/collapse uses `SidebarToggleButton` (the sidebar-pane glyph).
- Render monochrome reference assets in template mode and tint them with semantic theme colors such as `INK`. Never rely on the bitmap's original black or white pixels; the same control must remain legible in light and dark themes.
- Keep paired header controls on the same fixed touch-target size and optical alignment. A responsive layout may reposition a control, but must not silently change its icon geometry or visual weight.
- Keep `SymbolGlyph` geometry separate from its touch target. When a glyph is clickable or sits in a decorated control, wrap it in `Stack({ alignContent: Alignment.Center })` (or use a centered `Button`) and give the glyph its visual size; do not stretch the glyph itself to the full 32vp/40vp/44vp target, because font metrics can make the icon look off-center.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,30 @@ Scope: `src/apps/mobile/harmonyos`,主要涉及双屏/三屏布局、会话来

## 实施状态

截至 2026-08-07:
截至 2026-08-13:

### 已实现

- 展开态只要存在可用纵向折痕(双折叠一道、三折叠两道),且当前视口放得下 master + detail,就进入 master-detail;合盖仍强制单屏。无折痕时只有平板走宽屏,普通手机即使超宽也保持单屏。
- 布局判定对齐官方 GridRow 横向断点:md `600vp` 起双栏,lg `840vp` 起 extra-wide;合盖与悬停(`HALF_FOLDED`)强制不走 master-detail。不再用 `deviceType === tablet` 决定宽屏。
- 展开折叠屏若 `getCurrentFoldCreaseRegion()` 为空,合成中线铰链(16vp)作为 master/detail 分界,避免内容压在铰链上。
- 悬停态会话使用官方 `FolderStack`:上半为显示区(时间线),下半为操作区(Composer)。
- 展开态只要存在可用纵向折痕(双折叠一道、三折叠两道),且当前视口放得下 master + detail,就进入 master-detail。
- 宽屏本地与远程左栏在相同位置复用固定的“本地 / Remote”分段选择器,选中态由当前路由推导。
- 来源选择器使用独立来源切换动作,不复用单屏 `enterCodeEntry()`;具体会话行只选择当前来源内的会话。
- 宽屏来源切换使用 `clear(false)` 与 `pushPath(..., false)` 无转场替换路由,不播放页面进入或退出动画。
- 宽屏本地和远程 master pane 均不提供全局侧边栏入口;远程宽屏标题栏已移除会叠加本地 drawer 的菜单按钮,单屏标题栏菜单保持不变。
- 本地与远程来源切换使用路由合同计算目标,不累积跨来源返回路径。
- 切回本地时停止手机侧远程轮询,但不清空远程活动会话或断开桌面。
- 通过 `display.getCurrentFoldCreaseRegion()` 读取折痕;第一道有效纵向折痕用于 master/detail 分界,无有效折痕时回退到 `344`。
- 通过 `display.getCurrentFoldCreaseRegion()` 读取折痕;第一道有效纵向折痕用于 master/detail 分界。
- 双折叠展开打开文件预览时,会话/预览分界对齐那一道折痕,而不是按视口对半切。
- 两道折痕或超宽宽度被识别为 extra-wide,但继续使用同一个 master-detail,不创建第三栏。
- 两道折痕或 lg 宽度被识别为 extra-wide,但会话仍使用同一个 master-detail,不创建第三会话栏。官方聊天分栏在 md/lg/xl 均为列表|会话;第三窗格只用于文件预览。
- 第二道折痕会把 detail 划分为无折痕候选内容带;会话视图选择最宽的候选带,同宽时优先右侧,避免 Composer、菜单和消息内操作热区跨越折痕。
- extra-wide detail 外层仍覆盖中间和右侧两屏;上述内容带只是 presentation 几何约束,不增加第三个业务 pane。
- 监听 `foldStatusChange` 与 `foldDisplayModeChange`,折叠形态变化时重算几何。

### 已验证

- 下列真机项仍以改前 HAP 为准,本轮官方断点/合成铰链/悬停改动需重新安装后复验。
- `assembleHap` 在固定来源选择器和第二道折痕安全内容带实现后重新构建通过;主题颜色治理检查通过。
- HarmonyOS 本地单元测试任务通过,覆盖折叠优先级、来源路由、连续来源切换不增长返回栈、零/一/两道折痕、非法折痕和非对称三屏几何。
- `ConversationLayoutPolicy` 的新增行、函数和分支覆盖率均为 100%。
Expand All @@ -41,13 +46,14 @@ Scope: `src/apps/mobile/harmonyos`,主要涉及双屏/三屏布局、会话来

### 待验证

- 本轮改动后的 Mate X7 展开态:折痕为空时应走中线铰链,而不是固定 344。
- Mate X7 悬停态:`FolderStack` 将 Composer 放到下半操作区。
- 两道折痕的三折叠真机完整展开态(Mate X7 不覆盖此项)。
- 三屏与双屏之间的真实折叠切换连续性。
- 固定来源选择器版本的最终 HAP 在折叠单屏下复验;代码路径仍通过默认关闭的显式宽屏开关隔离。

当前 Mate X7 的 DisplayManagerService 报告 `dms.hidumper.supportdebug false`,因此单屏验收使用设备真实折叠状态完成,没有用显示调试命令模拟合盖。

在上述真机项完成前,不应把三屏手工验收标记为完成。折叠单屏基线矩阵已经完成,但固定来源选择器最终 HAP 仍需一次真实折叠复验后才能关闭回归项。
在上述真机项完成前,不应把三屏手工验收标记为完成。

## 背景

Expand Down Expand Up @@ -78,8 +84,9 @@ HarmonyOS 手机在展开双屏、完整展开三屏或其他宽屏形态下,
- 不改变远程连接、配对、鉴权或断线重连协议。
- 不因为切换会话来源而停止桌面端正在执行的任务。
- 第一阶段不调整 `RemoteCreate` 的页面结构。
- 宽度阈值(720 / 1080)和合盖强制单屏保持不变;展开后是否进入宽屏由纵向折痕与设备类型共同决定。
- 不因为三屏增加一个独立的第三栏、第二套会话列表或新的业务路由模式。
- 宽度阈值对齐官方 md/lg(600 / 840);合盖与悬停强制单屏(悬停走 FolderStack 上下分带)。展开后是否进入宽屏由宽度、纵向折痕和折叠状态决定,不使用 `deviceType`。
- 不因为三屏增加一个独立的第三会话栏、第二套会话列表或新的业务路由模式。文件预览可以占用第三窗格。
- 展开会话不改造成 `FoldSplitContainer` 外壳:该组件只有预设比例,无法对齐实测折痕像素。折叠屏展开继续用折痕几何;悬停用官方 `FolderStack`。

## 当前逻辑

Expand All @@ -89,13 +96,14 @@ HarmonyOS 手机在展开双屏、完整展开三屏或其他宽屏形态下,

```text
设备处于折叠状态 -> 单屏
设备非折叠,宽视口,有可见纵向折痕,且放得下两栏 -> 宽屏(双折/三折展开)
设备非折叠,宽视口,折叠状态为展开/半折,即使折痕缺失 -> 宽屏(双折展开兜底)
设备非折叠,宽视口,无折痕,且设备类型为平板 -> 宽屏
设备处于悬停(HALF_FOLDED) -> 单屏 + FolderStack(上显示 / 下操作)
设备非折叠,宽视口(≥600vp 或 media query),有纵向折痕,且放得下两栏 -> 宽屏
设备非折叠,宽视口,折叠屏展开且折痕缺失 -> 宽屏,合成中线铰链
设备非折叠,宽视口,无折痕 -> 宽屏(断点驱动,含平板)
其他 -> 单屏
```

折叠状态优先于宽度和折痕。因此即使报告的可用宽度较大,只要设备处于折叠状态,也必须继续使用单屏布局。双折叠展开的一道折痕与三折叠的两道折痕使用同一套 master-detail,不新增第三会话栏。
折叠与悬停优先于宽度和折痕。双折叠展开的一道折痕与三折叠的两道折痕使用同一套 master-detail,不新增第三会话栏。

### 路由分流

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import display from '@ohos.display';
import deviceInfo from '@ohos.deviceInfo';
import mediaQuery from '@ohos.mediaquery';
import { RemoteUiState } from '../../services/RemoteUiState';
import { AppShell } from './AppShell';
Expand Down Expand Up @@ -46,14 +45,6 @@ function safeFoldStatus(): display.FoldStatus {
}
}

function safeDeviceType(): string {
try {
return deviceInfo.deviceType || '';
} catch (_err) {
return '';
}
}

@ComponentV2
export struct AppRootPresentation {
@Param shellState: AppShellState = new AppShellState();
Expand All @@ -78,14 +69,17 @@ export struct AppRootPresentation {
@Local restoreCollapsedMasterAfterPreview: boolean = false;
@Local showRemoteViewSettings: boolean = false;
@Local remoteSurfaceState: RemoteSurfaceState = new RemoteSurfaceState();
private readonly deviceType: string = safeDeviceType();
private verticalCreases: ConversationLayoutCrease[] = [];
private wideQueryListener?: mediaQuery.MediaQueryListener;
private foldStatusChanged: (status: display.FoldStatus) => void =
(status: display.FoldStatus): void => {
this.foldStatus = status;
this.refreshWideGeometry();
};
private foldDisplayModeChanged: (mode: display.FoldDisplayMode) => void =
(_mode: display.FoldDisplayMode): void => {
this.refreshWideGeometry();
};
private wideQueryChanged: (result: mediaQuery.MediaQueryResult) => void =
(result: mediaQuery.MediaQueryResult): void => {
this.wideLayoutMatched = result.matches;
Expand Down Expand Up @@ -181,6 +175,7 @@ export struct AppRootPresentation {
// Compact conversations own the drawer, not a back control: Local and
// Remote both open the sidebar over the chat instead of leaving it.
showBackButton: false,
hoverOperateMode: this.isHoverLayout(),
onRestoreSidebar: () => this.restoreWideMasterPane()
})
}
Expand Down Expand Up @@ -271,7 +266,7 @@ export struct AppRootPresentation {
try {
const appMediaQuery = this.getUIContext().getMediaQuery();
this.wideQueryListener = appMediaQuery.matchMediaSync(
`screen and (${ConversationLayoutPolicy.WIDE_LAYOUT_MIN_WIDTH} < width)`
`screen and (${ConversationLayoutPolicy.MD_MIN_WIDTH} < width)`
);
this.wideLayoutMatched = this.wideQueryListener.matches;
this.wideQueryListener.on('change', this.wideQueryChanged);
Expand All @@ -287,6 +282,7 @@ export struct AppRootPresentation {
this.foldStatus = display.getFoldStatus();
this.refreshWideGeometry();
display.on('foldStatusChange', this.foldStatusChanged);
display.on('foldDisplayModeChange', this.foldDisplayModeChanged);
} catch (_err) {
this.foldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN;
this.unbindFoldStatus();
Expand All @@ -296,6 +292,7 @@ export struct AppRootPresentation {
private unbindFoldStatus(): void {
try {
display.off('foldStatusChange', this.foldStatusChanged);
display.off('foldDisplayModeChange', this.foldDisplayModeChanged);
} catch (_err) {
}
}
Expand All @@ -319,9 +316,9 @@ export struct AppRootPresentation {
this.viewportWidth,
this.wideLayoutMatched,
this.foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED,
this.deviceType,
verticalCreases,
this.isExpandedFoldable()
this.isExpandedFoldable(),
this.isHoverLayout()
);
const geometry = ConversationLayoutPolicy.resolveWideGeometry(
this.viewportWidth,
Expand All @@ -339,15 +336,36 @@ export struct AppRootPresentation {
}
}

private isHoverLayout(): boolean {
return this.foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED;
}

private isExpandedFoldable(): boolean {
return this.foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ||
this.foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED;
return this.foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED && this.isFoldableDevice();
}

private isFoldableDevice(): boolean {
try {
return display.isFoldable();
} catch (_err) {
return this.foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ||
this.foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED ||
this.foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED;
}
}

private currentVerticalCreases(): ConversationLayoutCrease[] {
if (this.foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED) {
if (this.foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED || this.isHoverLayout()) {
return [];
}
return ConversationLayoutPolicy.effectiveVerticalCreases(
this.viewportWidth,
this.reportedVerticalCreases(),
this.isExpandedFoldable()
);
}

private reportedVerticalCreases(): ConversationLayoutCrease[] {
try {
const region = display.getCurrentFoldCreaseRegion();
const creaseRects = region.creaseRects;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppShellState } from '../state/AppShellState';
import { ConversationLayoutPolicy } from '../policy/ConversationLayoutPolicy';
import { PAGE_BG } from './Theme';

const WIDE_SETTINGS_SHEET_MAX_WIDTH: number = 680;
Expand Down Expand Up @@ -133,7 +134,7 @@ export struct AppShell {
if (this.shellWidth <= 0) {
return 280;
}
if (this.shellWidth < 720) {
if (this.shellWidth < ConversationLayoutPolicy.MD_MIN_WIDTH) {
return Math.min(280, Math.max(220, Math.round(this.shellWidth * 0.68)));
}
return Math.min(420, Math.max(280, Math.round(this.shellWidth * 0.35)));
Expand Down
Loading