你好欢迎来到我的博客我是【菜鸟学鸿蒙】我是一名在路上的移动端开发者正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来也为了和更多同路人互相启发我决定把探索 HarmonyOS 的过程都记录在这里。️主要方向ArkTS 语言基础、HarmonyOS 原生应用Stage 模型、UIAbility/ServiceAbility、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战以及 Android → 鸿蒙的迁移踩坑与复盘。内容节奏从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘让每篇都有可落地的代码与方法论。 我相信写作是把知识内化的过程分享是让生态更繁荣的方式。如果你也想拥抱鸿蒙、热爱成长欢迎关注我一起交流进步前言做图文阅读、视频播放或图片浏览这类页面时横竖屏切换几乎是绕不开的问题。旋转设备后封面图、操作按钮、进度条的位置突然错乱或者内容区宽度没有随窗口变化——这类问题的根源通常不是 ArkUI 的布局 bug而是开发者对窗口尺寸变化时页面到底发生了什么这件事理解得不够准确。这篇文章用一个最小化的图文详情页走完横竖屏切换、全屏进入和退出、业务状态保持这一整条路。一、横竖屏变化本质上改变了什么很多开发者第一反应是屏幕方向变了但更准确的描述是页面可用窗口的宽高比发生了变化。这里有几件事需要明确区分屏幕Display和窗口Window不是同一个概念。ohos.display描述的是物理屏幕的属性ohos.window管理的是应用窗口。横竖屏切换时真正影响布局的是window.WindowProperties中的windowRect也就是当前应用窗口的实际矩形区域而不是屏幕的物理尺寸。在分屏模式下两者的差距会更加明显。HarmonyOS Stage 模型下UIAbility 默认不会因为旋转而重建。与某些 Android 早期版本不同ArkUI 框架会在窗口尺寸变化时触发布局重算但UIAbility生命周期和页面状态变量默认会保留。这是好事但也意味着如果开发者把控件尺寸硬编码为固定数值布局重算时这些固定值不会自动更新就会出现错位。全屏和横屏是两个独立的概念。横屏是指窗口宽度大于高度全屏是指应用窗口扩展到整个屏幕区域通常伴随状态栏和导航栏的隐藏。两者可以同时存在也可以只存在其中一个。一个常见的理解误区是把进入横屏等同于进入全屏在具体接口调用上这会导致行为不一致。二、为什么不能只维护两套固定尺寸有的做法是在竖屏状态写一套固定宽高在横屏状态写另一套固定宽高靠判断方向切换。这种方式在手机单机场景可能暂时奏效但有几个明显问题平板、折叠屏、分屏模式下同样是横屏窗口宽度可能差距悬殊分屏模式下用户可以拖动分割线窗口宽度是连续变化的没有固定值HarmonyOS 官方推荐的做法是使用响应式布局让组件尺寸基于当前窗口的实际宽度计算而不是依赖预设断点对应的固定像素值。官方文档中的响应式布局体系基于**断点Breakpoint**概念将窗口宽度划分为若干区间在不同区间内选用不同的布局策略。断点以 vp 为单位因此与屏幕像素密度无关。三、搭建最小示例一个图文详情页下面这个示例模拟一个阅读类 App 的文章详情页包含顶部标题区封面图正文内容区底部操作栏点赞、收藏、评论目标效果竖屏标题、封面图、正文上下堆叠底部操作栏固定横屏封面图和正文左右分栏扩大内容区全屏隐藏顶部标题区和底部操作栏只保留内容和退出按钮。先看整体状态结构这里用State管理三个关键变量// 窗口当前宽度vpStatewindowWidth:number0// 窗口当前高度vpStatewindowHeight:number0// 是否处于全屏状态StateisFullScreen:booleanfalsewindowWidth和windowHeight直接驱动布局不另外维护一个方向枚举——宽大于高就是横向布局高大于宽就是纵向布局这样分屏场景也能自然适配。四、核心代码实现4.1 监听窗口尺寸变化在onWindowStageCreate中获取主窗口并注册on(windowSizeChange)监听。根据官方 API ReferenceWindow类提供on(type: windowSizeChange, callback: Callbackwindow.Size): void从 API 9 起支持。// EntryAbility.tsimport{UIAbility,Want,AbilityConstant}fromkit.AbilityKit;import{window}fromkit.ArkUI;importhilogfromohos.hilog;exportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent(pages/ArticleDetailPage,(err){if(err.code){hilog.error(0x0000,Ability,loadContent failed: %{public}s,JSON.stringify(err));return;}});// 获取主窗口注册窗口尺寸变化监听windowStage.getMainWindow((err,windowObj){if(err.code){return;}windowObj.on(windowSizeChange,(size:window.Size){// size.width 和 size.height 单位为 px需转换为 vp// 使用 AppStorage 在 Ability 与页面之间共享状态constdensitywindowObj.getWindowProperties().density;AppStorage.setOrCreatenumber(windowWidth,size.width/density);AppStorage.setOrCreatenumber(windowHeight,size.height/density);});// 页面初始化时同步一次当前尺寸constpropswindowObj.getWindowProperties();constdensityprops.density;AppStorage.setOrCreatenumber(windowWidth,props.windowRect.width/density);AppStorage.setOrCreatenumber(windowHeight,props.windowRect.height/density);});}}window.Size中width和height的单位是px物理像素而 ArkUI 布局使用vp虚拟像素必须除以屏幕密度density转换否则在高分辨率屏幕上数值会偏大直接传入会导致控件尺寸不正确。getWindowProperties()返回WindowProperties其中windowRect描述窗口在屏幕坐标系中的矩形density是当前屏幕的像素密度比例。4.2 页面内响应窗口尺寸页面通过StorageProp绑定AppStorage中的数据这样 Ability 侧更新后页面会自动触发重新布局。// ArticleDetailPage.etsimport{window}fromkit.ArkUI;EntryComponentstruct ArticleDetailPage{StorageProp(windowWidth)windowWidth:number360StorageProp(windowHeight)windowHeight:number780StateisFullScreen:booleanfalse// 判断当前是否为横向布局宽度大于高度getisLandscape():boolean{returnthis.windowWidththis.windowHeight;}build(){Stack({alignContent:Alignment.TopStart}){if(this.isLandscape){// 横屏左右分栏this.LandscapeLayout()}else{// 竖屏上下堆叠this.PortraitLayout()}// 全屏时覆盖一个退出按钮if(this.isFullScreen){Button(退出全屏).position({x:16,y:16}).onClick((){this.exitFullScreen();})}}.width(100%).height(100%)}BuilderPortraitLayout(){Column(){// 顶部标题区if(!this.isFullScreen){Row(){Text(文章标题示例).fontSize(18).fontWeight(FontWeight.Bold)}.width(100%).height(56).padding({left:16,right:16}).backgroundColor(#FFFFFF)}// 封面图区域Image($r(app.media.cover)).width(100%).aspectRatio(16/9).objectFit(ImageFit.Cover).onClick((){if(!this.isFullScreen)this.enterFullScreen();})// 正文内容区竖屏占满剩余高度Scroll(){Text(这里是文章正文内容模拟长文本段落……).fontSize(16).lineHeight(26).padding(16)}.layoutWeight(1)// 底部操作栏if(!this.isFullScreen){Row({space:32}){Button(点赞).type(ButtonType.Normal)Button(收藏).type(ButtonType.Normal)Button(评论).type(ButtonType.Normal)}.width(100%).height(56).justifyContent(FlexAlign.Center).backgroundColor(#F5F5F5)}}.width(100%).height(100%)}BuilderLandscapeLayout(){Column(){// 横屏下隐藏顶部标题或保留视产品需求if(!this.isFullScreen){Row(){Text(文章标题示例).fontSize(16).fontWeight(FontWeight.Bold)}.width(100%).height(48).padding({left:16,right:16}).backgroundColor(#FFFFFF)}// 横屏主体封面图 正文左右分栏Row(){// 左侧封面图宽度约占 45%Image($r(app.media.cover)).width(45%).height(100%).objectFit(ImageFit.Cover).onClick((){if(!this.isFullScreen)this.enterFullScreen();})// 右侧正文占剩余宽度Scroll(){Text(这里是文章正文内容模拟长文本段落……).fontSize(15).lineHeight(24).padding(16)}.layoutWeight(1).height(100%)}.layoutWeight(1)// 横屏底部操作栏同样可以保留或隐藏if(!this.isFullScreen){Row({space:32}){Button(点赞).type(ButtonType.Normal)Button(收藏).type(ButtonType.Normal)Button(评论).type(ButtonType.Normal)}.width(100%).height(48).justifyContent(FlexAlign.Center).backgroundColor(#F5F5F5)}}.width(100%).height(100%)}// 进入全屏设置沉浸式隐藏系统栏enterFullScreen(){constctxgetContext(this)ascommon.UIAbilityContext;window.getLastWindow(ctx).then((windowObj){// 设置沉浸式布局状态栏区域归属页面windowObj.setWindowLayoutFullScreen(true).then((){// 隐藏状态栏和导航栏windowObj.setWindowSystemBarEnable([]).then((){this.isFullScreentrue;});});});}// 退出全屏恢复系统栏exitFullScreen(){constctxgetContext(this)ascommon.UIAbilityContext;window.getLastWindow(ctx).then((windowObj){windowObj.setWindowSystemBarEnable([status,navigation]).then((){windowObj.setWindowLayoutFullScreen(false).then((){this.isFullScreenfalse;});});});}}真正需要关注的几个地方layoutWeight(1)是让子组件按比例分配剩余空间的关键在竖屏的Column和横屏的Row中分别使用避免把高度或宽度写死setWindowLayoutFullScreen和setWindowSystemBarEnable都是异步 Promise需要链式调用不能只调其中一个setWindowSystemBarEnable([])传入空数组表示隐藏所有系统栏恢复时传[status, navigation]。4.3 切换后恢复业务状态由于 HarmonyOS Stage 模型下 UIAbility 不会因旋转重建State中保存的数据例如文章阅读进度、点赞状态在旋转后自然保留。但有一个容易忽略的情况Scroll 组件的滚动偏移量。如果竖屏下用户已经滚动到正文中间横屏后Scroll是一个新渲染的实例因为横屏和竖屏分别是不同的Builder分支之前的偏移量不会自动传递过去。可以用Scroller控制器保存当前偏移量在布局切换时恢复// 在组件级别持有一个 Scroller 对象privateportraitScroller:ScrollernewScroller();privatelandscapeScroller:ScrollernewScroller();// 保存最近一次的滚动偏移pxStatesavedScrollOffset:number0;// 在 Scroll 组件上绑定 scroller 和 onScroll 回调Scroll(this.portraitScroller){// ...内容}.onScroll((xOffset:number,yOffset:number){// 竖屏滚动时记录当前偏移this.savedScrollOffsetthis.portraitScroller.currentOffset().yOffset;})切换到横屏布局时在aboutToAppear或通过Watch监听isLandscape变化后用scrollTo恢复到对应位置。这里需要注意竖屏和横屏下内容区可用宽度不同文本排版后行数不同相同的yOffset不代表相同的阅读位置。更精准的方案是记录当前可见的第一个段落 ID而不是原始 px 偏移量但这属于业务层实现不在本文范围内展开。五、几个关键点拆开看5.1on(windowSizeChange)和onWindowStageEvent是两件不同的事onWindowStageEvent监听窗口的前后台切换SHOWN、HIDDEN、ACTIVE、INACTIVE并不会在旋转时触发。如果只监听onWindowStageEvent是感知不到横竖屏变化的。on(windowSizeChange)才是响应窗口尺寸变化包括旋转、分屏拖拽的正确入口。5.2 设置屏幕方向偏好如果需要锁定某个页面的方向可以调用Window.setPreferredOrientationAPI 9 起支持import{window}fromkit.ArkUI;// 锁定为竖屏windowObj.setPreferredOrientation(window.Orientation.PORTRAIT);// 锁定为横屏windowObj.setPreferredOrientation(window.Orientation.LANDSCAPE);// 跟随系统传感器旋转windowObj.setPreferredOrientation(window.Orientation.AUTO_ROTATION);window.Orientation枚举值包括UNSPECIFIED、PORTRAIT、LANDSCAPE、PORTRAIT_INVERTED、LANDSCAPE_INVERTED、AUTO_ROTATION等。注意这个设置作用于窗口不是全局生效。如果需要从一个页面进入某个横屏子页面后再返回竖屏需要在对应页面的aboutToAppear和aboutToDisappear分别设置和还原。5.3windowRect里的 px 与布局 vp 的转换这是一个非常容易出错的地方。WindowProperties.windowRect中width和height是像素单位。ArkUI 所有尺寸属性接受的是 vp。不做转换直接用来驱动布局在 2× 或 3× 屏幕上会得到两三倍于预期的尺寸。window.Size中的数值同理on(windowSizeChange)回调里拿到的也是 px。density可以从WindowProperties.density获取。5.4 安全区域与全屏的关系进入全屏后导航栏和状态栏的区域会被应用内容覆盖但系统手势区域仍然存在。如果底部操作栏在全屏模式下仍然保留需要通过expandSafeArea或配合padding处理底部安全区域否则控件可能落在手势区域内点击体验会有问题。六、容易踩的坑问题一布局里写了固定width数值一旦某个容器设置了如width(375)旋转后这个数值不会变内容区会溢出或留白。应改为width(100%)或借助layoutWeight。问题二只在aboutToAppear里读一次窗口尺寸如果只在页面初始化时读一次windowRect就不再更新旋转后尺寸状态还是旧值布局不会响应。必须通过on(windowSizeChange)持续更新。问题三setWindowLayoutFullScreen和setWindowSystemBarEnable顺序颠倒这两个接口是异步操作如果只调setWindowSystemBarEnable([])而不调setWindowLayoutFullScreen(true)系统栏区域会留白而不是被内容覆盖反之只调setWindowLayoutFullScreen(true)而不隐藏系统栏系统栏还在内容会被遮住。两者需要配合使用。问题四横竖屏分支用了不同的状态变量实例上面示例中isFullScreen是共享的State横屏和竖屏分支共用同一个变量。如果不小心在Builder内部引入了局部变量来控制显隐切换布局后局部变量会被重置造成全屏状态丢失。七、排查顺序旋转后控件位置异常建议按这个顺序排查确认on(windowSizeChange)有正确注册且回调中的状态更新路径确实触发了 ArkUI 的重新布局检查State或StorageProp是否被正确更新检查所有容器的宽高设置找出硬编码的固定尺寸改为相对值或layoutWeight确认 px 到 vp 的转换是否正确尤其是直接使用windowRect.width驱动布局的地方检查横竖屏切换时的 Builder 分支确认共享状态变量没有在分支内被局部变量覆盖如果涉及全屏检查setWindowLayoutFullScreen和setWindowSystemBarEnable是否都已调用且顺序正确分屏场景验证在支持分屏的设备上拖动分割线观察布局是否能平滑跟随宽度变化而不是只在两个固定值之间跳变。开发经验总结横竖屏切换的本质是窗口尺寸变化正确的感知入口是Window.on(windowSizeChange)而不是生命周期回调窗口尺寸数据单位是 pxArkUI 布局单位是 vp务必通过density换算布局驱动变量应来自实时的窗口宽度而非硬编码的横竖屏枚举这样分屏场景也能自然适配全屏需要setWindowLayoutFullScreen与setWindowSystemBarEnable配合缺一会有视觉残留Stage 模型下旋转不重建 AbilityState状态天然保留但Scroll等有内部偏移状态的组件需要手动保存与恢复。如果你正在做类似页面可以重点检查一下当窗口宽度连续变化如分屏拖拽时你的布局是逐步平滑适应还是只在某个临界点做了一次跳变——这个细节往往比单纯处理横竖屏更能反映布局方案是否健壮。 写在最后如果你觉得这篇文章对你有帮助或者有任何想法、建议欢迎在评论区留言交流你的每一个点赞 、收藏 ⭐、关注 ❤️都是我持续更新的最大动力我是一个在代码世界里不断摸索的小码农愿我们都能在成长的路上越走越远越学越强感谢你的阅读我们下篇文章再见✍️ 作者菜鸟不学编程 本文原创转载请注明出处。
阅读完成 · 觉得有帮助?