十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android页面级屏幕旋转控制:实现指定Fragment横屏,其他页面保持竖屏的完整方案

Android页面级屏幕旋转控制:实现指定Fragment横屏,其他页面保持竖屏的完整方案 在移动端应用开发里横竖屏旋转这个需求看起来是个小功能真做起来坑不少。尤其是“只对某几个页面支持旋转其他页面锁死竖屏”这种需求几乎每个工具类、视频类、阅读类App都会遇到。很多人一开始直接去改全局配置结果要么所有页面都转了要么转完之后页面状态全乱了要么在平板上出现布局错乱各种莫名其妙的问题接踵而至。我前阵子刚好在一个资讯类App里完整处理过这个需求涉及页面级屏幕旋转控制、状态保存恢复、特定页面锁定横屏、以及跨页面跳转时的方向平滑过渡。这中间踩了不少坑也沉淀了一套比较稳定的方案。这篇就把完整思路和可复现的代码逻辑梳理一遍偏实战尽量少讲虚的。1. 需求解析为什么“只让部分页面旋转”会这么难先说清楚这个需求的实际场景不是所有App都适合全局横竖屏自由切换。比如电商、社交、工具类应用核心操作路径都是竖屏设计的一旦全局放开旋转用户在浏览商品、刷信息流或填表单时设备一歪界面突然重排轻则视觉跳跃重则误触、卡顿。但某些页面确实需要横屏体验典型的就是视频播放页、图片预览页、图表数据页、横版表单编辑页。于是“部分页面支持旋转大部分页面锁定竖屏”就成了刚需。这个需求的难点在于Android的屏幕方向控制系统层面是Activity级别的不是View级别的。而绝大多数应用是单Activity多Fragment架构尤其是用Jetpack Navigation的即使不是单Activity架构很多页面复用了同一个Activity承载只是通过Fragment切换内容。这就导致一个问题如果直接给承载Activity加了android:screenOrientationfullSensor那所有挂在它下面的Fragment都会跟着转完全无法做到“只让其中一两个Fragment旋转”。另外还有一层麻烦即使你通过运行时设置requestedOrientation实现了页面级控制但在页面切换、对话框弹出、WebView加载等边界场景下方向变化时Activity会经历销毁重建Fragment状态、滚动位置、输入框内容全部需要手动兜住。如果初始化就做不对后面全是补丁。所以这个题本质上不是“怎么让页面转一下”而是“怎么让同一个宿主Activity内部的多个页面各自拥有独立的方向策略并且切换时不出乱子”。想明白这点方案就好办了。2. 方案盘点全局配置、动态切换、按需锁定的取舍处理这类需求业内大致有三条路每条都有自己的适用边界我挨个说清楚方便你对号入座。2.1 方案一为不同页面拆多个Activity各自配置方向这是最朴素、最稳妥的做法。竖屏页面放Activity A配置screenOrientationportrait横屏页面放Activity B配置screenOrientationsensorLandscape或userLandscape。跳转时用Intent切换方向由系统自动处理。这也是很多视频类App的经典方案播放器单独一个Activity天然横屏。优点显而易见方向控制是系统级的稳定可靠状态保存也相对独立不会互相污染。缺点是如果应用里需要旋转的页面很多或者横竖屏切换是同一个页面内的动态交互比如用户点个按钮从竖屏切到横屏拆Activity会显得笨重页面间传值、共享ViewModel、转场动画都要额外处理代码量膨胀。而且在Fragment已经是主流的今天为了方向控制强行拆Activity有点开倒车。2.2 方案二宿主Activity动态切换requestedOrientation这是目前单Activity架构下的主流方案。宿主Activity的screenOrientation不用写死在页面切换时根据当前页面的需求动态调用setRequestedOrientation()改变方向。比如进入视频页时通过源码反射或接口回调在onResume里把方向切到横屏离开视频页时再切回竖屏。这个方案能做到“页面级”控制也不破坏Fragment架构。但坑很多最典型的一个是setRequestedOrientation()是会触发Activity重新走一遍onPause、onDestroy、onCreate的configChange没配对应screenOrientation——如果你的Fragment依赖Activity的实例状态比如在onCreate里通过findViewById拿View引用重建后这些引用全部失效页面就白了、闪了、或者回到栈顶了。所以这个方案的核心工程点不是“调用一下API”而是“怎么处理重建期间的状态和页面栈”。后面我会重点讲这个。2.3 方案三在Manifest里锁定方向运行时手动旋转View一些特殊场景会用这个思路比如游戏、播放器内部自绘的渲染层。宿主Activity始终保持竖屏不触发系统级旋转页面内部通过自定义View的rotation属性、Matrix变换等方式将UI内容、触摸坐标整体旋转90度实现视觉上的横屏。这个方法可以完全绕开Activity重建问题性能也好但工程成本很高触摸坐标映射、尺寸适配、软键盘弹出位置、系统对话框都需要手工处理非必要不建议用。适合那种页面极少、且内部是自绘Canvas或OpenGL渲染的场景。综合来看日常业务需求方案二是最合理的平衡点付出的工程代价可控又能满足“页面级方向自适应”。下面我重点展开方案二把关键细节和坑全部剖开。3. 页面级方向控制的核心实现base配置与动态切换3.1 Manifest清单里的正确姿势在动态切换方案里Manifest文件不用把screenOrientation写死但也不要完全不写。我建议把宿主Activity的screenOrientation设为portrait作为兜底这样冷启动、异常退出恢复、第三方页面跳转回来都默认是竖屏不会闪一下奇怪的横屏再切回来。activity android:name.MainActivity android:screenOrientationportrait android:configChangesorientation|screenSize|keyboardHidden|smallestScreenSize android:windowSoftInputModestateHidden|adjustResize /activity这里有一个非常关键的细节configChanges里建议加上orientation|screenSize。很多人会问既然我用setRequestedOrientation触发方向变化不就是要让系统重建Activity吗加上configChanges之后Activity不会重建那方向变化怎么处理答案是configChanges的存在是让你在“需要重建的场景”和“不需要重建的场景”之间有一个取舍空间。实际业务里方向切换时如果页面结构简单、状态少你当然希望Activity不重建只通过onConfigurationChanged回调手动刷新布局避免闪烁和状态丢失。如果页面结构复杂、状态分散反而希望重建一次让所有组件统一走onSaveInstanceState和restoreInstanceState的沉淀流程不容易漏。所以关键不在于硬编码“一定重建”或“一定不重建”而在于页面有没有为重建做好准备。我的实践是宿主Activity不配orientation|screenSize的configChanges让系统在方向切换时重建Activity但所有Fragment必须老老实实做好状态保存。原因后面说。3.2 动态切换的APIsetRequestedOrientation的用法和时机核心API就是Activity.setRequestedOrientation(int)参数用ActivityInfo.SCREEN_ORIENTATION_PORTRAIT、SCREEN_ORIENTATION_SENSOR_LANDSCAPE或SCREEN_ORIENTATION_LANDSCAPE。单Activity架构下通常做法是在Fragment的onResume或者onHiddenChanged里判断当前Fragment是否允许旋转然后调用宿主Activity的requestedOrientation。本着“页面自己管自己的方向”的原则我会在BaseFragment里封装一个方法public abstract class BaseFragment extends Fragment { // 子类重写该方法默认竖屏 protected boolean isSupportOrientation() { return false; } Override public void onResume() { super.onResume(); updateOrientation(); } private void updateOrientation() { Activity activity getActivity(); if (activity null) return; int target isSupportOrientation() ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; // 当前方向已经和目标方向一致时不重复设置 if (activity.getRequestedOrientation() ! target) { activity.setRequestedOrientation(target); } } }在需要旋转的页面里只需要重写isSupportOrientation()返回true页面进入时系统就会自动切到横屏页面离开时自动切回竖屏。等等这里有个大坑如果你在Fragment A竖屏跳转到Fragment B横屏再按返回键回Fragment A此时A的onResume时机和系统方向动画之间是有时间差的。实测中setRequestedOrientation是异步生效的你返回竖屏页面时可能页面先以横屏渲染了一帧然后再转回竖屏造成肉眼可见的闪屏。我的解决办法是在onHiddenChanged里同样调用一次updateOrientation()并且在onPause时根据“即将不可见”的时机提前把方向切回默认值让方向变化和页面切换动画同时发生视觉上就不会有割裂感。具体代码Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (!hidden) { updateOrientation(); } else { Activity activity getActivity(); if (activity ! null isSupportOrientation()) { // 离开旋转页面前先切回竖屏避免下一个页面闪现横屏 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } }当然如果目标页是视频播放页这种希望返回时仍然停留在横屏就不要在onPause里切回竖屏。是否提前锁回竖屏取决于业务如果下一个页面是竖屏就切如果下一个页面也是横屏或者要回到一个全屏的容器就不要切。这个逻辑我建议做成配置而不是写死在基类里。3.3 页面状态保存方向切换导致Activity重建的兜底方案前面说了我没在configChanges里加orientation|screenSize所以方向切换时Activity会销毁重建。这就逼着我把状态保存做实否则页面切方向肯定丢状态。对于Fragment状态核心是两处代码其一在Activity的onSaveInstanceState里官方推荐让FragmentManager自己保存状态但有些版本存在Fragment状态没保存完就触发重建的情况报Can not perform this action after onSaveInstanceState异常。规避手段是所有add、replace、hide、show操作必须在onSaveInstanceState之前完成不要在onResume之后还异步去操作Fragment事务。其二Fragment内部的数据必须在onSaveInstanceState(Bundle outState)里存而不是依赖字段直接跨重建存活。常见做法是结合ViewModel因为ViewModel在Activity重建时不会销毁天然绕开了状态恢复问题。这也是我强烈建议结合ViewModel的原因public class PlayViewModel extends ViewModel { private final MutableLiveDataBoolean isLandscape new MutableLiveData(false); private final MutableLiveDataInteger progress new MutableLiveData(0); }ViewModel在这里的用法是方向切换重建Activity时Fragment的实例会重新创建但ViewModel还是原来那个你把旋转状态、播放进度、UI开关状态都放ViewModel里onCreate时从中恢复就再也不会丢失了。这个方案比存Bundle简单十倍也是业内处理旋转的标准姿势。3.4 整体方向策略的统一管理如果页面数量多每个Fragment都自己调setRequestedOrientation容易满天飞。我建议做一个方向策略管理器统一收口public enum ScreenOrientationPolicy { PORTRAIT_ONLY, // 竖屏锁定 SENSOR_LANDSCAPE, // 横屏跟随传感器 AUTO_ROTATE; // 完全自由旋转 private static final MapString, ScreenOrientationPolicy POLICY_MAP new HashMap(); public static void registerPolicy(String fragmentTag, ScreenOrientationPolicy policy) { POLICY_MAP.put(fragmentTag, policy); } public static ScreenOrientationPolicy getPolicy(String fragmentTag) { ScreenOrientationPolicy policy POLICY_MAP.get(fragmentTag); return policy null ? PORTRAIT_ONLY : policy; } }在Fragment的onResume里从POLICY_MAP查当前页面tag对应的策略再映射到requestedOrientation。这个做法的好处是页面创建时不一定要立刻调用注册页面可以在加载数据后才能确定方向策略注册时机灵活且所有策略集中在同一个类里排查问题只需看一处。同时也便于A/B测试比如某类视频需要横屏运营配置切到竖屏锁定时只需要改注册表。还有一个隐蔽坑如果你在onCreate里注册策略但Fragment被FragmentManager恢复实例时不走onCreate会直接onResume那么首次跳转没问题重建后策略查不到方向就回到竖屏了。所以注册时机应该放到onAttach或onCreate里都行但不能放到onResume后面。我在项目里把注册放在BaseFragment的onCreate中搭配getArguments()里的页面routeId作为key保证实例恢复后策略仍然有效。4. 横竖屏切换与View层自适应的细节处理页面级方向控制只是第一步更费心思的是View层在不同方向下的布局自适应。很多页面在竖屏长这样顶部标题栏中部内容区底部操作按钮。一旦切到横屏可用宽度暴增、高度骤减竖屏布局直接变形。4.1 用资源限定符区分横竖屏布局最标准的做法是layout-land目录。竖屏布局放res/layout/activity_video.xml横屏布局放res/layout-land/activity_video.xml系统方向变化后会自动加载对应布局。很多新手担心改布局文件名会丢失状态其实不会只要View的id保持一致状态绑定就不会断。但要注意layout-land里View的id必须和竖屏布局中完全一致否则onCreate里findViewById会拿到null直接空指针崩掉。如果某个View只在横屏存在、竖屏没有代码里要用if (findViewById(R.id.xxx) ! null)做判空避免NPE。4.2 代码动态适配onConfigurationChanged与尺寸监听并不是所有场景都适合写两份XML有时只在代码里根据方向调整一两个参数就够了比如视频画面的宽高比。例如播放器页面竖屏时是16:9上下留黑横屏时是全屏铺满用layout-land就不灵活更适合在onConfigurationChanged里动态改Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); boolean isLandscape newConfig.orientation Configuration.ORIENTATION_LANDSCAPE; if (isLandscape) { // 横屏隐藏系统栏全屏沉浸 getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); // 调整播放器容器宽高为全屏比例 mPlayerContainer.getLayoutParams().width ViewGroup.LayoutParams.MATCH_PARENT; mPlayerContainer.getLayoutParams().height ViewGroup.LayoutParams.MATCH_PARENT; } else { // 竖屏恢复16:9比例 mPlayerContainer.getLayoutParams().width ViewGroup.LayoutParams.MATCH_PARENT; mPlayerContainer.getLayoutParams().height (int) (screenWidth * 9f / 16f); } mPlayerContainer.requestLayout(); }这种动态适配的好处是横竖屏切换时不会重新加载XML布局View实例不会重建画面状态、播放进度、SurfaceTexture连接全部都还在不会断流也不会黑屏。适用于播放器、相机预览、地图这类有重量级Surafce或Texture绑定的页面。4.3 FlexboxLayout等自适应布局的兼容问题现在很多页面用FlexboxLayout来实现复杂自适应横竖屏切换时FlexboxLayout本身是OK的但容易出现子项宽高计算不及时的问题。比如flexWrapwrap配合flexShrink切屏后因为宽度变化子项换行逻辑变了但子项的measure尺寸可能还缓存着旧值。解决办法是在onConfigurationChanged里强制刷新mFlexBoxLayout.requestLayout();另外如果用了ConstraintLayout做自适应横竖屏切换时某些guideline的百分比会重新计算这部分通常是可靠的但注意别在setGuidelinePercent时用了绝对像素那样方向一变就乱了。我的经验是布局尽量用百分比或权重避免在横竖屏差异大的页面里写死dp。5. 特殊场景处理WebView、Dialog、三方页面与电量方向5.1 WebView加载H5页面时的方向问题WebView比较复杂。H5页面自己会用window.orientation和matchMedia来响应横竖屏但WebView所在容器方向没有正确传递的话H5侧拿到的还是旧值导致页面宽度错乱、字体偏大。这里我踩过一个很深的坑在video标签全屏播放时Android设备上通常会自动横屏但如果你在外部用setRequestedOrientation把方向锁成竖屏WebView内部的全屏视频将被强制退出全屏或者画面旋转异常。解决方案是在WebView的onShowCustomView回调里把宿主方向动态切换成横屏在onHideCustomView里切回页面策略方向。代码如下mWebView.setWebChromeClient(new WebChromeClient() { Override public void onShowCustomView(View view, CustomViewCallback callback) { super.onShowCustomView(view, callback); if (getActivity() ! null) { getActivity().setRequestedOrientation( ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); } } Override public void onHideCustomView() { super.onHideCustomView(); if (getActivity() ! null) { getActivity().setRequestedOrientation( ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } });如果你希望H5页面能收到方向变化还要确保在页面可见时WebView能获取到系统方向而不是Activity方向被锁死后的默认竖屏。一种技巧是进入H5页面时清掉screenOrientation限制设置SCREEN_ORIENTATION_UNSPECIFIED然后让WebView跟随系统方向自己决定。适合那些全屏播放视频的H5页面能直接和系统原生播放器一致。5.2 Dialog窗口的方向跟随Dialog是独立Window默认不会跟随Activity方向变化即使宿主Activity重建了Dialog还会停留在原来的位置上甚至错位。解决方案分两种。如果是普通Dialog建议在方向切换时直接关闭方向恢复后重新弹出这样最干净也不用管Dialog内部状态。如果是DialogFragment那就得保证DialogFragment的宿主Fragment注册了合理的方向策略并且DialogFragment本身实现了onSaveInstanceState保存关键数据。另外有个bug很隐蔽在横屏页面上show一个Dialog然后旋转到竖屏Dialog的宽高还是按横屏计算的按钮会被截掉。我处理时会在onConfigurationChanged里对Dialog做一个自适应重置Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (mDialog ! null mDialog.isShowing()) { mDialog.getWindow().setLayout( (int) (getResources().getDisplayMetrics().widthPixels * 0.9f), WindowManager.LayoutParams.WRAP_CONTENT); } }5.3 第三方SDK页面和系统权限弹窗的方向冲突有些第三方SDK的页面支付、登录、分享是独立Activity它们自己的方向策略你控制不了。常见场景是视频播放页是横屏点支付跳到SDK页面SDK页面是竖屏用户支付完返回视频页恢复成横屏这个流程一般没问题。但如果支付SDK页面也是横屏或者跟随系统用户支付时把设备转正返回后视频页会卡在竖屏重新播放时又强制横屏来回跳转头都晕。我的方案是在onResume里做强校验每次从后台回到页面都要重新执行一次updateOrientation()。这样即使中间被其他Activity打断重新可见时方向也会被纠正回所属页面的策略。同理在onWindowFocusChanged里也可以加一道防线如果页面要求横屏但实际方向是竖屏就重新调用setRequestedOrientation。5.4 系统自动旋转开关与Sensor延迟用户关掉了系统自动旋转开关后SENSOR_LANDSCAPE就失效了即使你把方向切到横屏页面还是固定在竖屏位置。这类场景没有万能解但如果你确实需要让某些页面在系统关闭自动旋转时也能横屏就用SCREEN_ORIENTATION_LANDSCAPE强制指定。这里有个细节强制LANDSCAPE会让左横、右横不跟随传感器转动即使用户把设备倒过来屏幕还是保持初始横屏方向。对于视频横屏播放这个副作用通常可以接受但对于地图、拍照预览这类设备转向有意义的场景就得用SENSOR_LANDSCAPE并且向用户说明需要打开系统自动旋转开关否则只能固定一个方向。6. 实战复盘我在项目里是怎么落地这套方案的铺垫了这么多来一个完整的落地案例。我之前做的资讯App底部Tab有首页、频道、消息、我的四个一级页面都是竖屏。二三级详情页里文章详情是竖屏图片浏览支持旋转视频详情页进入后就强制横屏视频播放完后返回详情流详情流保持竖屏。工程上宿主只有一个HomeActivity用Navigation组件管理Fragment栈。第一步Manifest设置activity android:name.HomeActivity android:screenOrientationportrait android:launchModesingleTask /activity注意这里我没有配configChanges方向切换时允许系统重建Activity。第二步BaseFragment注册方向策略在每个Fragment的onCreate里根据路由名注册方向策略public class VideoDetailFragment extends BaseFragment { private PlayViewModel playViewModel; Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ScreenOrientationPolicy.registerPolicy(VideoDetailFragment, ScreenOrientationPolicy.SENSOR_LANDSCAPE); playViewModel new ViewModelProvider(this).get(PlayViewModel.class); } }文章详情页注册成竖屏图片预览页注册成AUTO_ROTATE。第三步Fragment业务状态全部收口到ViewModel播放进度、播放/暂停状态、当前清晰度、弹幕开关全部放在PlayViewModel。Activity重建后Fragment拿到同一个ViewModel直接恢复UI不需要额外处理。第四步处理页面间切换时的方向提前复位在HomeActivity里监听Fragment切换navController.addOnDestinationChangedListener((controller, destination, arguments) - { String tag destination.getClassName(); ScreenOrientationPolicy policy ScreenOrientationPolicy.getPolicy(tag); int targetOrientation; switch (policy) { case SENSOR_LANDSCAPE: targetOrientation ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; break; case AUTO_ROTATE: targetOrientation ActivityInfo.SCREEN_ORIENTATION_FULL_USER; break; default: targetOrientation ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; break; } if (getRequestedOrientation() ! targetOrientation) { setRequestedOrientation(targetOrientation); } });让宿主Activity统一监听目的地变化统一去调API比每个Fragment各调各的靠谱多了。Fragment的onResume里保留一个兜底调用即可防止某些极端情况比如进程恢复、冷启动下Navigation监听没来得及注册。第五步横竖屏的View层处理图片预览页用layout和layout-land两套布局方向切换系统自动换布局省心。视频详情页不用两套XML用onConfigurationChanged动态调整播放器容器尺寸避免Surface重建和播放状态丢失。第六步WebView处理资讯详情页里有不少H5链接WebView页面跟随容器方向但在网页内需要全屏播放的视频时走onShowCustomView切横屏、onHideCustomView恢复原策略。整个流程跑下来表现是首页冷启永远是竖屏点进视频详情时系统旋转动画自然过渡到横屏返回时平滑回到竖屏播放进度、弹幕开关全部记忆WebView的H5全屏播放也不出幺蛾子。用户对方向的感受几乎没有主动性直觉上就是“该横的地方横该竖的地方竖”。7. 常见问题排查与避坑经验速查这节整理一下我在项目测试阶段遇到的典型问题按频率和破坏力排了个序给后面做类似需求的人当参考。7.1 setRequestedOrientation导致Activity提前重建如果你在onCreate里设置方向系统可能会在onCreate还没执行完时就再次触发onCreate导致页面闪屏两次。解决方法是把setRequestedOrientation放到onPostCreate或onResume里执行首次创建页面时也按这个节奏来。7.2 Fragment事务和方向切换并发冲突在竖屏页面点击跳转横屏页面setRequestedOrientation触发Activity重建此时如果FragmentTransaction还在执行很可能报Fragment already added或Can not perform this action after onSaveInstanceState。规范做法是跳转事务的commit方法统一用commitNow()或者确保在onCreate中完成Fragment的初始化不要依赖异步回调来添加Fragment。7.3 横屏页面返回后前一个页面布局错乱这是典型的Fragment状态保存没做好。前一个竖屏页面被压栈后Activity重建时FragmentManager会恢复View树但如果你在竖屏页面里用了layout-land的资源限定符恢复时可能因为方向还没稳定而加载了错误的布局资源。解决方法是Activity重建后在onCreate里先根据目标方向设置好requestedOrientation再执行Fragment的恢复流程。7.4 横屏时输入法遮挡横屏键盘高度很大windowSoftInputMode的adjustResize有时候不生效导致EditText被挡住。建议横屏页面统一用adjustPan或者adjustResize配合fitsSystemWindows做吸底适配。另外横屏时键盘本身就有全屏倾向需要针对onConfigurationChanged里键盘的高度变化做手动校验可以用ViewTreeObserver监听全局布局变化动态调整输入框位置。7.5 兼容性问题Android 12以上旋转动画卡顿Android 12引入旋转动画优化理论上旋转应该更顺滑但如果你的Activity用的是sharedTransition旋转时会因为布局缓存失效导致转场动画卡顿。解决办法是在旋转页面里关闭SharedElement过渡或者把windowOptOutEdgeToEdgeEnforcement设为true绕开新增的边到边约束。7.6 方向策略注册表的内存回收时机如果你的策略注册表用的是static Map需要小心内存泄漏和僵尸条目。页面销毁后策略还留着如果复用路由名新页面可能拿到旧策略。规范做法是页面onDestroy时移除自己的策略或者在启动对应页面时每次都重新覆盖注册。我的项目直接采用了“每次进页面都覆盖注册”的方案简单可靠不会因为异步回收出问题。7.7 测试机上“转一下”和“转两下”的行为差异有些测试设备把传感器灵敏度调得很高用户轻微倾斜就触发旋转导致页面在横竖屏之间反复切换不仅卡还耗电。如果你发现页面偶尔抖动旋转可以在页面的onConfigurationChanged里加一个旋转间隔锁比如每次方向变化后500毫秒内忽略新的方向变化请求private long lastOrientationChangeTime 0; private static final long ORIENTATION_CHANGE_THRESHOLD 500; Override public void onConfigurationChanged(Configuration newConfig) { long now System.currentTimeMillis(); if (now - lastOrientationChangeTime ORIENTATION_CHANGE_THRESHOLD) { return; } lastOrientationChangeTime now; // 正常处理方向变化 }7.8 某些平板上传感器方向混乱平板和翻盖设备的方向传感器组和手机不太一样SCREEN_ORIENTATION_SENSOR_LANDSCAPE在平板上经常出现方向偏移。如果你需要面向平板做兼容建议用SCREEN_ORIENTATION_USER_LANDSCAPE它会尊重用户在系统设置里选择的旋转方向左横或右横不会随传感器乱跳。8. 横屏页面的布局细节打磨不转白、不截断、不变形页面能在横竖屏之间切换只是第一关真正的体验门槛在切换之后的布局细节。我见过很多App方向转了但横屏状态下控件错位、表格被截断、图片变形还不如不转。这里有几个关键点分享一下。8.1 横屏下的安全区域适配横屏状态下的刘海屏、打孔屏避让区更复杂。系统栏、挖孔区域、手势提示条都可能和你的UI重叠。不要自己写死避让值要适配系统的窗口Insets。在横屏页面里我通常会在根布局上设置fitsSystemWindows并且在onApplyWindowInsets里动态计算左右避让距离ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) - { Insets systemBars insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return WindowInsetsCompat.CONSUMED; });特别是横屏视频播放页让画面内容居中偏下左右给系统挖孔区域留白比左上角顶到边好看太多。完整的方案可以参考官方的“边到边”适配指南我这里强调一下别用getStatusBarHeight()这类硬编码各家厂商定制系统上数值差异很大。8.2 横屏状态下的触摸坐标换算如果你的横屏页面用了自定义View而且触摸事件需要和内容坐标做映射方向切换后坐标原点、方向都有可能变化。比如图表页竖屏时x轴从左到右横屏时可能因为布局变化x轴变成自上而下。这种场景我建议在onSizeChanged里重新计算坐标系而不是缓存旧坐标。另外自定义View在横竖屏切换时不要用getWidth()去判断方向因为getWidth()在切换过程中可能返回旧值要用getResources().getConfiguration().orientation判断。8.3 多页签Fragment嵌套时的方向冲突如果一个横屏页面内部还有横向ViewPager里面嵌了好几个Fragment每个子Fragment都去设置requestedOrientation就乱了。我的原则是只有最外层可见页面才能控制方向子Fragment一律不碰方向API。外层的横屏容器Fragment负责方向切换子Fragment只管内容布局。如果子Fragment的内部确实有自己需要旋转的模块比如横滑的Canvas那是View层内部逻辑不该影响系统方向。9. 跨平台框架视角小程序、uni-app、Flutter页面如何实现类似效果移动端不只是原生Android现在很多App是跨平台方案Uni-app、Flutter、React Native都常见。页面级旋转控制在不同框架里都有自己的限制和玩法这里简单带一下给遇到类似需求的人指个方向。9.1 Uni-app/小程序页面配置文件控制小程序的全局配置和页面配置是分开的app.json里的pageOrientation控制全局默认方向单个页面的.json配置文件里也可以单独设置pageOrientation: auto或portrait。所以小程序天然支持页面级方向控制用起来很方便。但是在H5端和App端Uni-app的pageOrientation支持不一致。H5端可以监听window.matchMedia((orientation: portrait))App端可以调用原生插件或plus.screen.lockOrientation来锁定。需要注意如果Uni-app的App端用的是nvue部分页面方向切换时会有兼容性问题最好是页面设计时就规划好横向和竖向两种布局。9.2 Flutter使用SystemChrome.setPreferredOrientationsFlutter是纯自绘UI方向控制交给系统原生层处理。常用的做法是SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]);这个调用是全局性的但可以在WidgetsBindingObserver的didChangeAppLifecycleState或RouteAware里根据当前路由重置。因为Flutter的页面栈和方向控制是分离的切换页面时如果上一个页面设了横屏、当前页面要竖屏新页面也要在initState里重置方向。此外Flutter的MediaQuery会自动响应旋转所以大部分布局自适应靠响应式Widget就够不需要像Android那样写两套布局。9.3 一个核心提示不管什么框架页面级方向控制本质上都是“页面生命周期 系统方向API”的组合。跨平台框架下方向API往往是异步的调用后立刻生效不说还受系统动画影响。所以做方向切换时一定要加防抖和页面可见性检查尽量在onShow/onResume/didChangeAppLifecycleState里统一触发不要在每个Widget的build里调。10. 沉淀这套方案做到什么程度才算合格页面级横竖屏旋转自适应这个需求说大不大说小也不小。真正的衡量标准不是“能转就行”而是体验的完整度。我自己梳理了几个质检维度你也可以拿来做验收标准。第一冷启动到任何一个页面方向都是确定的不会先竖屏闪一帧再横屏。这个只要统一了策略管理器并且在启动时快速设置方向就能做到。第二横竖屏切换时页面状态不丢。播放进度、滚动位置、输入内容、选中态全都要保留。这个必须借助ViewModel或状态持久化兜底。第三横屏页面内的控件不重叠、不截断自适应布局在常见分辨率下都经得起看。这个靠资源限定符和动态尺寸调整组合。第四WebView、系统弹窗、输入法等第三方交互不捣乱。每次方向切换都把WebView全屏播放、软键盘高度、Dialog位置这些边界场景全部过一遍。第五方向切换过程平滑没有明显的白屏、抖动、闪屏。这个和转场动画、方向复位时机相关联也是我反复提“提前复位”的原因。按这五条逐项过一遍基本可以达到一个“用户感知不到方向设置存在只觉得很顺手”的状态。11. 一组可以直接抄走的工具类示例最后给一个可以直接拿去改的基础工具类包含了方向切换和状态保存的一些核心逻辑。基于你自己的工程结构调整包名稍微打磨一下就能用。public final class ScreenOrientationHelper { private ScreenOrientationHelper() { } /** * 切换到目标方向同时避免重复触发 */ public static void switchTo(Activity activity, int targetOrientation) { if (activity null || activity.isFinishing()) { return; } if (activity.getRequestedOrientation() ! targetOrientation) { activity.setRequestedOrientation(targetOrientation); } } /** * 从策略值映射到ActivityInfo的方向值 */ public static int toActivityInfoOrientation(ScreenOrientationPolicy policy) { switch (policy) { case AUTO_ROTATE: return ActivityInfo.SCREEN_ORIENTATION_FULL_USER; case SENSOR_LANDSCAPE: return ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; default: return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } } /** * 是否正在横屏 */ public static boolean isLandscape(Context context) { return context.getResources().getConfiguration().orientation Configuration.ORIENTATION_LANDSCAPE; } }这东西放到BaseActivity或BaseFragment的基类里每个页面只需要声明自己的策略即可不用再关心API调用细节。最后再说一个我实际开发中的体感页面级横竖屏旋转最怕的不是代码实现复杂而是需求边界不清晰。什么时候该横、什么时候该竖、系统自动旋转关了怎么办、用户强制横屏怎么处理这些最好在产品阶段就说清楚。等代码写完了再反复改方向策略那才真的是噩梦。你把方向策略当成一种全局状态来管理而不是散落在某个页面里的临时操作这个问题的所有坑基本都能提前避开。
返回列表