[targetSDK升级为35] 恶心的EdgeToEdge适配

开发多年, 碰到不少恶心需求与恶心甲方了. 但这次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应该为0
  • Android 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-的设备了. 它们的体验和以前一样的.

效果如下:

明显从上面看到这个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_heightvalues-v35里设置为46dp, 却仍是让button给显示不出来.

我最终设置成110dp, button才成功显示出来, 我也是晕了.

4.5 ConstraintLayout的坑

上面讲了, 在处理top, 以及BottomSheet时, 我们都采用了不同OS上有不同dimen的做法, 如:

  • values/dimens.xml : 0dp
  • values-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.01dp
  • values-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/...

3). BottomSheet与EdgeToEdge

4). 老婆的各种实践

5). 我自己的各种实践

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android