开发多年, 碰到不少恶心需求与恶心甲方了. 但这次edgeToEdge真是刷新了我对google下限的想像.
事情是Google要求8月底前, 所有play store上的app升级targetSDK为35 (政策链接是这个: developer.android.com/google/play..., 目前只说2024年8月底前到34; 但按google每年的规律, 今年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. 很多新东西也越来越倾向于大厂的开发/经理为了自己的KAP生硬地搞新东西, 而不是想这些新东西能解决什么难受的痛点.
二. 着手做之前的分析
虽然这个需求让人很无语, 吐槽完后做还是肯定要做的. 不过我们有必要在编码之前, 先分解下这个需求, 以及分析下它到底影响了哪些部分, 这样我们才能一步一步地去解决这些问题.
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会写上:
kotlin
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:
xml
<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中用:
kotlin
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库.
arduino
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布局如下:
xml
<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添加一个属性:
xml
<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库也可以升级了, 如:
gradle
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()
这个回调不会被触发
- 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原本调试是50dp, statusBar调试是34dp, 所以我们原来的dimen是: topbar_height = 50dp
的.
现在我们修改下, 在topBar上新加一个View, 宽度为全屏, 高度为top_placeholder_height
:
res/values/dimens.xml
: 设置top_placeholder_height = 0dp
res/values-v35/dimens.xml
: 设置toolbar_height = 34dp
当然你若是担心statusBar高度不一定是34dp, 也可以在代码中得到后更新这个topbar的高度即可:
kotlin
// 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中设置:
kotlin
override fun onStart() {
val pageRootView = window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
val hasBottomBar = pageRootView.findViewById<BottomNavigationView>(R.id.bottomBar) != null
ViewCompat.setOnApplyWindowInsetsListener(pageRootView) { v, insets ->
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.3.2 处理OS navigation zone的颜色
明显从上面看到这个navigation zone的颜色不太对, 太透明了, 所以我们来修改下:
kotlin
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
xml
// 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.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 Material2的theme
我没有碰到这种情况(因为我用的是material1的theme), 但老婆碰到了. 老婆说她们公司用的是Material2的theme, 这时要适配edgeToEdge, 就得升级到Material3的theme.
但熟悉的朋友一定发现了, 改整个Application的theme是个非常危险的操作, 意味着可能每个页面都有所变化, 可能你要修改每个页面的UI了.
具体方法可能只能升级到material3页面, 然后一个页面一个页面地修改material theme升级带来的影响了.
5.2 Dark theme
若你的app还支持dark theme, 那你会发现BottomSheet的方案有问题, 在Android 15设备上paddingBottom过大了:

原因也是Google Material库的锅.
- Android认为, 又是values-v35, 又是dark theme的情况下, 那dimen的值就去
values-v35
中取, 自然paddingBottom这时就是100dp - 但是, Google Material库认为, 一个dark theme的BottomSheet, style应该走dark, 所以它的各个子view又不用得像是Android 14一样(即
fitsSystemWindows = false
)
这二者一结合就冲突了. 但明显是Android的处理更正常.
正确的解决办法, 新加一个res/values-night-v35
目录:

总结下来就是:
values/dimens.xml
:bottomSheet_paddingBottom
= 0.001dp
values-v35/dimens.xml
:bottomSheet_paddingBottom
= 110dp
values-night-v34/dimens.xml
:bottomSheet_paddingBottom
= 0.001dp
六. 结语
从上面的篇幅, 可以看出我在这上面花费了多少时间. Google这种强制app更新UI的方式很恶心, 偏偏它的Material库又给我们增加了很多障碍. 好在现在大体完工了. 多谢你的阅读~
参考资料
1). developer.android.com/about/versi...
2). developer.android.com/develop/ui/...
4). 老婆的各种实践
5). 我自己的各种实践