上一篇 下一篇 回到顶部 目录 返回首页
目录

Jetpack Compose 布局系统全解析:从入门到自适应

发表于
更新于
14 53.4~68.7 分钟 24028

前言

Jetpack Compose 的布局系统采用声明式、修饰符驱动的架构,彻底告别了传统 Android View 系统中嵌套层级带来的性能问题。本文将系统梳理 Compose 布局的完整知识体系:从基础的 Column/Row/Box,到懒列表、Pager、Flow,再到响应式自适应布局和 Material 3 标准范式。


第一部分:布局基础

1.1 三大基础布局

Compose 提供了三个最基本的布局原语:

组件

方向

用途

Column

垂直

从上到下堆叠子元素

Row

水平

从左到右排列子元素

Box

Z 轴

将子元素层叠在一起

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

1.2 单次测量布局模型

Compose 的布局树在一次高效的遍历中完成:

  1. 向下测量:父元素将尺寸约束传递给子元素

  2. 向上报告:叶子节点报告解析后的尺寸

  3. 放置:父元素计算自身大小并放置子元素

关键原则:父元素先测量子元素,但在子元素之后才被 sizing 和 placed。

这种单次测量模型允许 UI 深度嵌套而不带来传统 View 系统的性能惩罚。

1.3 修饰符(Modifiers)

修饰符是用于装饰或增强可组合函数的标准 Kotlin 对象。它们可以:

  • 改变组件的尺寸、布局、行为和外观

  • 添加无障碍标签或处理用户输入

  • 启用高级交互(可点击、可滚动、可拖拽等)

最佳实践:每个可组合函数都应接受 modifier 参数并传递给第一个发射 UI 的子元素:

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(24.dp)) {
        Text(name)
    }
}

1.4 修饰符的顺序至关重要

修饰符链的顺序是确定性的,每个函数都会修改前一个函数的结果:

// 可点击区域包含 padding
Modifier.clickable { }.padding(16.dp)

// 可点击区域不包含 padding
Modifier.padding(16.dp).clickable { }

1.5 作用域安全

Compose 通过作用域接口限制某些修饰符只能在特定父元素中使用:

  • BoxScope:允许 matchParentSize(子元素匹配父 Box 尺寸但不撑大它)

  • RowScope / ColumnScope:允许 weight(按比例分配可用空间)

Row {
    Image(modifier = Modifier.weight(2f))  // 占 2/3 宽度
    Text(modifier = Modifier.weight(1f))   // 占 1/3 宽度
}

1.6 提取和复用修饰符

为了提升性能,将复杂的修饰符链提取为变量,避免在频繁重组时重复分配对象:

// 在 Compose 外部定义,分配只发生一次
val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

@Composable
fun MyComponent() {
    Text("Hello", modifier = reusableModifier)  // 复用,无新分配
}

可以使用 .then() 扩展已提取的修饰符:

reusableModifier.then(Modifier.clickable { /*...*/ })

第二部分:修饰符详解

2.1 影响约束的关键修饰符

修饰符

行为

size()

适应指定尺寸,但遵守传入的最小/最大约束

requiredSize()

覆盖传入约束,传递精确约束

width() / height()

固定一个维度,另一个保持灵活

sizeIn()

设置精确的最小/最大宽高

fillMaxSize()

将最小约束设为最大可用空间

wrapContentSize()

重置最小约束,允许子元素比可用空间小

2.2 经典陷阱:fillMaxSize + size

Modifier.fillMaxSize().size(50.dp)
// ❌ 结果:图片填满容器。size(50.dp) 被忽略。
// 原因:fillMaxSize() 将最小约束设为最大。size() 必须遵守这些约束。

修复方式 — 使用 wrapContentSize() 重置最小约束:

Modifier.fillMaxSize().wrapContentSize().size(50.dp)
// ✅ 结果:50dp 的图片居中在容器中。

2.2 修饰符分类速查

操作类(Actions)

  • clickablecombinedClickable(支持长按/双击)

  • draggableanchoredDraggableswipeable

  • toggleabletriStateToggleable

对齐类(Alignment)

  • align:在 Row/Column/Box 中对齐子元素

  • alignBy / alignByBaseline:沿自定义线或文字基线对齐兄弟元素

动画类(Animation)

  • animateBounds:在 LookaheadScope 中动画位置/尺寸变化

  • animateEnterExit:覆盖 AnimatedVisibility 子元素的进出过渡

  • animateItem:自动动画 LazyList 中的增删和重排

边框与绘制(Border & Drawing)

  • borderclipclipToBounds

  • alphazIndexbackgroundshadowdropShadow

  • drawBehinddrawWithContentdrawWithCache

焦点(Focus)

  • onFocusChanged / onFocusEvent:焦点变化回调


第三部分:布局容器

3.1 懒列表和懒网格(LazyList / LazyGrid)

懒布局只组合和放置当前视口中的可见项,是 Compose 版的 RecyclerView

组件

方向

用途

LazyColumn / LazyRow

垂直 / 水平

标准滚动列表

LazyVerticalGrid / LazyHorizontalGrid

垂直 / 水平

统一网格布局

LazyVerticalStaggeredGrid / LazyHorizontalStaggeredGrid

垂直 / 水平

不同高度/宽度的瀑布流网格

基础用法

LazyColumn {
    item { Text("单个 Header") }
    items(messages) { message -> MessageRow(message) }  // 接受 List<T>
    items(5) { index -> Text("Item $index") }           // 接受数量
    item { Text("Footer") }
}

网格尺寸

// 固定 2 列
LazyVerticalGrid(columns = GridCells.Fixed(2)) { /*...*/ }

// 自适应列数,最小 128dp
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) { /*...*/ }

自定义跨度

LazyVerticalGrid(columns = GridCells.Adaptive(30.dp)) {
    item(span = { GridItemSpan(maxLineSpan) }) { CategoryCard("Header") }
}

状态管理

val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()

LazyColumn(state = listState) { /* ... */ }

// 响应滚动(使用 derivedStateOf 避免不必要的重组)
val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }

// 编程式滚动
coroutineScope.launch { listState.animateScrollToItem(index = 0) }

关键性能要点

  1. 始终提供稳定的 keyitems(messages, key = { it.id })

  2. 使用 contentTypeitems(items, contentType = { it.type }) 让 Compose 在相同结构的项之间复用组合

  3. stickyHeader:固定分组标题

  4. 避免 0 像素项:始终提供默认尺寸或占位符

  5. 不要嵌套同方向的可滚动容器

  6. 使用 Modifier.animateItem() 平滑动画增删和重排

3.2 Pager(分页器)

HorizontalPagerVerticalPager 替代了传统的 ViewPager。页面是懒组合的,只有可见/附近页面才会被渲染。

val pagerState = rememberPagerState(pageCount = { 10 })

HorizontalPager(state = pagerState) { page ->
    Text("Page: $page", modifier = Modifier.fillMaxWidth())
}

核心 API

功能

用法

编程式翻页

pagerState.scrollToPage() / animateScrollToPage()

状态追踪

currentPagesettledPagetargetPage

预加载相邻页

beyondBoundsPageCount > 0

页面尺寸

pageSize = PageSize.Fixed(100.dp) 或自定义

内容内边距

contentPadding = PaddingValues(horizontal = 32.dp)

自定义滚动

flingBehavior = PagerDefaults.flingBehavior(...)

页码指示器

Row(horizontalArrangement = Arrangement.Center) {
    repeat(pagerState.pageCount) { i ->
        Box(
            Modifier
                .size(16.dp)
                .clip(CircleShape)
                .background(if (pagerState.currentPage == i) Color.DarkGray else Color.LightGray)
        )
    }
}

基于滚动的变换效果(如淡出非中心页):

val pageOffset = ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue
Modifier.graphicsLayer {
    alpha = lerp(0.5f, 1f, 1f - pageOffset.coerceIn(0f, 1f))
}

3.3 Flow 布局

FlowRowFlowColumn 类似 Row/Column,但会在空间不足时自动换行。适合标签、筛选 chips 等响应式 UI。

FlowRow(modifier = Modifier.padding(8.dp)) {
    ChipItem("Price: High to Low")
    ChipItem("Avg rating: 4+")
    ChipItem("Free breakfast")
    ChipItem("Free cancellation")
    ChipItem("£50 pn")
}

核心特性

特性

说明

主轴排列

horizontalArrangement(FlowRow)/ verticalArrangement(FlowColumn)

交叉轴对齐

整个流容器的内容对齐

单行对齐

子元素用 Modifier.align() 在当前行内对齐

每行最大项数

maxItemsInEachRow / maxItemsInEachColumn 强制换行

每行权重

Modifier.weight() 只在当前行内计算

分数尺寸

Modifier.fillMaxWidth(fraction) 相对整个容器

等宽尺寸

fillMaxColumnWidth() / fillMaxRowHeight() 匹配行内最大项

用 FlowRow 做响应式网格

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 3
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .weight(1f)  // 均分行内剩余空间
        .clip(RoundedCornerShape(8.dp))
        .background(Material3.Blue200)

    repeat(9) { Spacer(modifier = itemModifier) }
}

3.4 Grid(实验性)

Grid 是 Compose 的实验性二维布局组件,用于构建灵活的自适应网格,适合结构性页面布局而非滚动内容。

组件

最佳场景

懒加载?

LazyVerticalGrid

大型同质数据集

Row / Column

一维布局

Grid

结构性 2D 布局和复杂组件

基础用法

Grid(
    config = {
        repeat(2) { column(160.dp) }
        repeat(3) { row(90.dp) }
    }
) {
    Card1(); Card2(); Card3()  // 自动放入单元格
}

轨道尺寸类型

类型

用法

说明

固定

column(160.dp)

精确尺寸

百分比

row(0.5f)

占可用空间的 0~100%

弹性

row(1.fr)

固定/百分比轨道之后分配剩余空间

内在

column(GridTrackSize.Auto)

基于内容:.MaxContent.MinContent.Auto

间距

config = {
    rowGap(16.dp)
    columnGap(8.dp)
    // 或简写:gap(8.dp)
}

放置方向

config = {
    flow = GridFlow.Column  // 先从上到下填充,再向右
}

GridItem 修饰符 — 控制单个项的位置、跨度和对齐:

// 精确单元格
Item(modifier = Modifier.gridItem(row = 2, column = 2))

// 从末尾计数
Item(modifier = Modifier.gridItem(row = -1, column = -2))

// 跨 2 行 2 列
Item(modifier = Modifier.gridItem(rowSpan = 2, columnSpan = 2))

// 在分配的区域内居中
Text("#1", modifier = Modifier.gridItem(rowSpan = 2, columnSpan = 2, alignment = Alignment.Center))

自动放置的项会跳过已被占用的单元格,显式和自动放置可以自由混合。

3.5 FlexBox(实验性)

FlexBox 是实验性的 Compose 容器,受 CSS Flexible Box 规范启发,自动调整、换行、对齐和分配空间。

何时使用:适合小数量、需要伸缩换行的布局项。大型数据集用懒列表,全屏布局用 Grid,简单不换行的流用 FlowRow/FlowColumn

@OptIn(ExperimentalFlexBoxApi::class)
FlexBox(
    config = {
        direction(FlexDirection.Row)
        wrap(FlexWrap.Wrap)
        alignItems(FlexAlignItems.Center)
        justifyContent(FlexJustifyContent.SpaceBetween)
        gap(8.dp)
    }
) {
    // 固定基础尺寸 + 均分剩余空间
    Text("Flexible", Modifier.flex { basis(100.dp); grow(1f) })

    // 2 倍速度增长
    Text("Heavy Grower", Modifier.flex { grow(2f); shrink(1f) })

    // 静态项
    Text("Fixed", Modifier.flex { basis(50.dp) })
}

容器行为

属性

用途

direction

主轴方向(Row/Column/Reverse)

wrap

换行模式(NoWrap/Wrap/WrapReverse)

justifyContent

主轴上的项和剩余空间分配

alignItems

单行内交叉轴对齐

alignContent

多行交叉轴对齐(仅换行时生效)

项行为Modifier.flex):

属性

说明

basis

初始/首选尺寸(Auto/固定 Dp/百分比 Float)

grow

分配剩余空间的比例(默认 0)

shrink

分配空间不足时的收缩比例(默认 1f)

alignSelf

覆盖容器 alignItems

order

视觉排序(不改变代码结构)


第四部分:自适应布局

4.1 核心理念

自适应布局的核心原则:关注应用窗口大小,而非设备屏幕大小。Compose 通过声明式重组和运行时窗口指标,用单一代码库优雅适配手机、平板、折叠屏和桌面窗口。

4.2 Window Size Classes(窗口尺寸类)

窗口尺寸类将可用视口分为标准化断点(CompactMediumExpandedLargeExtra-Large),用于驱动响应式布局决策。

断点参考

类别

宽度断点

高度断点

Compact

< 600dp

< 480dp

Medium

600dp ~ 840dp

480dp ~ 900dp

Expanded

840dp ~ 1200dp

≥ 900dp

Large

1200dp ~ 1600dp

N/A

Extra-Large

≥ 1600dp

N/A

实现

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass =
        currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).windowSizeClass
) {
    when (windowSizeClass.widthSizeClass) {
        WindowWidthSizeClass.Compact -> MobileLayout()
        WindowWidthSizeClass.Medium -> TabletLayout()
        WindowWidthSizeClass.Expanded -> DesktopLayout()
    }

    // 垂直空间紧张时隐藏顶栏
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(
        WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND
    )
}

关键特性:

  • 设备无关:基于可用窗口空间计算,不是物理屏幕尺寸

  • 动态变化:运行时可变(旋转、多窗口、折叠/展开)

  • 宽度驱动:大多数布局决策由宽度类驱动

4.3 支持不同显示尺寸

应用级决策 — Window Size Class

@Composable
fun MyApp(windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass) {
    // 集中布局决策
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(...)
    MyScreen(showTopAppBar = showTopAppBar)
}

组件级决策 — BoxWithConstraints

@Composable
fun Card(imageUrl: String, title: String, description: String) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column { Image(imageUrl); Title(title) }  // 紧凑布局
        } else {
            Row {
                Column { Title(title); Description(description) }
                Image(imageUrl)
            }
        }
    }
}

最佳实践

  • 顶层用 WindowSizeClass 路由布局

  • 组件级用 BoxWithConstraints 或自定义布局适配

  • 预先传入所有数据,避免按尺寸条件加载数据

  • 提升状态以在调整大小/旋转时保持状态不丢失

  • 依赖窗口分配指标,而非物理屏幕尺寸

4.4 MediaQuery(实验性)

mediaQuery 抽象了动态设备/上下文查询,自动在查询值变化时触发重组。

启用(Application 类):

class MyApplication : Application() {
    override fun onCreate() {
        ComposeUiFlags.isMediaQueryIntegrationEnabled = true
        super.onCreate()
    }
}

可用参数UiMediaScope):

参数

类型

说明

windowWidth / windowHeight

Dp

当前窗口尺寸

windowPosture

Posture

设备姿态(Flat/Tabletop/Book)

pointerPrecision

PointerPrecision

输入精度(Fine/Coarse/Blunt/None)

keyboardKind

KeyboardKind

键盘类型(Physical/Virtual/None)

hasCamera / hasMicrophone

Boolean

硬件支持

viewingDistance

ViewingDistance

预期用户距离(Near/Medium/Far)

用法

// 不频繁变化的状态用 mediaQuery
if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) {
    TabletopLayout()
}

// 窗口尺寸等频繁变化用 derivedMediaQuery
val isNarrow by derivedMediaQuery {
    windowWidth < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND.dp
}

4.5 标准布局模式

List-Detail 布局

使用 NavigableListDetailPaneScaffold 实现,大屏并排显示,小屏单 Pane 切换。

NavigableListDetailPaneScaffold(
    navigator = navigator,
    listPane = {
        AnimatedPane {
            MyList(onItemClick = { item ->
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, item)
            })
        }
    },
    detailPane = {
        val selected = navigator.currentDestination?.contentKey
        AnimatedPane {
            if (selected != null) MyDetail(item = selected)
        }
    }
)

Supporting Pane 布局

主内容搭配上下文信息,大屏并排(约 70/30),中屏等分,小屏支撑 Pane 移到底部。

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = { AnimatedPane { /* 主内容 */ } },
    supportingPane = { AnimatedPane { /* 辅助内容 */ } }
)

自适应导航

NavigationSuiteScaffold 根据窗口大小自动在底部导航栏和侧边导航轨之间切换。

NavigationSuiteScaffold(
    navigationSuiteItems = {
        AppDestinations.entries.forEach { dest ->
            item(
                icon = { Icon(dest.icon, contentDescription = null) },
                label = { Text(stringResource(dest.label)) },
                selected = dest == currentDestination,
                onClick = { currentDestination = dest }
            )
        }
    }
) {
    when (currentDestination) {
        AppDestinations.HOME -> HomeScreen()
        AppDestinations.FAVORITES -> FavoritesScreen()
    }
}

4.6 多窗口支持

Manifest 配置

<application android:resizeableActivity="true" />
<activity
    android:name=".MainActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />

独占资源管理(API 29+):

override fun onTopResumedActivityChanged(topResumed: Boolean) {
    super.onTopResumedActivityChanged(topResumed)
    if (topResumed) {
        // 重新获取相机、麦克风等
    } else {
        // 释放或暂停独占资源
    }
}

4.7 桌面窗口化(Android 15+)

自定义标题栏

window.insetsController?.setSystemBarsAppearance(
    WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND,
    WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
)

@Composable
fun CaptionBar() {
    if (WindowInsets.isCaptionBarVisible) {
        Row(
            modifier = Modifier
                .windowInsetsTopHeight(WindowInsets.captionBar)
                .fillMaxWidth()
                .background(if (isSystemInDarkTheme()) Color.White else Color.Black),
            horizontalArrangement = Arrangement.Center
        ) {
            Text("Caption Bar Title", style = MaterialTheme.typography.titleMedium)
        }
    }
}

多实例支持

<application>
    <property
        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
        android:value="true" />
</application>

4.8 外接显示器支持

  • 使用 LocalConfiguration.currentLocalDensity.current 获取显示指标(自动触发重组)

  • 不要硬编码像素值 — 外接显示器密度和尺寸差异很大

  • 使用 rememberSaveable 或 ViewModel 保存状态

  • 运行时查询外接显示器,而非设备型号白名单

4.9 Android 16 方向与可调整性变更

API 36+ 且在最小宽度 ≥ 600dp 的设备上:

  • android:screenOrientation 的 portrait/landscape 等值被忽略

  • android:resizeableActivity 所有值被忽略

  • 游戏和 < 600dp 设备除外

  • API 37 将移除退出机制

4.10 自适应 Do’s and Don’ts

领域

要做

不要

可调整性

resizeableActivity="true"

设为 false 或排除多窗口

方向

移除 screenOrientation

锁定横竖屏

宽高比

移除 min/maxAspectRatio

硬编码比例限制

尺寸

currentWindowAdaptiveInfo()

使用废弃的 Display API

组件

用 M3 Adaptive 脚手架

重新发明面板/导航逻辑

输入

支持键鼠/触控笔

假设只有触控


第五部分:精确布局工具

5.1 对齐线(Alignment Lines)

对齐线是可组合函数暴露给父元素的参考坐标,用于精确的跨子元素对齐(如文字基线对齐、图表值标注)。

内置对齐线Text 自动暴露 FirstBaselineLastBaseline

使用 paddingFrom 做基线内边距

Modifier.paddingFrom(FirstBaseline, top = 32.dp)

自定义对齐线

// y 在 Compose 中向下增长,视觉上"最大" = 更小的 y 坐标
val MaxChartValue = HorizontalAlignmentLine { old, new -> min(old, new) }
val MinChartValue = HorizontalAlignmentLine { old, new -> max(old, new) }

对齐线值自动传播到直接和间接父元素,祖父级及以上的祖先也可以读取和对齐。

5.2 内部测量(Intrinsic Measurements)

内部测量允许在正式测量阶段之前查询可组合函数的理想尺寸,使父元素能够基于子元素内容进行 sizing。

Row(modifier = Modifier.height(IntrinsicSize.Min)) {
    Text(text = text1, modifier = Modifier.weight(1f))
    VerticalDivider(modifier = Modifier.fillMaxHeight().width(1.dp))
    Text(text = text2, modifier = Modifier.weight(1f))
}

流程:Row 查询 minIntrinsicHeight → Text 返回单行高度 → Divider 内在高度为 0 → Row 高度 = 最高的 Text → Divider 通过 fillMaxHeight() 展开匹配。

关键点:

  • 内部测量在测量之前查询,不会触发第二次测量

  • height(IntrinsicSize.Min)递归的,向下传播到组件树

  • 自定义布局中应重写内部测量以获得精确结果

5.3 可见性追踪

onVisibilityChangedonLayoutRectChanged 用于追踪 UI 元素何时在屏幕上可见。

Text(
    text = "Sample Text",
    modifier = Modifier
        .onVisibilityChanged { visible ->
            if (visible) { /* 触发分析或副作用 */ }
        }
        .padding(vertical = 8.dp)  // padding 放在可见性追踪之后
)

关键参数

  • minDurationMs:连续可见指定时长后才触发回调

  • minFractionVisible:设置可见面积的最小占比(0.0 ~ 1.0)


第六部分:性能最佳实践

  1. 避免 SELECT * — 只查询需要的列(对应 Compose:只重组需要的部分)

  2. 避免嵌套同方向可滚动容器 — 用 LazyList DSL 的 item/items/stickyHeader 统一处理

  3. 每个 item {} 块限制元素数量 — 多个独立可组合函数作为单个实体处理会降低性能

  4. 使用 contentType — 让 Compose 只在相同结构类型的项之间复用组合

  5. 提供稳定的 keyitems(messages, key = { it.id }) 保持状态在数据变化时不丢失

  6. 提取修饰符 — 在 Compose 外部定义复杂修饰符链,避免重复分配

  7. 使用 BoxWithConstraints 做局部约束决策,而非全局窗口指标


结语

Jetpack Compose 的布局系统可以概括为:

  1. 入门:掌握 Column/Row/Box、Modifier 链和修饰符顺序

  2. 进阶:熟练使用 LazyList、Pager、FlowLayout、Grid 和 FlexBox

  3. 高级:掌握 Window Size Classes、Material 3 自适应脚手架、MediaQuery

  4. 精通:理解内部测量、对齐线、可见性追踪和自定义布局

建议从创建一个练习项目开始,逐一实现本文中的布局模式,感受 Compose 声明式布局带来的开发效率提升。