Android 15 全屏模式适配:A15TopView 自定义组件分享
背景
target 35 全屏模式兼容
Android 15 (API level 35) 引入了重要的行为变更,特别是针对 Window Insets 的强制边到边显示策略。根据 Google 的要求,开发者需要在 2025年8月31日之前更新目标 API 级别,以便继续发布应用更新。
边到边显示强制执行
从 Android 15 开始,系统将强制执行边到边显示行为,这意味着:
- 应用内容将延伸到系统栏区域
- 状态栏和导航栏变为透明或半透明
- 开发者需要主动处理系统窗口插入 (Window Insets)
这一变更直接影响了应用的 UI 布局,特别是顶部区域容易出现内容被状态栏遮挡或背景空白的问题。 为了解决这个适配痛点,开发 A15TopView
组件。
功能特性
主要特点
- 适配成本较低,只需要调整布局文件即可,无需代码额外适配
- 自动同步原本顶部状态栏背景变化,可见等状态变更
1. 顶部预留 StatusBar 高度
检测系统版本和配置,为顶部添加状态栏高度的 padding:
kotlin
if (needFitSystem || Build.VERSION.SDK_INT >= 35) {
setPadding(0, StatusBarUtil.getStatusBarHeight(context), 0, 0)
}
确保 Android 15 edge_to_edge
模式下,内容不会被状态栏遮挡。
2. 自适应背景色,代码动态设置背景也可以生效
背景同步机制,会自动将子 View 的背景同步到父容器,填充状态栏区域:
kotlin
private fun syncBackgroundFromChild() {
if (childCount > 0) {
val firstChild = getChildAt(0)
val currentBackground = findValidBackground(firstChild) ?: return
if (currentBackground is ColorDrawable) {
// 处理颜色背景
if (currentBackground.color != lastChildBackground) {
lastChildBackground = currentBackground.color
this.setBackgroundColor(lastChildBackground!!)
}
} else {
// 处理 Drawable 背景
if (currentBackground != lastChildDrawable) {
lastChildDrawable = currentBackground
this.background = currentBackground
}
}
}
}
背景查找策略:
- 优先使用子 View 的直接背景
- 如果子 View 是 ViewGroup,会递归查找其子 View 的背景
- 只同步满足条件的 View 背景(高度匹配、可见、不透明等)
- 支持运行时动态背景变更
3. 兼容 fitsSystemWindows 属性
支持标准的 fitsSystemWindows
属性,同时提供自定义配置:
kotlin
val needFitSystem = ta.getBoolean(R.styleable.A15TopView_fitsSystemWindows, false)
val needAsyncBgColor = ta.getBoolean(R.styleable.A15TopView_asyncBgColor, true)
注意: 如果你的布局中已经设置了 fitsSystemWindows="true"
,需要先移除原有设置,然后在 A15TopView 上配置。
4. 使用简单,布局文件包裹即可
只需要用 A15TopView 包裹你的原有布局,无需修改现有代码逻辑:
xml
<com.yourpackage.A15TopView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:asyncBgColor="true">
<!-- 原有的布局内容 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@color/primary_color">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题栏" />
</LinearLayout>
</com.yourpackage.A15TopView>
技术要点
Drawable 所有权处理
Android 中一个 Drawable 同时只能被一个 View 使用。当父 View 设置了子 View 的 background 后:
- 子 View 失去对这个 background 的"所有权"
- 父 View 获得这个 background 并正常显示
- 子 View 的背景变成透明
因此对 ColorDrawable 使用 setBackgroundColor()
而不是直接设置 drawable,避免背景抢占问题。
背景同步条件
kotlin
private fun shouldSyncChildBackground(child: View): Boolean {
// 高度检查
if (!isHeightSufficient(child)) return false
// 可见性检查
if (child.visibility != View.VISIBLE) return false
// 透明度检查
if (child.alpha < 0.5f) return false
return true
}
自定义属性
app:fitsSystemWindows
:是否启用系统窗口适配app:asyncBgColor
:是否启用背景同步功能
完整示例
kotlin
class A15TopView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
private var lastChildBackground: Int? = null
private var lastChildDrawable: Drawable? = null
private val needAsyncBgColor: Boolean
init {
val ta = getContext().obtainStyledAttributes(attrs, R.styleable.A15TopView)
val needFitSystem = ta.getBoolean(R.styleable.A15TopView_fitsSystemWindows, false)
needAsyncBgColor = ta.getBoolean(R.styleable.A15TopView_asyncBgColor, true)
ta.recycle()
if (needFitSystem || Build.VERSION.SDK_INT >= 35) {
setPadding(0, StatusBarUtil.getStatusBarHeight(context), 0, 0)
}
}
private fun syncVisible() {
if (childCount > 0) {
val firstChild = getChildAt(0)
if (firstChild.visibility != visibility) {
visibility = firstChild.visibility
}
}
}
private fun syncBackgroundFromChild() {
if (!needAsyncBgColor) {
return
}
if (childCount > 0) {
val firstChild = getChildAt(0)
// 综合高度检查
if (!shouldSyncChildBackground(firstChild)) {
return
}
val currentBackground = findValidBackground(firstChild) ?: return
if (currentBackground is ColorDrawable) {
// colorDeawable 不应该抢占,因此 设置颜色值
if (currentBackground.color != lastChildBackground) {
lastChildBackground = currentBackground.color
this.setBackgroundColor(lastChildBackground!!)
}
} else {
if (currentBackground != lastChildDrawable) {
/**
* Android中的Drawable在同一时间只能被一个View使用。当父View设置了子View的background后:
*
* 子View失去了对这个background的"所有权"
* 父View获得了这个background,所以父View显示正常
* 子View的背景变成了透明或默认状态
*
*/
lastChildDrawable = currentBackground
this.background = currentBackground
}
}
}
}
private fun shouldSyncChildBackground(child: View): Boolean {
// 1. 可见性检查
if (child.visibility != View.VISIBLE) {
return false
}
// 2. 基础高度检查
if (!isHeightSufficient(child)) {
return false
}
// 3. 透明度检查
if (child.alpha < 0.5f) {
return false
}
return true
}
private fun isHeightSufficient(child: View): Boolean {
return child.height == measuredHeight - StatusBarUtil.getStatusBarHeight(context)
}
private fun findValidBackground(child: View): Drawable? {
// 优先使用直接背景
child.background?.let { return it }
// 如果是ViewGroup,递归查找
if (child is ViewGroup) {
for (i in 0 until child.childCount) {
val innerChild = child.getChildAt(i)
// 只查找同样满足高度要求的子View
if (shouldSyncChildBackground(innerChild)) {
findValidBackground(innerChild)?.let { return it }
}
}
}
return null
}
override fun onDescendantInvalidated(child: View, target: View) {
super.onDescendantInvalidated(child, target)
syncBackgroundFromChild()
syncVisible()
}
override fun requestLayout() {
super.requestLayout()
syncVisible()
}
}
适用场景
- 需要适配 Android 15 边到边显示的应用
- 全屏应用的顶部区域适配
- 需要状态栏背景延伸的导航栏
- 2025年8月31日前的 API 35 目标适配
时间紧迫性
⚠️ 重要提醒 :Google 要求开发者在 2025年8月31日之前更新目标 API 级别至 35,以便继续发布应用更新。使用 A15TopView 可以帮助快速完成 Android 15 的适配工作,确保应用在新系统上的正常显示。
通过这个组件,可以轻松解决 Android 15 边到边模式下状态栏区域的背景适配问题,让开发更加简单高效。
参考资料: