
普通内容应用做横竖屏适配往往是列表从一列变两列编辑器更难因为画布、工具栏、图层、调色板、时间轴、历史记录和弹窗都在争夺同一块屏幕。把竖屏尺寸按比例缩小后塞进横屏不会得到更紧凑的编辑器只会得到一组文字太小、按钮难点、面板互相遮挡的控件。bitArt 的处理思路是先保证画布和主要操作再根据窗口宽高选择手机竖屏、手机横屏、平板和二合一工作台。EntryAbility.ets负责全屏窗口与顶部避让区EditorLayoutService.ets集中判断布局档位和面板尺寸工作区组件只消费计算结果PixelCanvasView.ets则让画布在尺寸变化后重新计算基准比例、缩放和平移边界。全屏不是忽略系统栏像素编辑器希望尽量多留空间给画布因此窗口开启全屏布局并把状态栏与导航栏颜色设成工作台深色。全屏布局只表示内容可以延伸到系统栏区域不表示顶部控件可以直接贴到屏幕物理边缘。EntryAbility分别读取系统避让区和挖孔避让区取两者顶部高度的最大值再写入AppStorage。当avoidAreaChange发生时重新计算旋转或窗口形态变化后也能更新。private updateTopSafeArea(mainWindow: window.Window): void { let topSafeAreaPx 0; const systemArea mainWindow.getWindowAvoidArea( window.AvoidAreaType.TYPE_SYSTEM); topSafeAreaPx Math.max( topSafeAreaPx, systemArea.topRect.height); const cutoutArea mainWindow.getWindowAvoidArea( window.AvoidAreaType.TYPE_CUTOUT); topSafeAreaPx Math.max( topSafeAreaPx, cutoutArea.topRect.height); AppStorage.setOrCreatenumber( editorTopSafeAreaPx, topSafeAreaPx); }工作区再按当前显示密度把像素值换算为 vp作为顶部工具栏和浮层的起点。这里不能把某个设备实测的 24 或 32 直接写死因为刘海、挖孔、状态栏高度和窗口模式都可能不同。布局判断要看窗口不只看设备类型平板可以分屏二合一设备也可以把窗口缩小。如果只根据deviceType决定桌面布局小窗口里仍会出现三栏工作台。EditorLayoutService直接使用viewportWidth和viewportHeight宽度至少 720 且高度至少 560 进入大屏布局宽度至少 960 进入桌面档桌面档且高度达到 700 时启用统一右侧面板。static isLargeScreenLayout(state: EditorLayoutState): boolean { return state.viewportWidth 720 state.viewportHeight 560; } static isDesktopLayout(state: EditorLayoutState): boolean { return state.viewportWidth 960 state.viewportHeight 560; } static isUnifiedSidePanelLayout(state: EditorLayoutState): boolean { return EditorLayoutService.isDesktopLayout(state) state.viewportHeight 700; }横屏判断也不只是width height。bitArt 要求宽度比高度多出 40vp或者已经满足大屏工作台条件。这个小缓冲可以减少接近方形窗口在轻微变化时反复切换布局。手机竖屏先保画布再让面板按需展开竖屏空间最紧不能同时常驻图层、调色板和时间轴。bitArt 把这些区域做成可开合面板紧凑状态只露出必要入口时间轴展开后占用固定高度帧列表横向滚动图层和调色板根据当前操作互相收纳。画布区域使用剩余空间而不是固定高度。顶部栏、底部工具和展开面板先占据可预测尺寸中间画布通过layoutWeight(1)获取剩余区域并配合constraintSize({ minWidth: 0, minHeight: 0 })防止子内容反向撑大父容器。Column() { this.TopBar() Row() { this.ToolRail() this.CanvasPanel() .layoutWeight(1) .constraintSize({ minWidth: 0, minHeight: 0 }) } if (this.timelineOpen) { this.TimelinePanel(expanded) } }手机竖屏不是把所有功能隐藏而是把低频内容延后到明确入口。设置、项目管理和素材库以浮层出现关闭后立即把空间还给画布。横屏要把高度问题交给滚动容器横屏看起来更宽实际高度更少。若侧栏把图层、调色板和帧列表纵向堆满底部播放和新增按钮最容易被裁掉。bitArt 在横屏使用窄侧栏图层和调色板内容可滚动帧卡片放入两列网格新增、删除、播放和展开操作固定在面板底部。Scroll() { Grid() { ForEach(this.animationFrames, (item: EditorFrame) { GridItem() { this.LandscapeFrameCard(item) } }) } .columnsTemplate(1fr 1fr) .columnsGap(6) .rowsGap(6) } .layoutWeight(1) .scrollBar(BarState.Auto) this.LandscapeFrameDurationControl() this.FixedTimelineActions()关键点是滚动区域必须有可计算高度。只给内部Column加Scroll外层却没有高度或权重约束滚动容器会按内容无限展开结果仍然溢出。大屏工作台不是放大的手机界面达到大屏阈值后bitArt 采用左侧工具、中间画布、右侧面板的三区域工作台。右侧可以常驻图层、调色板和时间轴用户不必频繁开关面板。桌面宽度达到 1280 时统一侧栏从 236 增到 252普通大屏档则使用 188 或 220 的侧栏宽度。这不是简单增大控件。画布仍然是视觉中心侧栏增加的是信息密度和并行可见性。设置面板、项目管理和素材库在大屏上也使用更宽的内容区允许两列或更多信息但高度仍受当前窗口限制。键盘快捷键只在大屏或键盘友好布局中开放。手机界面不显示一整套快捷键设置避免为不存在的输入设备占用空间。二合一设备的鼠标滚轮则直接作用于画布缩放。浮层尺寸必须同时受宽度和高度约束设置面板在竖屏可用 340×520横屏可能更宽却只能更矮桌面面板希望达到 720 宽但窗口分屏后不能越界。EditorLayoutService集中计算可用尺寸宽度扣除两侧安全保护和边缘间距高度扣除顶部偏移、底部保护和边缘间距再在首选值与最小值之间裁剪。static getFloatingPanelHeight(state: EditorLayoutState, preferredHeight: number, minHeight: number, topOffset: number 0): number { const availableHeight state.viewportHeight - topOffset - EditorLayoutService.BOTTOM_GUARD_VP - 8; if (availableHeight minHeight) { return Math.max(1, availableHeight); } return Math.min(preferredHeight, availableHeight); }面板内部仍要可滚动因为尺寸计算只能保证外框不越界不能保证所有设置项一次显示完。底部保护值还能让主要按钮避开圆角和手势区域。画布尺寸变化后要重算基准比例工作区从竖屏切到横屏画布组件的可用宽高会变化。PixelCanvasView在区域变化时更新viewportWidth与viewportHeight通过EditorViewportService.getCanvasBaseScale重新计算画布基础宽高然后重新夹紧平移量并安排完整重绘。private updateCanvasSide(width: number, height: number): void { this.viewportWidth Math.max(1, width); this.viewportHeight Math.max(1, height); const baseScale EditorViewportService.getCanvasBaseScale( width, height, this.canvasWidth, this.canvasHeight); this.canvasBaseWidth Math.max(1, this.canvasWidth * baseScale); this.canvasBaseHeight Math.max(1, this.canvasHeight * baseScale); this.updatePan(this.panOffsetX, this.panOffsetY); this.scheduleCanvasRedraw(true); }非方形画布尤其需要同时使用宽度和高度。如果仍按单一canvasSize计算32×64 的画布会被当成正方形触控映射和视觉位置都会偏移。平移边界只在画布超出视口时开放缩放后的画布小于视口时不应该允许拖到角落大于视口时又要允许查看被裁切部分。updatePan把每个轴分别交给EditorViewportService.clampPanOffset依据视口和实际绘制尺寸计算范围。鼠标滚轮缩放还要围绕光标位置。bitArt 先计算光标在旧画布中的相对锚点再算新尺寸下需要的偏移使光标指向的像素尽量留在原位。const anchorX (x - previousLeft) / previousDrawWidth; const anchorY (y - previousTop) / previousDrawHeight; this.zoomScale EditorViewportService.clampZoom(zoom); const nextPanOffsetX x - (this.viewportWidth - nextDrawWidth) / 2 - anchorX * nextDrawWidth; const nextPanOffsetY y - (this.viewportHeight - nextDrawHeight) / 2 - anchorY * nextDrawHeight; this.updatePan(nextPanOffsetX, nextPanOffsetY);双指缩放、双指平移、移动工具和临时空格平移都复用同一组缩放与边界函数避免不同输入方式把画布推到不同的非法位置。性能适配也是布局适配的一部分横屏和大屏能显示更多面板也意味着更多缩略图、洋葱皮和覆盖层同时存在。如果每次画笔移动都让全部区域重新合成设备越大反而越容易闪烁。bitArt 把移动触摸按约 16 毫秒合并画布重绘也用定时器合并有有效脏区域时只更新局部否则才完整重绘。private scheduleCanvasRedraw(fullRedraw: boolean): void { this.pendingFullRedraw this.pendingFullRedraw || fullRedraw; if (this.redrawTimerId 0) { return; } this.redrawTimerId setTimeout(() { this.redrawTimerId -1; this.captureDirtyBounds(true); if (this.pendingFullRedraw || !this.drawDirtyCanvas()) { this.drawCanvas(); } this.pendingFullRedraw false; }, 16); }正在绘制时暂时跳过像素溢色和洋葱皮等昂贵效果笔触结束再恢复。面板开合、窗口旋转和缩放属于几何变化需要完整重绘单个像素变化则优先走脏区域。这种区分比单纯降低画质更能保持编辑手感。适配检查要覆盖窗口组合只在一台手机竖屏看起来正常还不够。至少要检查手机竖屏、手机横屏、平板全屏、平板分屏、二合一宽窗口和接近方形的小窗口。每种组合都要观察顶部栏是否避开挖孔、底部按钮是否高于手势区、画布是否保持完整比例、侧栏能否滚到底、弹窗是否越界、长项目名是否截断以及旋转后缩放和平移是否仍在合理范围。交互检查也要跟着布局走横屏增加到十帧确认播放按钮仍可达图层增加到二十层确认列表滚动而不是挤压画布打开颜色选择器和设置面板确认确认按钮没有落到屏外大屏使用鼠标滚轮围绕光标缩放退到小窗口后确认快捷键设置不会占据手机布局。bitArt 在module.json5中声明phone、tablet和2in1并允许auto_rotation。这只是系统层面的入口真正的适配仍由窗口数据、布局档位、滚动边界和画布几何共同完成。复杂编辑器的响应式设计重点不是让每个控件在所有尺寸中同时出现而是决定每个尺寸下哪些信息常驻、哪些按需展开、哪些必须滚动以及画布如何拿到稳定的剩余空间。安全区解决物理遮挡窗口阈值解决工作台形态面板约束解决内容溢出画布边界和局部重绘则保证旋转之后仍然好用。做到这些手机上的紧凑编辑和大屏上的并行工作才是同一个产品的两种可靠形态。