开发多年, 碰到不少恶心需求与恶心甲方了. 但这次edgeToEdge真是刷新了我对google下限的想像.
事情是Google要求8月底前, 所有play store上的app升级targetSDK为35 (政策链接是这个: developer.android.com/google/play…, 目前只说2024年8月底前到34; 但按google每年的规律, 前几年也是每年都升了33, 34的, 所以今年8月底前要肯定是升到35的).
其它所有变动都可以理解, 只有一项是恶心的EdgeToEdge, 即Google强制所有app的所有页面全部变为全屏模式(full screen, edge to edge).
一. EdgeToEdge为何让人火大?
先来个图说明下这个强制要求会让我们的app如何变化:
恶心的点就在于Google是强制所有app都要这样做, 而且还有一些细节点很恶心, 后续会讲到. 这样就给我们的每一个app都造成了麻烦. 我们app设计得好好的, 为什么一定要全屏.
而且你强制让最上面的OS status bar给占了有什么意思哦, 这不是把"时间, 电量, wifi信号, .."这些都给遮挡了嘛?!
所以Google就会让你做适配, 不要占了status bar (如上图的And15+所示, 页面内部就没有占据status bar的区域)? 我就晕死了, 那适配后的效果跟不全屏不就一样了, 那你强制全屏有什么意思!!!!
同理, 你的手机要是底部是"三按钮"或"二按钮"的OS navigation区域(如"后退键 + home键 + recentTasks键"), 那全屏后你的内容也会和这个区域重合.
重合自然是不行的, 不然你按下去, 到底是响应OS的back键, 还是响应你页面中的某View的点击呢? 所以你肯定要做成不重合.
那又回到原点? 要是不强制全屏, 本来就不重叠; 你现在强制全屏, 又要给开发增加额外工作量来做到不重合. 这就像极了, 本来一条路好好的, 你非要拆了, 再重新修一遍, 重新搞得和原来的路一样, 那你干嘛重修这条路?
p.s. Google离最初的"不作恶"初心越来越远了, 现在的Android更新也不侧重于用户的体验, 反而一个劲地推销自己的AI. 很多新东西也越来越倾向于大厂的开发/经理为了自己的KPI生硬地搞新东西, 而不是想这些新东西能解决什么难受的痛点.
二. 着手做之前的分析
虽然这个需求让人很无语, 吐槽完后做还是肯定要做的.
先说明一下, 我们的UI设计有自己的风格, 我们自己有些页面是全屏, 有些页面不是. UX也没有说要改动的意思, 所以我们适配edge to edge的目的并不是要让页面全屏(即不是Google所希望的方向), 而是让我们的app的视觉效果保持和以前一致.
主要是有同学总在说我的适配方法根本不是Google推荐的, 但这是因为Google推荐的一不全面(解决不了所有问题), 二不是我们的UI设计方案不是全屏. 我个人也很讨厌Google自己主推的material design(比如说一个阴影搞这么复杂), 也讨厌Google主推的edge to edge, 强制splash, .... 所以并不是Google说什么我们就会去做什么, 我们app有自己的UI上的考量.
好了, 回归正题, 有了需求后, 我们有必要在编码之前, 先分解下这个需求, 以及分析下它到底影响了哪些部分, 这样我们才能一步一步地去解决这些问题.
2.1 影响哪些设备?
经过测试, 一个app的targetSDK为34-时, 在任何设备上都不会被强制全屏.
但要是你的app升级targetSDK到了35+了, 那么:
- 在Android 14-的设备上, 不会全屏
- 在Android 15+的设备上, 会强制全屏.
- (这个其实有个后门, 在
第三章 最快的适配里会讲到)
- (这个其实有个后门, 在
这个意味着什么?
: 意味着, 要是我做了某些操作, 来让Android 15+适配此需求; 但同时我需要要让Android 14-不要受此影响 (因为Android 14-的设备没有强制全屏嘛!)
2.2 影响所有页面, 所有组件吗?
若你升级了targetSDK到35+, 那在Android 15+的设备上就会强制全屏.
不过, 有些细节要注意:
- 若你的页面本来就是全屏 (如很多app的splashActivity本身就是全屏, 或是video play页面本身也是全屏), 那它不受影响, 仍会是全屏的.
- 若你的页面本身没有全屏, 那就会被强制全屏.
所以问题的规模再次缩小: 我们需要让所有本来非全屏的页面来适配这个edgeToEdge的要求.
另外强制一下, 而这里要做的改动是涉及到: Activity, Fragment, BottomSheetDialogFragment, ... 这些所有组件的哦!
2.3 代码上如何实现?
其实现在你新建一个Android工程, 它的Activity会写上:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}
它这里的R.id.main就是整个Activity的rootView:
<ConstarintLayout android:id="@+id/main" ...>
<TextView ..../>
</ConstraintLayout>
这个代码已经就已经是我们的解决方案了. 其核心思想就是:
- 在时机合适时 (
OnApplyWindowInsetsListener)时, 给Activity中的内容加一个上下左右的padding, 这样就可以让我们app的页面不和上面的status bar重叠, 也不全下面的navigation zone重叠.
2.5 思路有了
那只要让Activity的rootView做一定的padding就行了, 相当于是:
- paddingTop = statusBar.height
- paddingBottom = navigationZone.height
那我们完全可以在BaseActivity中用:
class BaseActivity : AppCompatActivity() {
override fun onCreate(bundle) {
super.onCreate(bundle)
enableEdgeToEdge()
setContentView(...)
ViewCompat.setOnApplyWindowInsetsListener {
rootView.setPadding(...)
}
}
}
这个问题貌似这样简单一个设置就能成功让所有页面适配edgeToEdge了. 哎, 事情要是能这么简单, 那也算google良心了. 可偏偏google搞了好多事情, 让我们本来困难的适配工作变得更加艰难.
2.5 困难1: top方向
第一个困难就来自于Google设计人员的有限小脑以为所有页面都是非全屏的, 所以你上面的设置就没问题.
但上面第2.2小节说了, 原本已经全屏的页面是不受影响的. 你要是再加上个paddingTop, 那效果反而变成非全屏了.
所以具体到top的方向, 我们就要区分处理:
- Android 14- (不全屏)
- Andorid 15+的全屏页面 (不要做paddingTop)
- Android 15+的非全屏页面(要做paddingTop)
2.6 困难2: top方向
其实从上一个图中可以看到, 我们的第二个问题, 那就是我们把Activity的rootView加了一个paddingTop后, 就把statusBar的区域给空出来了, 但空出来的好怪, 成了一个透明色, 搞得时间, 电量, 这些信息都看不见了.
所以在top方向上, 我们要做的其实是:
- Android 14- (不全屏)
- Andorid 15+的全屏页面 (不要做paddingTop)
- Android 15+的非全屏页面
- (要做paddingTop)
- (还要给statusBar区域加上颜色)
2.7 困难3: bottom方向
联明的你肯定想到了bottom的方向上的困难也是类似的, 也要给navigation zone添加颜色 (加了paddingBottom后其背景色也成了空白了). 是的, 这样的想法是对的.
但其实更困难的点来自于Google的Material库.
implementation 'com.google.android.material:material:1.12.0'
比如说, 若你使用了Material库中的BottomNavigationView, 来看下效果:
是的, Google"贴心"地已经为Material库里做好了edgeToEdge的适配. 但这种所谓的"贴心"反而增加了我们的工作量, 因为我们本来就是想在BaseActivity中简单加上 paddingBottom = systembars.bottom, 现在发现不能这样了, 因为有了BottomNavigationView这样的, 底部paddingBottom应该为0了.
小总结下, 在bottom方向
Android 14-不用变化Android 15+ && 有BottomNavigationView/BottomAppBar: 这时paddingBottom应该为0Android 15+ && 没有BottomNavigationView/BottomAppBar: paddingBottom应该为systemBar.bottom
2.8 困难4: BottomSheet
当我们变化targetSDK为35后, bottomSheet也变了, 来看下示例:
我们的BottomSheet布局如下:
<LinearLayout orientation = "vertical">
<RecyclerView height="0dp" weight="1"/>
<Buttton height="40dp" />
</LinearLayout>
即是把Button先放到最下方, 然后RecyclerView再占据剩下的所有空间.
从上面的表格可以知道, And15+下,
- Navigation Zone倒是和BottomSheet没有重叠
- 但是BottomSheet的下方一大段内容 (测试约有110dp的样子)消失了.
所以我们要对Android 15+的BottomSheet也要特别处理一下.
2.9 小总结
综上所述,
- Google的Material库会自动适配, 导致我们要特别处理一下它;
- 同时要是已经全屏的页面, 也要处理下;
- 同时要注意下And 14-, And 15+有所不同, 要小心点
2.10 more...
好吧, 其实还有更多细节. 不过更多的细节问题只是小问题了, 我们后续会讲到, 并不影响我们这里的分类了.
三. 最快的适配 (有后患)
上面的分析已经大致了解了要分别处理哪几点, 但其实若你的时间紧, 来不及处理这么多, Google是开了个后门的. 你可以这样:
step 1.
找到application的theme
step 2.
给这个theme添加一个属性:
<item
name="android:windowOptOutEdgeToEdgeEnforcement"
tools:targetApi="35">true
</item>
这样一来, 你的页面即使在Android 15+设备上也不会强制全屏. 不过Google也说明了, 这个flag的设置只在Android 15有用, 到了Android 16上你就是设置了它为true, 也会被Android给忽略掉.
所以设置这个flag, 只能让你今年不去适配edgeToEdge, 但明年(2026年)8月底, 你还是要去适配edgeToEdge的. 即还是要走第四章 稳当的适配的各个步骤.
四. 稳当的适配 (无患)
4.1 升级Activityx库 (可选)
现在既然升级到了targetSDK = 35, 那一些Androidx库也可以升级了, 如:
implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.fragment:fragment-ktx:1.7.1"
这些库一般是和targetSDK绑定的, 你不升到某个版本还用不了高版本的activity-ktx库.
升级它的原因是我们需要Activity中的enableEdgeToEdge()方法, 低版本的activity-ktx里没有.
4.1.1 为何说它是可选
因为在我的测试过程中, 我发现enableEdgeToEdge()其实并不是必需的.
是的, 现在Google官网, Google的demo, 以及很多文章都在介绍:
- 你要要使用
enableEdgeToEdge()这个方法, - 再配合
ViewCompat.setOnApplyWindowInsetsListener(rootView) {v, insets -> ...}来调整padding 这就行了.
但我想了想, 其实它并不是必需的. 在我的多次测试下, 如何你没用调用enableEdgeToEdge()方法, 那么:
- Android 14-上页面也不全屏.
ViewCompat.setOnApplyWindowInsetsListener()这个回调不会被触发- 注意, 在And14-的全屏页, 这个listener会触发 !!!
- Android 15+上页面仍是全屏.
ViewCompat.setOnApplyWindowInsetsListener()这个回调仍会被触发
但要是你调用了enableEdgeToEdge(), 那么:
- Android 14-上页面就是全屏
- Android 15+上页面也是全屏.
- 在所有机型上,
ViewCompat.setOnApplyWindowInsetsListener()这个回调都被触发了
说到这里, 其实我们就明白了,
- Android 15+自动强制全屏, 即相当于为我们调了
enableEdgeToEdge() - Android 14-上不调用
enableEdgeToEdge()就不会是全屏.
=> 所以enableEdgeToEdge()是想在全平台上实现全屏效果.
我都哭死了, 这么恶心的全屏效果我才不想在全屏上实现呢. 我只想让Android 15+有这样的效果就行了. 所以我的适配方案是没有调用的enableEdgeToEdge()方法的!!
4.2 top方向上的调整
不调用enableEdgeToEdge(), 那如何调整top上的全屏呢?
我们来看几种情况:
- Android 14-: 因为没调用
enableEdgeToEdge(), 所以完全不用理会这种情况 - Android 15+ && 全屏页面: 就是升级到targetSDK = 35, 效果仍不变, 所以不用管
- Android 15+ && 非全屏页面: 这些页面我们都是有ActionBar/ToolBar/TopBar在上面的, 如下图所示:
这里有一个规律: 即非全屏页基本上全是有ActionBar/TopBar/ToolBar这样的头在top上的, 所以我们只要修改TopBar的源码(自己公司写的一个类)就能完美适配了:
假设我们的topBar原本调试是50dp, statusBar调试是34dp, 所以我们原来的dimen是: topbar_height = 50dp的.
现在我们修改下, 在topBar的layout xml里面, 给原来的topbar内容再新加一个View, 宽度为全屏, 高度为top_placeholder_height:
res/values/dimens.xml: 设置top_placeholder_height = 0dpres/values-v35/dimens.xml: 设置toolbar_height = 34dp
当然你若是担心statusBar高度不一定是34dp, 也可以在代码中得到后更新这个topbar的高度即可:
// BaseActivity
val isAndroid15 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM //VANILLA_ICE_CREAM是35
val hasTopBar = contentView.findViewById(R.id.topbar) != null
if (isAndroid15 && hasTopBar) {
topPlaceHolderView.updateLayoutParams<ViewGroup.LayoutParams> {
height = getStatusBarHeight()
}
}
小结:
- Android 14-与Android 15+的全屏页面可以不用管
- Android 15+的非全屏页面, 只要调整下actionBar的调试即可.
没有使用enableEdgeToEdge(), 也没有使用paddingTop = systembars.top, 只在有TopBar的地方加一个空view即可. 空View的高度则是根据Android版本不同而变化.
4.3 Bottom方向
4.3.1 处理paddingBottom
这个因为有些页面有BottomNavigationBar, 有些没有, 所以只好全局地设置了, 我们应该要在BaseActivity中设置:
override fun onStart() {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return // And14-的全屏页面也会触发这个listener, 所以没有必要. 我们只要处理And15+即可. 所以加上这句
val pageRootView = window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
ViewCompat.setOnApplyWindowInsetsListener(pageRootView) { v, insets ->
val hasBottomBar = pageRootView.findViewById<BottomNavigationView>(R.id.bottomBar) != null
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val paddingBottom = if(hasBottomBar) 0 else systemBars.bottom
v.updatePadding(systemBars.left, 0, systemBars.right, paddingBottom)
insets
}
}
-
说明1: BaseActivity还不知道每个子类页面是inflate了哪个layout XML, 所以我们要在onStart这种已经inflate完毕的时机里去调用这个
ViewCompat.setOnApplyWindowInsetsListener方法 -
说明2: 如上面所述, 有bottomNavigationView的地方, 我们就不要调整paddingBottom了, 所以有了上面的
val paddingBottom = if(hasBottomBar) 0 else systemBars.bottom的设置 -
说明3: Android 14-的设备, 因为没有调用
enableEdgeToEdge()方法, 所以这个ViewCompat.setOnApplyWindowInsetsListener回调根本不会被调用, 所以无须担心Android 14-的设备了. 它们的体验和以前一样的. -
说明4: 要是你的Activity里还有Fragment, 那可能就要注意用下面的代码来判断
val currentFragment = activity.supportFragmentManager.findFragmentById(container) //或findFragmentByTag
hasBottomBar = fragment.view?.findViewById(R.id.bottomBar)
效果如下:
4.3.2 处理OS navigation zone的颜色
明显从上面看到这个navigation zone的颜色不太对, 太透明了, 所以我们来修改下:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.navigationBarColor = getColor(R.color.purple)
}
效果如下:
备注: 要是有同学看其源码, 能发现这方法说And 15+上, 这个方法将无效
-> 不过, 我的实践证明这个方法在targetSDK = 35时仍可以用.
4.4 BottomSheet的处理
这里又要骂Google不当人子了, 你既然是所有页面都要app们去适配, 那为何不让开发们自己去做就好了? Google一定要去修改BottomNaivgationView, BottomSheet的代码, 导致我们app的开发工作量更大了.
这里也是, BottomSheet的处理更复杂, 原因就是BottomSheet来自于Google Material库.
4.4.1 他人的建议
文章来源于: 《Advanced Android Edge-ToEdge: BottomSheet》
这位作者的做法就是分两步
第一步, 修改theme
第二步, Google Material把BottomSheet的多个View都设置成了 fitsSystemWindows = true了, 我们要设置为false (And 14-就是false), 并做一定修改:
我试了下这些做法, 可以说, 效果有, 但并不完美, 我们app的页面看起来和And14-的效果不一样. 所以我最终没有采用方法.
(不过我仍然列出这篇文章在这里, 说不定其它同学会觉得它正好适合于你们app)
4.4.2 自己加paddingBottom
好在我们App中的BottomSheet并不多, 只有3个. 所以我的做法就是像给topBar加一个View做为paddingBottom. 这样反而效果和我们预期的一样.
不过仍是有坑, 那就是我得出来的systemBars.bottom约为46dp, 于是我给BottomSheet加了一个高度为46dp的空view
// sheet_carousel_info.xml
<LinearLayout orientation="vertical">
<RecyclerView height="0dp" weight="1">
<Button height="50dp"/>
<View height="@dimen/bottom_placeholder_height"/>
</LinearLayout>
这个bottom_placeholder_height在values-v35里设置为46dp, 却仍是让button给显示不出来.
我最终设置成110dp, button才成功显示出来, 我也是晕了.
4.4.3 BottomSheet的另一解法
上面4.4.2是手动添加padding, 适合那些App里BottomSheet不多的情况. 对于App里很多页面都是BottomSheet, 可以用这样的方法来做到:
val fragments = this.supportFragmentManager.fragments
if(fragments.size > 0) {
val frag = fragments[0]
if(frag is BottomSheetDialogFragment) {
bottomSheet.view?.updatePadding(systemBars.left, 0, systemBars.right, paddingBottom)
}
}
当然, 这其实和你们app里具体如何显示 BottomSheet有关. 所以我的这种全局解法算是一种抛砖引玉.
4.5 ConstraintLayout的坑
上面讲了, 在处理top, 以及BottomSheet时, 我们都采用了不同OS上有不同dimen的做法, 如:
values/dimens.xml: 0dpvalues-v35/dimens.xml: 34dp
但这个做法在碰到ConstraintLayout时就出问题了. 以top为例哦, 这样的设置后, 来看下效果:
即在And 15+上正常, 在And14-上却是top上的palceHolderView占据了整个屏幕.
原因就是在ConstraintLayout中, 0dp不再代表0高度, 而是代表根据约束来. 所以这就可能导致上面点满全屏的情况.
(再骂一句Google, 0dp就应该只有一种意义, 表示0高度; 而不是有2种意思, 导致了现在的问题)
解决办法其实也简单, 不过是我老婆想到的. 她说既然0dp不行, 那我设置为0.01dp总可以吧. 果然, 试了下, 真的行!
所以为了兼容ConstraintLayout, 当使用dimen方案时, 请这样设置:
values/dimens.xml: 0.01dpvalues-v35/dimens.xml: 34dp
这样就基本Okay了.
4.6 总结
- 针对top方向, 只要处理非全屏页面, 所以我们给topBar加了一个高度可变的topPlaceHolderView
- (全局地生效)
- 针对bottom方向, 要用Google推荐的设置paddingBottom的方法,
- (全局地生效)
- 只不过要注意 bottomNavigationView, 以及OS navigation zone的颜色
- 针对BottomSheet,
- 要么用我推荐的文章 (全局地生效)
- 要么用我的方法(手动地一个个地加bottomPlaceHolderView)
五. 其它的可能有影响的点
5.1 Material各版本的不同影响
根据Target-SDK 35的Behavior Change一节里说, 不同版本的Material Design有不同的行为, 这也会影响到我们的适配. 现摘录其中最关键的部分如下:
- 如果您的应用在 Compose 中使用 Material 3 组件 (
androidx.compose.material3),例如TopAppBar、BottomAppBar和NavigationBar,这些组件很可能不会受到影响,因为它们会自动处理边衬区。 - 如果您的应用使用的是 Compose 中的 Material 2 组件 (
androidx.compose.material),这些组件不会自动处理边衬区。不过,您可以获得边衬区的访问权限,然后手动应用边衬区。在 androidx.compose.material 1.6.0 及更高版本中,使用windowInsets参数可为BottomAppBar、TopAppBar、BottomNavigation和NavigationRail手动应用边衬区。 同样,请为Scaffold使用contentWindowInsets参数。 - 如果您的应用使用视图和 Material 组件 (
com.google.android.material),则大多数基于视图的 Material 组件(例如BottomNavigationView、BottomAppBar、NavigationRailView或NavigationView)都会处理边衬区,因此不需要执行额外的操作。不过,如果使用的是AppBarLayout,则需要添加android:fitsSystemWindows="true"。
所以请看准你使用的到底是哪个版本的material组件, 相应地进行处理. 我们的项目基本上是用的Material 组件 (com.google.android.material), 所以处理起来就是BottomNavigationView等已经处理好了padding, 我们只要在不同页面做不同的paddingBottom就行了.
5.2 Dark theme
若你的app还支持dark theme, 那你会发现BottomSheet的方案有问题, 在Android 15设备上paddingBottom过大了:
原因也是Google Material库的锅.
- Android认为, 又是values-v35, 又是dark theme的情况下, 因为
dimens(night).xml里没值, 所以那dimen的值就去values-v35中取, 自然paddingBottom这时就是100dp - 但是, Google Material库认为, 一个dark theme的BottomSheet, style应该走dark, 所以它的各个子view又不用得像是Android 14一样(即
fitsSystemWindows = false)
这二者一结合就冲突了. 但明显是Android的处理更正常.
正确的解决办法,
- 可以在
res/values-night/dimensx.xml里新加dimen值 - 但为了最精准匹配, 最好是新加一个
res/values-night-v35目录:
总结下来就是:
values/dimens.xml:bottomSheet_paddingBottom= 0.001dp
values-v35/dimens.xml:bottomSheet_paddingBottom= 110dp
values-night-v35/dimens.xml:bottomSheet_paddingBottom= 0.001dp
5.2.1 资源匹配规则
en语言类优先级最高 >
sw-xxdp等新屏幕大小体系 >
large等旧屏幕大小体系 >
port,land屏幕方向 >
night, notnight >
hdpi, xhdpi >
v35的版本区别
(出处: About app resources )
六. 番外: Compose体系的edgeToEdge
Compose上的处理其实取决于你的每个Composable页面是否是单独的Scaffold.
- 要是你是一个Activity, 里面自带Scaffold, Scaffold里带路由表, 那这个看似能全局性地修改padding, 但其实灵活性差
- 要是你是一个Activity, 里面直接带路由表. 然后每个Composable有自己的Scaffold, 这个看起来工具量大, 但其实灵活性好.
- 比如说, 你一个页面没有TopBar, 另一个页面有BottomBar, 再一个页面是完全全屏, ... , 像这些情况, 每个Composable有自己的Scaffold的方法, 就能更好地处理一个个的情形.
我个人项目中就是第二个方案, 仍是以top与bottom方向来分解. 但其实因为Compose里Scaffold自带了padding值, 不用我们去写什么ViewCompat.setOnApplyWindowInsetsListener(), 所以其实是更容易了
6.1 ComposeHostActivity
这个Activity跟前面的第一到五章不一样, 它是调用了enableEdgeToEdge()的.
class ComposeHostActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() //<= <= 注意, 这里enable了的!!
setContent {
MyProjectTheme {
val router = rememberNavController()
NavHost(navController = router, startDestination = "home") {
appSubRoute(router)
}
}
}
}
}
6.2 top方向
6.2.1 全屏页
top方向上的全屏, 其实就是指页面会侵入到系统status bar的区域嘛, 这个其实就是不在scaffold中加入topPadding就能自动做到. 也就是这样的代码:
Scaffold(..) { paddings -> // 故意不处理这个paddings
Column() { //页面的主体内容
6.2.2 有topBar, 并要处理padding
这里的问题其实就是, 当topBar有topPadding后, 原来status bar的地方会不会变成透明的, 看起来好像空出来一在块?
: 实践发现 "不会空出来一块", 反而是这个TopBar会自动占据原来statusBar的区域, 这正是我们想要的. (这一点比View体系的edgeToEdge适配要简单)
而且请注意, And14-与And15+上, 行为是一致的(另一个好消息). 所以我们只要这样处理:
Scaffold(modifier = Modifier.fillMaxSize(),
topBar = { TopBar("非全屏页", router)}) { paddings ->
Column(modifier = Modifier.fillMaxSize().padding(paddings)) { // 主体内容
备注: 我的TopBar其实就是CenterAlignedTopAppBar:
@OptIn(ExperimentalMaterial3Api::class)
@Composable fun TopBar(
title: String, router: NavHostController
) {
CenterAlignedTopAppBar(title = {Text(title)},
....
6.2.3 最终效果
6.3 Bottom方向
6.3.1 若没有NavigationBar
Scaffold(modifier = Modifier.fillMaxSize(),
topBar = { TopBar("非全屏页", router)}) { paddings ->
Column(modifier = Modifier.fillMaxSize().padding(paddings)) { // 主体内容
6.3.2 若有NavigationBar
注意, 这个NavigationBar是在Scaffold里, 不在content里, 所以我们的content得为这个Scaffold里的NavigationBar留出空间来显示 (不然content最下方的一部分内容会被NavigationBar给遮挡住)
Scaffold(modifier = Modifier.fillMaxSize(),
bottomBar = { BottomBar() }) { paddings ->
Column(modifier = Modifier.fillMaxSize()
.padding(bottom = paddings.calculateBottomPadding())) { // 主体内容
6.3.3 最终效果
七. 结语
其实Google早就有"开发, 我是你爹"的感觉了. 早在targetSDK升31时, 就已经有过类似事件了. 当时Google根本不管你app是否有splash, 它自己就是强行加一个splash. 结果你不适配就会变成有2个splash页. 这种一拍脑袋就要干, 完全不给开发自己选择是否跟进的选择很让人反感. 不过当时适配splash的工作量较小, 我也没有十分在意. 现在则不同了, edgeToEdge的改动太大了, 真的让人如鲠在喉了.
从上面的篇幅, 可以看出我在这上面花费了多少时间. Google这种强制app更新UI的方式很恶心, 偏偏它的Material库又给我们增加了很多障碍. 好在现在大体完工了. 多谢你的阅读~
后述
后续有人质疑文中的多种细节方案是否合适, 为何不按Google推荐的方式来适配. 这里也解释一下
1). 上述方案通过了我们QA与PO的验收, 现在项目已经在2025年7月上线, And15+上UI不再突兀了. 算是本方案基本是可行的.
2). 我最初也是按Google推荐的方法搞的, 但奈何问题多多啊, 全屏页有问题, BottomSheet有问题, BottomNavigationView有问题, statusBar空出一起有问题, Fragment有问题, .... 我也只能见招拆招, 一个个地解决, 这才总结出了本方案
3). 为何And14-上没强制 enableEdgeToEdge()?
: 其实是因为And14-完全不受影响 (除了4.1.1小节的一小部分), 所以完全没有必要去动And14-的部分. 这样一来, 我开发自测只要测试And15+即可, QA测试也只要测试And15+也行. 而且 And14-下的页面几乎不受影响 , 大多数的我们app的用户不受影响, 更稳定一些.
当然, 事情解决方案并不是只有我这一种, 要是你强烈不适, 就想And14-也用edgeToEdge也行, 那就要处理上面的各种问题. 但也不失为另一种方向 .
版本说明
v1: 最初版本, 外加一些语言上的小修改.v2: BottomSheet的处理, 新加一个全局适配的说明 (4.4.3小节)v3: bottom方向的处理上, 新加了 Activity中的Fragment的use case的说明. 主要是现在用Activity来findViewById去找BottomNavigationView已经不靠谱了. (4.3.1小节)v4: 新加And14-上 OnApplyWindowInsetListener的说明 (4.1.1小节)v5: 新加了第六章, 即"Compose上edgeToEdge的处理"v6: 5.1小节重写. 原来的5.1小节来自老婆经验, 我没有处理过, 怕有误会, 我还是重写了5.1小节. 这次是引入了Google的官方说明, 说明不同版本的material design要如何处理.v7: 5.2小节重新加入resource匹配规则, 更加详细了
参考资料
1). developer.android.com/about/versi…
2). developer.android.com/develop/ui/…
4). 老婆的各种实践
5). 我自己的各种实践