ARTICLE DETAIL

资讯详情

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

Android横屏与多屏幕适配实战:从生命周期到刘海屏的完整指南

Android横屏与多屏幕适配实战:从生命周期到刘海屏的完整指南 横屏适配做了这么多年踩过的坑比踩过的键盘还多。每次产品经理一句“这个页面横屏看一下”接下来的几小时基本就是和Activity重建、布局错乱、刘海屏遮挡斗智斗勇。而屏幕适配这件事更是从早期的dimens多套文件到现在的sw限定符走了不少弯路。这篇文章就把横屏适配和多屏幕适配这两件事从头到尾捋一遍。横屏适配的核心是搞清楚Activity生命周期变化、资源目录切换、布局差异处理这三件事多屏幕适配的核心则是理解dp/dpi原理、掌握主流的sw最小宽度限定符方案、处理好刘海屏折叠屏这类特殊场景。内容既有原理讲解也有可以直接抄作业的代码和配置适合刚接触适配的初级开发者也能给做了几年还在靠hack方案硬撑的朋友一些参考。1. 先把横竖屏切换这件事拆明白很多人一提到横屏适配第一反应就是“给layout目录加个land后缀不就行了”。真这么简单就不会有那么多线上事故了。横竖屏切换触发的不只是布局切换而是整个Activity的重建流程这里面的每一个环节都可能出问题。1.1 系统为什么会重走一遍Activity生命周期默认情况下Android设备发生配置变更Configuration Change时系统会销毁当前Activity并重新创建一个新的。这个过程不是只换一张布局那么简单onPause、onStop、onDestroy依次走完然后重新走onCreate、onStart、onResume。说白了旧Activity整个被抛弃新Activity从零开始。系统这么设计不是为了折腾开发者而是默认所有资源都是跟配置绑定的横竖屏对应不同的layout目录、不同的dimens、不同的drawableAndroid希望你能用同一套代码加载不同配置下的资源那就只能从头走一遍生命周期让所有资源引用重新解析。但问题就出在这个“重新解析”上。如果你在Activity里持有了一些状态数据比如用户输入到一半的文本、网络请求返回的结果、滚动列表的位置这些数据存在成员变量里Activity一重建就全丢了。常见的结果就是用户横屏之前写了一堆字转一下屏幕输入框空了。解决这个问题的核心思路有两个一个是在onSaveInstanceState里保存数据并在onCreate中恢复另一个是让系统不重建Activity。前者是正统方案适用于大多数场景后者是用configChanges属性声明“这些配置变化我来自己处理”绕开系统重建机制。哪个更合适取决于页面复杂度。1.2 三种处理策略的取舍第一种策略也是最正统的不做任何特殊处理依赖系统重建Activity所有状态走onSaveInstanceState和ViewModel保存。看起来简单但状态恢复涉及序列化、ViewModel作用域、Fragment的自动恢复细节非常多。适合页面状态不复杂、登录页引导页这类低频交互页面。第二种策略用android:configChangesorientation|screenSize|keyboardHidden声明不给系统重建的机会自己监听onConfigurationChanged回调去刷新布局。这个方案最大的优势是Activity不会重建数据不丢改动量小。但代价是你得自己在回调里手动切换布局适配逻辑比如重新setContentView或者动态调整View参数。而且很多第三方组件库本身没有做横屏适配它们内部可能依赖生命周期重建来刷新自己你手动切断重建后这些组件反而表现异常。第三种策略强制锁定屏幕方向不响应横屏。通过android:screenOrientationportrait锁定竖屏或者代码里调用setRequestedOrientation。这是最省事的方案很多工具类App都这么干。但代价是牺牲用户体验特别是视频播放、图片预览、图表展示这类横屏体验明显更好的场景。我的建议是视频播放、图表、分屏多任务这类核心横屏场景老老实实用第一种方案配合状态保存内部工具页、设置页、列表页能锁就锁别硬撑横屏对于那些必须响应横屏又不能重建的复杂页面再用configChanges方案并做好onConfigurationChanged里的布局刷新逻辑。实战中大部分适配问题都是三种方案混在一起用页面级差异化处理。2. 横屏布局适配从layout-land到尺寸微调生命周期问题解决之后遇到最多的就是布局错乱。横屏后屏幕宽度变大但是高度变小竖屏时刚刚好的控件位置横屏后要么互相遮挡要么被挤出屏幕。这部分靠的就是资源目录和布局策略的配合。2.1 资源限定符怎么用才不踩坑横屏资源目录的基本规则是在res目录下创建layout-land、values-land、dimens-land等带land限定符的目录系统在横屏时会自动加载这些目录下的资源不用写任何判断代码。但这里有一个容易踩的坑系统限定符的匹配是“最佳匹配”而不是“唯一匹配”。比如你创建了layout和layout-land两个目录竖屏时加载layout横屏时加载layout-land这没问题。但如果再叠加一个layout-sw600dp目录情况就复杂了横屏且屏幕最小宽度大于600dp的设备会同时匹配layout-land和layout-sw600dp此时系统会根据限定符优先级选一个最精确的。land和sw这两类限定符没有绝对的优先级关系匹配结果取决于设备具体配置一旦匹配不如预期布局错乱就很隐蔽。横屏适配的目录规划我建议按照页面重要程度分级处理极少数核心页面单独做layout-land大多数页面不做横屏专属布局靠通用布局的自适应能力。这样既不臃肿也不容易发生限定符打架。如果只是尺寸微调可以只在values-land里放一份dimens覆盖竖屏值比如把某个margin从16dp改成24dp无需复制整个布局文件。复制布局文件最大的问题是维护成本后期改一个竖屏按钮的位置漏改横屏文件测试阶段就会漏问题。2.2 横竖屏布局差异的常见处理套路横屏的常见差异有几个类型。第一个是比例型差异竖屏时适合上下堆叠的线性布局横屏时换成左右并排的相对布局或约束布局。我的套路是优先使用ConstraintLayout它天然支持按比例约束和自适应间距很多页面一套布局横竖屏通吃。比如一个竖屏时居中偏上的卡片用ConstraintLayout加垂直偏移比例横屏时不需要换布局卡片位置自动合理。第二个是尺寸型差异竖屏时全宽的输入框横屏如果还是全宽就太长了。此时可以把宽度约束为最大宽度或者水平方向的margin放大。典型做法是设置layout_width和layout_height后再用layout_constraintWidth_maxSize限制最大宽度配合layout_constraintWidth_percent设定占据屏幕的比例。第三个是容器型差异ScrollView在竖屏时可能因为内容超高而需要滚动横屏时内容可能变得很短。反过来有的内容在竖屏刚好放下横屏高度不够需要滚动。横屏布局里根据内容结构决定是否包裹ScrollView并且注意ScrollView里套RecyclerView的嵌套滚动问题。这种页面的常规做法是横屏时把RecyclerView的高度设为wrap_content或者干脆不让它滚动由外层ScrollView统一负责。还有一个隐藏比较深的问题软键盘。横屏时软键盘几乎占据半个屏幕底部按钮很容易被顶飞。处理方式是在AndroidManifest的windowSoftInputMode里设置adjustResize并配合ViewCompat.setOnApplyWindowInsetsListener监听键盘弹出高度动态调整底部按钮的margin。这个细节不处理横屏输入场景基本必翻车。3. 多屏幕适配的核心dp、dpi与计算逻辑说完了横屏再看多屏幕适配。市面上的安卓设备屏幕尺寸从4.7英寸到12英寸以上都有分辨率从720P到2K、4K像素密度差异巨大。要在这么杂的环境下统一视觉表现彻底理解dp和dpi的关系是第一位的。3.1 px、dp、dpi的关系与换算先厘清概念。px是物理像素也就是屏幕上的发光点。dpi是屏幕像素密度表示每英寸有多少个像素点计算公式是屏幕对角线像素数除以对角线英寸数。dp是密度无关像素为了保证视觉尺寸在不同屏幕上表现一致。核心换算公式是px dp * (dpi / 160)。这个160是基准密度Android假设在160dpi的设备上1dp等于1px。如果一台设备dpi是320那么1dp就等于2px。为什么要除以160因为视觉尺寸的本质是物理尺寸1dp在理论上等于1/160英寸。屏幕密度越高同样的物理尺寸需要更多的物理像素所以dp换算成px时需要乘上密度系数。理解了这一点就不会再犯在代码里写死像素值的错误。实际项目中最常用的换算场景有两个。一个是在代码里把dp转px用TypedValue.applyDimension或者直接用Resources.getSystem().getDisplayMetrics().density乘以dp数值。另一个是从设计稿拿到px尺寸后除以设备密度得到dp值再写进布局。很多新手会在这里犯一个错以为把设计稿标注的px直接换成dp写在布局里就完事了。实际上设计稿通常基于某个固定宽度比如375dp你要把设计稿尺寸换算成目标设备的dp值需要先明确设计稿的dp基准宽度。比如设计稿宽750px、按2倍图切图那么基准密度是2设计稿的750px对应375dp之后的布局都应该在这个dp语义下书写。3.2 屏幕适配方案的演进从百分比到sw限定符早期的屏幕适配方案是按屏幕分辨率建dimens目录values-1280x720、values-1920x1080这样搞。这套方案文件爆炸维护成本极高而且系统限定符匹配时有容错机制找不到精确匹配就看default实际效果很不稳定。后来出现过一个比较知名的百分比库思路是把控件宽高按父容器的百分比动态计算。它解决了一部分问题但侵入性太强布局可读性差性能上也有损耗现在已经很少有人在用了。当前主流方案有两个。一个是sw最小宽度限定符也就是values-sw480dp、values-sw600dp、values-sw720dp这种目录。sw指的是屏幕最短边对应的dp值系统按这个值匹配最接近的资源目录。这套方案的优点是纯资源方案无侵入布局文件不用改直接换dimens里的数值就能全局调整尺寸。缺点是文件量还是不小而且sw值需要按主流设备分档档位分得不合理就会出现尺寸跳变。另一个是今日头条适配方案核心思路是放弃dp语义直接把designWidth设为目标宽度dp值比如360dp然后在App启动时通过修改全局DisplayMetrics的density、scaledDensity、densityDpi三个值强制让系统按当前设备的实际宽度动态计算dp换算率使得所有使用dp单位的布局都按照设备宽度等比缩放。头条方案最大的优点是布局代码完全不用动设计稿标注px直接默认为dp写上去即可所见即所得。缺点是它修改的是全局密度会影响动画、文字显示、第三方控件内部布局如果项目里混用了多种单位和库很容易出现意外缩放。还有一点如果你的App要支持平板头条方案默认是按屏幕宽度缩放的平板上会放大得非常夸张需要额外加平板判断逻辑。我的选择倾向是工具类、表单类、信息列表类App直接上头条方案开发效率最高游戏、视频、创意设计类对精确视觉要求高的用sw方案或者自研适配框架更可控。另外还有一个折中方案dimens只建标准值和常用档位配合ConstraintLayout的百分比约束来消化中间差值项目结构简单时这套组合很实用。3.3 实际项目中的适配清单落到实际项目里多屏幕适配要检查的点大致可以整理成下面这个流程。全局基础设置确认设计稿宽度基准统一density换算工具类避免各处写死转换逻辑。布局层面外层优先ConstraintLayout用比例约束、链、Guideline替代固定间距和固定宽高。字体适配字体单位尽量用sp但也要知道sp会跟随系统字体缩放设置如果不想让大字模式打乱布局可以设置字体不随系统缩放或者在特定页面关闭sp缩放。图片与图标切图使用drawable-xxhdpi等密度目录控件尺寸用dpBitmap在代码中加载时避免手动解压到全尺寸。列表itemitem根布局高度尽量自适应内容不要固定死高度除非产品明确要求固定行高。横竖屏切换测试每个页面至少在竖屏和横屏下各过一遍特别是有RecyclerView、EditText、弹窗的页面。这个清单看着简单实际执行起来最考验的是设计规范。如果设计稿本身没有按标准网格出图间距随意开发阶段再强的适配方案也救不回来。所以好的适配一定是设计、开发、测试三方对齐规范的结果。4. Android Studio里的多屏验证与调试技巧代码写完了适配效果不能靠想象要借助工具验证。Android Studio自带的工具里有对适配帮助极大的功能用好的话能省大量真机测试时间。4.1 Layout Validation的使用实战Layout Validation是Android Studio自带的多屏预览工具。入口在编辑器右上角的预览窗口点击View菜单选择Tool Windows里的Layout Validation打开后可以同时预览同一个布局在多个设备上的渲染效果。这个工具最大的价值是能在不改动代码的情况下快速发现布局在窄屏、宽屏、平板上的表现差异。比如一个ConstraintLayout约束的卡片页面你在Preview里选一台Pixel 2和一台Pixel Tablet宽高比不同布局会不会挤压、图片会不会变形、间距合不合理一眼就能看出来。我在实际使用中一般会把这些设备加进集合里统一预览一台小屏旗舰比如5.5英寸左右1080P、一台大屏旗舰6.7英寸左右2K、一台老式中端机720P、一台平板10英寸左右。每次改完布局代码先跑一遍Layout Validation再看真机。这样做最大好处是快速筛选出明显的问题布局再集中针对性修复。Layout Validation只能看布局渲染效果无法验证生命周期变化和状态保存逻辑所以它替代不了真机测试但作为适配前期的自测工具确实高效。4.2 设备模拟器与云真机测试的取舍Android Studio自带的AVD模拟器可以创建各种屏幕尺寸和密度组合的设备。但模拟器资源消耗大启动慢而且模拟器的传感器、真实硬件行为跟真机有差异比如刘海屏的真实显示区域、键盘弹出行为、系统字体缩放模拟器模拟得不够真实。针对需要精确验证的机型我的经验是先本地模拟器跑主流配置再交给云真机平台在真实设备矩阵上做自动化遍历测试。云真机平台的好处是设备覆盖型号广可以在真实设备上自动跑Monkey和UI自动化用例截屏对比。横竖屏切换、软键盘弹出这类场景云真机里都有真实系统行为可以复现。不过云真机也有局限。自动化的用例覆盖面取决于用例脚本写得多深如果测试用例本身没有覆盖到某个横屏页面那云真机跑再多遍也发现不了问题。所以在适配联调阶段核心页面我建议一定要在真机上手工滑一遍特别是那种“看着没事点下去才发现按钮偏移”的交互层问题自动化脚本不容易发现。5. 进阶场景刘海屏、挖孔屏和折叠屏多屏幕适配做到这个阶段基础工作已经完成剩下的都是麻烦。这里说的麻烦是指刘海屏、挖孔屏、瀑布屏、折叠屏这类形态差异大的设备它们在系统层面对App的渲染区域有特殊处理使用的就是WindowInsets和窗口布局参数。5.1 WindowInsets与安全区域Android从API 21开始引入WindowInsets的概念系统把状态栏、导航栏、刘海区域、键盘等系统窗口区域通过WindowInsets传给应用应用按需处理。默认情况下Target SDK 35的App在不做任何配置时内容是不会自动避开刘海等区域的实际行为是系统会强制把窗口设置为不延伸到这些区域但竖屏下底部导航栏有时会被内容覆盖横屏时刘海区域的显示问题更明显。处理安全区域的正统做法是监听View的setOnApplyWindowInsetsListener拿到系统栏insets之后给需要避让的View设置padding或者margin。示例代码大致是这样ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets - val bars insets.getInsets(WindowInsetsCompat.Type.systemBars()) binding.contentLayout.updatePadding( left bars.left, top bars.top, right bars.right, bottom bars.bottom ) insets }这样就能保证内容始终不被系统栏遮挡。对于横屏场景导航栏通常在屏幕底部或侧边刘海通常在一侧使用systemBars类型获取到的值基本覆盖了这些区域。需要特别注意的是displayCutout这个类型。有些设备的刘海区域不在系统栏范围内比如横屏时刘海在屏幕侧边中间位置此时只有使用cutout类型才能拿到实际的安全距离。val cutout insets.getInsets(WindowInsetsCompat.Type.displayCutout())如果App的目标SDK比较高系统默认会尽量帮你处理cutout但有些设备厂商会提供“刘海屏显示”开关强制App的显示区域扩展到刘海区域这时候想不遮挡就只能自己适配。还有一个思路是把根布局的fitsSystemWindows设为true让系统替你处理padding但这个方案的应用场景受限它只解决padding问题如果布局内部有重叠或滚动内容依然需要手动适配。5.2 折叠屏适配要点折叠屏设备这两年越来越多适配思路跟普通多屏幕稍不一样。折叠屏展开后会触发screenLayout、smallestScreenSize等配置变化相当于一次屏幕尺寸的剧烈改变如果Activity没有做configChanges声明就会被系统重建。折叠屏适配的第一个重点是确认你的App是否要支持展开态。如果只是普通应用默认在展开态下的效果基本是屏幕变大而已大多数情况下布局不至于崩溃但观感可能会比较空。想要展开后全体验升级通常需要监听posture变化在onConfigurationChanged里判断当前是否为展开状态并切换布局资源目录或者调整控件比例。第二个重点是横竖屏与折叠的交叉情况。折叠屏展开后屏幕比例接近正方形此时竖横屏的判断跟普通手机不太一样开发者可能会发现系统对横竖屏的判定跟预期不符。排查思路是检查smallestScreenSize的实际值并据此配置布局资源。第三个重点是自适应窗口。Android官方推荐使用自适应窗口库Jetpack WindowManager来监听窗口状态变化它提供了windowLayoutInfo的回调可以拿到当前窗口的显示姿态、折叠角度等信息。示例代码大致是这样WindowInfoRepository().windowLayoutInfo(activity) .collect { layoutInfo - val foldingFeature layoutInfo.displayFeatures .filterIsInstanceFoldingFeature() .firstOrNull() // 根据foldingFeature的state和orientation做适配 }折叠屏适配不用追求一步到位一般做到“展开不崩溃、不遮内容、关键控件可用”就算合格。过度适配反而容易引入新的兼容问题。6. 常见问题排查与避坑记录这一章的内容是从实际项目中踩出来的教训。适配问题大多不会出现在代码语法层面而是隐藏在系统的各种默认行为里排查起来比较费劲。6.1 横屏Activity被重建数据丢失这是横屏适配出现频率最高的问题。现象是横屏后Activity重新执行onCreate界面上用户输入的内容全部丢失。排查顺序如下先看AndroidManifest里这个Activity是否声明了configChanges。如果没有声明系统必然重建Activity那就需要检查状态保存是否完整。onSaveInstanceState里是否保存了EditText的输入内容RecyclerView的滚动位置是否保存了如果数据存在ViewModel里Activity重建后ViewModel是否会被清空如果声明了configChanges数据还是丢了那问题就出在声明不全。比如只写了orientation没写screenSize部分Android版本上仍然会重建Activity。因为从API 13开始screenSize也是触发生命周期变化的配置项。正确写法是android:configChangesorientation|screenSize|keyboardHidden|screenLayout这里需要补一个细节声明了configChanges意味着所有配置变化都交给App自己处理那么onConfigurationChanged里必须自己处理布局切换和尺寸变化否则会出现界面不刷新、布局残留的问题。6.2 横屏后布局错乱但是竖屏正常这类问题的根源通常不在屏幕方向而在于布局本身不具备自适应能力。比如固定宽高的TextView、写死margin的ConstraintLayout、没有处理长文本换行的控件在宽高比变化时就会露出马脚。排查思路是用Layout Validation切到横屏设备看看实际效果。根布局如果是LinearLayout检查权重是否分配正确如果是RelativeLayout检查依赖关系的控件是否因为空间不足而重叠如果是ConstraintLayout检查是否有控件宽度设置了固定值没有使用约束链或比例约束。还有一种常见情况是横屏专属布局文件写好了但没生效。检查layout-land目录是否存在、命名是否正确必须是layout-land而不是layout_land、Activity的setContentView是否使用了R.layout下的同一常量。系统资源目录的匹配是靠目录名实现的目录名写错系统不会报错只是静默加载默认布局这个坑特别隐蔽。还有一种情况是代码逻辑里对宽高做了预设。比如在某些麒麟芯设备上系统汇报的屏幕尺寸跟实际显示不一致导致dp计算偏离预期。遇到这种设备相关的问题先打印DisplayMetrics确认实际值再针对性处理。6.3 我的适配排查顺序速查表下面这个表格是我在实际项目中总结出的排查顺序基本能覆盖90%的适配问题。现象排查步骤常见原因横屏后Activity重建检查configChanges声明声明缺失或不全横屏后数据丢失检查onSaveInstanceState和ViewModel状态没有保存或保存类型不可序列化横屏布局加载不对检查layout-land目录和布局文件名目录名错误或文件缓存未刷新横屏控件被遮挡检查安全区域padding和刘海屏适配没有处理WindowInsets和displayCutout平板界面控件拉伸变形检查px/dp混用情况代码中写死px像素值字体显示异常检查sp单位和系统字体缩放sp被全局density修改方案污染动画或弹窗位置偏移检查根布局和DecorView层级弹窗的anchor位置计算用的是旧的屏幕尺寸这个表里的每一项都是我真实处理过的案例。特别提醒一下字体缩放那个问题用了全局修改density的方案之后字体缩放会变得异常因为scaledDensity也会被改掉。如果不想让用户设置的大字体被适配方案吞掉可以在修改全局density时单独保存原始的scaledDensity再根据系统字体缩放比例叠加。6.4 适配的边界什么时候该停手适配工作最大的陷阱不是不会做而是做得过火。我把适配按设备类型划分成几个边界手机和平板共用一个布局但平板端只保证信息可读、操作可用不追求视觉上的完全统一。按分辨率建dimens目录的方案只维护当前用户设备TOP10的分辨率不做全量覆盖。桌面级设备、电视、车载等大屏场景只做基本不崩的标准不专门优化。适配的本质是取舍。不可能让每一个屏幕像素都跟设计稿一模一样这既不可能也没必要。适配的最终标准是内容不遮挡、操作可达、视觉无明显变形。超出这个标准的优化大部分是自嗨。横屏适配和多屏幕适配说到底是一套理解资源系统、理解生命周期、理解窗口机制的工程实践。把基础概念搞清楚把工具用好把测试做扎实再刁钻的屏幕变化也翻不了天。我的经验是每次遇到适配问题先别急着写代码花十分钟确认系统行为到底是怎么样的往往比埋头改布局高效得多。
返回列表