Compose 中的系统区域适配

在移动应用开发中,界面元素常常需要避开系统关键区域(如状态栏、导航栏)和临时出现的交互区域(如输入法),以确保内容完整可见且交互流畅。Compose 提供了较多便捷的 Modifier 方法,帮助开发者轻松实现这类适配需求,无需手动计算复杂的边距值。

常用系统区域 Padding Modifier

  1. Modifier.statusBarsPadding()

    为组件顶部添加与状态栏(StatusBar)匹配的内边距,避免内容被顶部状态栏遮挡。

  2. Modifier.navigationBarsPadding()

    为组件添加与导航栏(NavigationBar)匹配的内边距,适配虚拟导航键或全面屏手势区域。

  3. Modifier.imePadding()

    当输入法(IME)弹出时,为组件底部添加与输入法高度匹配的内边距,确保输入框或底部按钮不会被输入法遮挡。

使用示例:

kotlin 复制代码
@Composable
fun ImePaddingDemo(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .fillMaxWidth()
            // 适配状态栏和导航栏,确保整体布局避开系统固定区域
            .statusBarsPadding()
            .navigationBarsPadding()
            // 添加自定义内边距,调整内容与边缘的距离
            .padding(horizontal = 30.dp, vertical = 20.dp)
            // 添加输入法适配,确保输入法弹出时内容上移不被遮挡
            .imePadding(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 输入框
        TextField(
            value = "",
            onValueChange = {}
        )
        Spacer(modifier = Modifier.weight(1f))
        Button(
            onClick = {},
            modifier = Modifier
                .fillMaxWidth()
                .height(48.dp)
        ) {
            Text(text = "确定")
        }
    }
}

注意事项

使用 imePadding() 时,需在 AndroidManifest.xml 中为对应 Activity 添加配置,否则在 Android 11(API 30)以下设备可能无法生效:

xml 复制代码
<activity
    android:name=".YourActivity"
    android:windowSoftInputMode="adjustResize"  <!-- 关键配置 -->
    ... >
</activity>

效果对比

通过上述适配,可在不同系统版本中获得一致的体验: 左边是 Android 15, 右边是 Android 10 。

源码分析

statusBarsPadding()、navigationBarsPadding()、imePadding() 都是通过 windowInsetsPadding() 来实现的。

以状态栏为例,代码如下:

kotlin 复制代码
actual fun Modifier.statusBarsPadding() =
    windowInsetsPadding(debugInspectorInfo { name = "statusBarsPadding" } ) { statusBars } 
kotlin 复制代码
@Suppress("NOTHING_TO_INLINE")
@Stable
private inline fun Modifier.windowInsetsPadding(
    noinline inspectorInfo: InspectorInfo.() -> Unit,
    crossinline insetsCalculation: WindowInsetsHolder.() -> WindowInsets,
): Modifier =
    composed(inspectorInfo) {
        val composeInsets = WindowInsetsHolder.current()
        remember(composeInsets) {
            val insets = composeInsets.insetsCalculation()
            InsetsPaddingModifier(insets)
        }
    }

WindowInsetsHolder.current()

WindowInsetsHolder 是 Compose 内部维护的一个"当前窗口 Insets 提供者",里面有 statusBars / navigationBars / ime 等各种 insets。

kotlin 复制代码
@Composable
fun current(): WindowInsetsHolder {
    val view = LocalView.current
    val insets = getOrCreateFor(view)

    DisposableEffect(insets) {
        insets.incrementAccessors(view)
        onDispose { insets.decrementAccessors(view) }
    }
    return insets
}

/**
 * Returns the [WindowInsetsHolder] associated with [view] or creates one and associates it.
 */
private fun getOrCreateFor(view: View): WindowInsetsHolder {
    return synchronized(viewMap) {
        viewMap.getOrPut(view) {
            val insets = null
            WindowInsetsHolder(insets, view)
        }
    }
}

LocalView.current 用于在 Compose 中获取当前组合树所关联的 AndroidComposeView。通过该 View 进一步调用 getOrCreateFor() 方法,获取 WindowInsetsHolder 对象。

核心就是 incrementAccessors 方法,首次访问时为 View 注册 InsetsListener 监听器,代码如下:

kotlin 复制代码
fun incrementAccessors(view: View) {
    if (accessCount == 0) {
        // add listeners
        ViewCompat.setOnApplyWindowInsetsListener(view, insetsListener)

        if (view.isAttachedToWindow) {
            view.requestApplyInsets()
        }
        view.addOnAttachStateChangeListener(insetsListener)

        ViewCompat.setWindowInsetsAnimationCallback(view, insetsListener)
    }
    accessCount++
}
kotlin 复制代码
private class InsetsListener(val composeInsets: WindowInsetsHolder) :
    WindowInsetsAnimationCompat.Callback(
        if (composeInsets.consumes) DISPATCH_MODE_STOP else DISPATCH_MODE_CONTINUE_ON_SUBTREE
    ),
    Runnable,
    OnApplyWindowInsetsListener,
    OnAttachStateChangeListener {
    

    override fun onProgress(
        insets: WindowInsetsCompat,
        runningAnimations: MutableList<WindowInsetsAnimationCompat>,
    ): WindowInsetsCompat {
        composeInsets.update(insets)
        return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
    }


    override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        // Keep track of the most recent insets we've seen, to ensure onEnd will always use the
        // most recently acquired insets
        savedInsets = insets
        composeInsets.updateImeAnimationTarget(insets)
        if (prepared) {
            // There may be no callback on R if the animation is canceled after onPrepare(),
            // so we won't know if the onPrepare() was canceled or if this is an
            // onApplyWindowInsets() after the cancelation. We'll just post the value
            // and if it is still preparing then we just use the value.
            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
                view.post(this)
            }
        } else if (!runningAnimation) {
            // If an animation is running, rely on onProgress() to update the insets
            // On APIs less than 30 where the IME animation is backported, this avoids reporting
            // the final insets for a frame while the animation is running.
            composeInsets.updateImeAnimationSource(insets)
            composeInsets.update(insets)
        }
        return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
    }

    // ...
}

InsetsListener 持有 WindowInsetsHolder(composeInsets),在两类场景触发更新:

  • onProgress:软键盘动画状态下更新 Insets
  • onApplyWindowInsets:非动画状态下更新 Insets
kotlin 复制代码
 fun update(windowInsets: WindowInsetsCompat, types: Int = 0) {
     ime.update(insets, types)
     navigationBars.update(insets, types)
     statusBars.update(insets, types)
     // ...
}

WindowInsetsHolder 的 update 方法会将原始 WindowInsetsCompat 分别更新输入法、导航栏、状态栏等子区域的 Insets 信息,为上层适配提供数据支持。

InsetsPaddingModifier(insets)

它同时做了三件事:

  1. LayoutModifier ------ 在 layout 阶段"增加 padding"
  2. ModifierLocalConsumer ------ 读取上游已经"消费过"的 insets
  3. ModifierLocalProvider ------ 把"已消费的 insets"继续向下游传递

所以它既是 Insets 的消费者 ,也是 Insets 消费情况的提供者。代码如下:

kotlin 复制代码
internal val ModifierLocalConsumedWindowInsets = modifierLocalOf { WindowInsets(0, 0, 0, 0) }

internal class InsetsPaddingModifier(private val insets: WindowInsets) :
    LayoutModifier, ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
    private var unconsumedInsets: WindowInsets by mutableStateOf(insets)
    private var consumedInsets: WindowInsets by mutableStateOf(insets)

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val left = unconsumedInsets.getLeft(this, layoutDirection)
        val top = unconsumedInsets.getTop(this)
        val right = unconsumedInsets.getRight(this, layoutDirection)
        val bottom = unconsumedInsets.getBottom(this)

        val horizontal = left + right
        val vertical = top + bottom

        val childConstraints = constraints.offset(-horizontal, -vertical)
        val placeable = measurable.measure(childConstraints)

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) { placeable.place(left, top) }
    }

    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
        with(scope) {
            val consumed = ModifierLocalConsumedWindowInsets.current
            unconsumedInsets = insets.exclude(consumed)
            consumedInsets = consumed.union(insets)
        }
    }

    override val key: ProvidableModifierLocal<WindowInsets>
        get() = ModifierLocalConsumedWindowInsets

    override val value: WindowInsets
        get() = consumedInsets

    // ...
}
  1. 计算 padding

    • unconsumedInsets:当前这个 Modifier 还需要处理的 Insets(已经剔掉上游消费过的部分)。
    • getLeft/getTop/getRight/getBottom:根据当前 MeasureScope(包含 density)和 layoutDirection,将 WindowInsets 转换成各方向的像素值。
  2. 测量子项:缩小约束(相当于预留 padding 空间)

    kotlin 复制代码
    val childConstraints = constraints.offset(-horizontal, -vertical)
    val placeable = measurable.measure(childConstraints)

    测量子组件时,会先把 padding 占用的空间从父约束中扣除,让子组件只在"扣除 padding 后的剩余空间"中测量自己。这样保证子组件不会占用 padding 区域,也不会被压缩或变形。

  3. 布局自身:扩展尺寸 + 偏移子项

    kotlin 复制代码
    val width = constraints.constrainWidth(placeable.width + horizontal)
    val height = constraints.constrainHeight(placeable.height + vertical)
    return layout(width, height) { placeable.place(left, top) }
    • 整体大小 = 内容尺寸 + padding
    • 子项放置在 (left, top),也就是"把内容往右下平移",形成 padding 效果。
  4. Insets 消费机制:ModifierLocalConsumer + ModifierLocalProvider

    这是这段代码最有意思、也最容易忽略的部分。

    kotlin 复制代码
    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
        with(scope) {
            val consumed = ModifierLocalConsumedWindowInsets.current
            unconsumedInsets = insets.exclude(consumed)
            consumedInsets = consumed.union(insets)
        }
    }
    
    override val key: ProvidableModifierLocal<WindowInsets>
        get() = ModifierLocalConsumedWindowInsets
    
    override val value: WindowInsets
        get() = consumedInsets

    这段代码负责什么?

    • 读取:上游 Modifier 已经消费过哪些 WindowInsets

    • 决定:当前 Modifier 还需要处理哪些 Insets(unconsumedInsets)

    • 更新:新的"到目前为止已消费集合"(consumedInsets = consumed.union(insets))

    • 暴露:把 consumedInsets 提供给下游 Modifier,作为 ModifierLocalConsumedWindowInsets.current 的新值

    为什么要"消费"?

    考虑以下场景:

    kotlin 复制代码
    @Composable
    fun TestDemo() {
        Column(
            Modifier
                .fillMaxSize()
                .statusBarsPadding()
                .background(Color.Gray)
        ) {
            Box(
                Modifier
                    .fillMaxSize()
                    .statusBarsPadding()
                    .background(Color.Red)
            )
        }
    }

    如果不做"消费"机制:

    • 外层 Column 会根据 statusBar Insets 加一层 paddingTop

    • 内层 Box 也会再加一层同样的 paddingTop

    现在有了 ModifierLocal 的"消费"机制:

    • 外层 statusBarsPadding():

      • 上游 consumed = 0
      • 自己的 insets = statusBars
      • unconsumedInsets = statusBars.exclude(0) = statusBars
      • 测量时使用完整 statusBar 高度做 padding
      • 计算 consumedInsets = 0.union(statusBars) = statusBars
      • 往下游提供:ModifierLocalConsumedWindowInsets = statusBars
    • 内层 statusBarsPadding():

      • 读取到上游 consumed = statusBars
      • 自己的 insets = statusBars
      • unconsumedInsets = statusBars.exclude(statusBars) = 0
      • 测量时 → padding 为 0(不会重复顶一次)
      • consumedInsets = statusBars union statusBars = statusBars(不变)

    结果:
    即使你在嵌套结构中多次使用 statusBarsPadding(),只有最外层那一次会真正产生 padding,内部的自然"忽略"掉。

    这就是 exclude / union 的用处。

相关推荐
SkyQvQ2 小时前
Android Studio 开发效率神器:Auto-import
android·android studio
q***72192 小时前
Y20030018基于Java+Springboot+mysql+jsp+layui的家政服务系统的设计与实现 源代码 文档
android·前端·后端
Code Warrior3 小时前
【MySQL数据库】数据类型
android·数据库·mysql
a***13144 小时前
python的sql解析库-sqlparse
android·前端·后端
r***86984 小时前
mysql的主从配置
android·mysql·adb
.豆鲨包4 小时前
【Android】深入理解Activity的生命周期和IntentFilter
android·java
啃火龙果的兔子4 小时前
安卓从零开始
android
CryptoRzz4 小时前
印度股票数据 PHP 对接文档 覆盖 BSE(孟买证券交易所)和 NSE(印度国家证券交易所)的实时数据
android·服务器·开发语言·区块链·php
安卓蓝牙Vincent5 小时前
Android多SDK合并为单个JAR包的完整指南
android