在移动应用开发中,界面元素常常需要避开系统关键区域(如状态栏、导航栏)和临时出现的交互区域(如输入法),以确保内容完整可见且交互流畅。Compose 提供了较多便捷的 Modifier 方法,帮助开发者轻松实现这类适配需求,无需手动计算复杂的边距值。
常用系统区域 Padding Modifier
-
Modifier.statusBarsPadding()为组件顶部添加与状态栏(StatusBar)匹配的内边距,避免内容被顶部状态栏遮挡。
-
Modifier.navigationBarsPadding()为组件添加与导航栏(NavigationBar)匹配的内边距,适配虚拟导航键或全面屏手势区域。
-
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)
它同时做了三件事:
- LayoutModifier ------ 在 layout 阶段"增加 padding"
- ModifierLocalConsumer ------ 读取上游已经"消费过"的 insets
- 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
// ...
}
-
计算 padding
- unconsumedInsets:当前这个 Modifier 还需要处理的 Insets(已经剔掉上游消费过的部分)。
- getLeft/getTop/getRight/getBottom:根据当前 MeasureScope(包含 density)和 layoutDirection,将 WindowInsets 转换成各方向的像素值。
-
测量子项:缩小约束(相当于预留 padding 空间)
kotlinval childConstraints = constraints.offset(-horizontal, -vertical) val placeable = measurable.measure(childConstraints)测量子组件时,会先把 padding 占用的空间从父约束中扣除,让子组件只在"扣除 padding 后的剩余空间"中测量自己。这样保证子组件不会占用 padding 区域,也不会被压缩或变形。
-
布局自身:扩展尺寸 + 偏移子项
kotlinval 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 效果。
-
Insets 消费机制:ModifierLocalConsumer + ModifierLocalProvider
这是这段代码最有意思、也最容易忽略的部分。
kotlinoverride 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 的用处。
-