Android 15 全屏模式适配:A15TopView 自定义组件分享

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 边到边模式下状态栏区域的背景适配问题,让开发更加简单高效。


参考资料:

相关推荐
_祝你今天愉快36 分钟前
Java-JVM探析
android·java·jvm
飞天卡兹克1 小时前
forceStop流程会把对应进程的pendingIntent给cancel掉
android
Monkey-旭9 小时前
Android Bitmap 完全指南:从基础到高级优化
android·java·人工智能·计算机视觉·kotlin·位图·bitmap
Mike_Wuzy14 小时前
【Android】发展历程
android
开酒不喝车14 小时前
安卓Gradle总结
android
阿华的代码王国15 小时前
【Android】PopupWindow实现长按菜单
android·xml·java·前端·后端
静默的小猫16 小时前
LiveDataBus消息事件总线之二-(不含反射和hook)
android
~央千澈~17 小时前
05百融云策略引擎项目交付-laravel实战完整交付定义常量分文件配置-独立建立lib类处理-成功导出pdf-优雅草卓伊凡
android·laravel·软件开发·金融策略
_一条咸鱼_17 小时前
Android Runtime冷启动与热启动差异源码级分析(99)
android·面试·android jetpack