Jetpack Compose 自定义布局
Jetpack Compose 系列第 15 篇。Jetpack Compose 系列文章写到现在入门的知识点已经都分享给了大家后续想继续分享一些进阶但未来开发中必定会遇到的知识。比如今天要分享的是——使用 Jetpack Compose 实现自定义布局。一、前言当 Jetpack Compose 内置的布局Column、Row、Box无法满足需求时根据在传统 Android View 时代的开发经验一般就要进行自定义布局——自定义ViewGroup而在 Jetpack Compose 时代一个Layout组件解决所有。本文会先回忆 Android View 时代自定义布局的老套路、看清它的痛点再介绍 Compose 的Layout包含“测量–定尺寸–摆放”三步法最后带来两个实战布局自动换行FlowLayout、带间距的SpacedColumn并聊聊Modifier.layout和进阶注意事项。二、回忆旧时代——ViewGroup 自定义布局在传统 Android View 开发中要实现自定义布局一般按照下面的步骤进行创建继承ViewGroup的类如MyFrameLayout.java重写onMeasure()确定当前ViewGroup和所有子View的尺寸解析MeasureSpec的三种模式EXACTLY/AT_MOST/UNSPECIFIED重写onLayout()确定所有子View的屏幕位置child.layout(left,top,right,bottom)在XML中使用完整类名引用添加自定义属性res/values/attrs.xml这个套路的缺点十分明显——代码量大而且几乎都是样板代码。哪怕你只是想实现一个简单的布局效果几百行代码都是必须要的。即使后来采用kotlin编程语言进行开发代码量减少约30%的前提下对于一个功能丰富的自定义布局类依旧有着复杂的计算逻辑和代码量。更致命的是View 体系中允许父 View 多次调用子 View 的measure()如LinearLayout中的weight会测量两次这无疑对性能有很大影响。Jetpack Compose 的到来给了我们新的选择它用一个Layout来解决一切。在 Jetpack Compose 中不需要考虑onMeasure和onLayout的调用时机、测量模式的分发规则、各个 child 与 parent 之间的约束只需要遵照一个核心原则即可——你给我子项和约束我把它们量好、摆好、显示好。三、认识核心——LayoutLayout是一个 Composable可组合项它把“测量child 摆放child”的全部逻辑封装进一个MeasurePolicy里。官方提供的布局组件Column、Row和Box等底层其实都是基于Layout来实现的。那么就来认识一下Layout函数ComposableinlinefunLayout(content:ComposableUiComposable()-Unit,modifier:ModifierModifier,measurePolicy:MeasurePolicy,)Layout函数中三个参数含义解释❗注意重点理解 content measurePolicy 的概念content子 Composable 的内容modifier修饰符measurePolicy核心它定义了如何测量和放置子元素MeasurePolicy是一个接口其关键方法截取如下StableJvmDefaultWithCompatibilityfuninterfaceMeasurePolicy{funMeasureScope.measure(measurables:ListMeasurable,constraints:Constraints):MeasureResult}measurables所有需要测量的子元素一个List结构类型constraints父布局对当前布局的尺寸约束minWidth/minHeight、maxWidth/maxHeight返回值 MeasureResult包含布局自身尺寸和子元素的放置位置来看一个最小示例——所有子项叠在一起的MyStack即类似官方提供的Box组件效果ComposablefunMyStack(modifier:ModifierModifier,content:Composable()-Unit){// ⭐Layout是一个Composable函数Layout(modifiermodifier,contentcontent){measurables,constraints-// 1. 测量把每个子项测量成 Placeablevalplaceablesmeasurables.map{it.measure(constraints)}// 2. 计算容器尺寸取所有子项的最大宽高valwidthplaceables.maxOfOrNull{it.width}?:0valheightplaceables.maxOfOrNull{it.height}?:0// 3. 摆放先 layout() 提交尺寸再逐个 placelayout(width,height){placeables.forEach{placeable-placeable.placeRelative(0,0)// 全部堆叠在左上角}}}}// 使用方式与内置布局没有区别ComposablefunMyStackDemo(){MyStack{Text(第一层,Modifier.background(Color.Red))Text(第二层,Modifier.background(Color.Green))Text(第三层,Modifier.background(Color.Blue))}}很简单的示例代码我们再用表格来对比下巩固对Compose里布局逻辑的理解Android View 体系Jetpack Compose职责onMeasure()measurables.map{it.measure(constraints)}测量子项MeasureSpecConstraints父级尺寸约束onLayout()placeable.placeRelative(x,y)摆放childLayoutParamsModifierchild对父布局的诉求使用placeRelative()而不是place()前者支持在RTL从右到左的布局下自动镜像坐标是官方推荐的写法后者不会跟随RTL翻转。四、理解 Constraints 约束上面的最小示例代码MyStack.kt中Layout()函数最后一个参数即measurePolicy的 lambda 表达式参数是measurables,constraints -测量阶段得到的constraints是一个Constraints对象它的职责就是告诉布局“你可以有多大”。其函数方法如下classConstraints(valminWidth:Int,valmaxWidth:Int,valminHeight:Int,valmaxHeight:Int)它只有四个值minWidth / maxWidth父级允许的最小/最大宽度minHeight / maxHeight父级允许的最小/最大高度它就相当于 Android View 中的MeasureSpec但Constraints不需要去解析MeasureSpec的 mode 和 Size ,它直观的的告诉布局最小和最大的宽高父布局根据自己的约束和布局意图如fillMaxWidth()会把maxWidth确定下来计算出每个子项的约束范围。一个关键原则单次测量Compose 规定每个子元素在一个布局传递中只能被测量一次如果对一个Measurable重复调用measure()会直接抛出异常。Compose 的这个原则从根本上“消灭”了 Android View 时代重复测量带来的性能隐患。// ❌ 错误多次测量同一个 measurablevalplaceable1measurable.measure(constraints1)valplaceable2measurable.measure(constraints2)// ✅ 正确每个 measurable 只测量一次valplaceablemeasurable.measure(constraints)// 然后使用这个 placeable 的尺寸来做布局决策五、认识 Modifier.layout有时候我们并不想花大力气完全重写组件行为而是只想修改某个组件的某一个行为比如——一个Box的宽度永远是父布局可用宽度的一半且高度自适应。类似这样的需求可以考虑使用Modifier.layout它在单个节点的测量与摆放层面做手脚。// 自定义修饰符宽度强制为父约束最大宽度的 50%funModifier.halfWidth()layout{measurable,constraints-// 1. 获取父布局的最大宽度valmaxWidthconstraints.maxWidth// 2. 计算新宽度取一半但不能小于最小宽度限制valnewWidth(maxWidth*0.5f).toInt().coerceAtLeast(constraints.minWidth)// 3. 创建新的约束仅修改宽度高度保持不变valnewConstraintsconstraints.copy(minWidthnewWidth,maxWidthnewWidth)// 4. 用新约束重新测量子组件valplaceablemeasurable.measure(newConstraints)// 5. 以测量后的尺寸提交布局layout(placeable.width,placeable.height){placeable.place(0,0)}}Box(modifierModifier.fillMaxWidth()){// 父容器撑满全屏Text(text我只有屏幕一半宽,modifierModifier.halfWidth().background(Color.Yellow))}Modifier.layout本质上是给节点加了一层自定义布局它有权修改传给子节点的约束也有权决定最终尺寸与摆放位置。(回忆之前的文章《Jetpack Compose Modifier 修饰符完全指南从入门到静通》里提到的“Modifier 顺序影响效果”)什么时候用Layout什么时候用Modifier.layout可以根据下表判断场景API选用需要同时摆放多个子项如瀑布流、FlexboxLayout{…}只需要微调某一个按钮、图标或文字的尺寸/位置/对齐方式Modifier.layout{…}六、实战1——手写FlowLayoutCompose 自定义布局概念和步骤清楚后下面就是经典实战环节——实现一个自动换行的流式布局FlowLayout——搜索历史标签、偏好设置那种效果子项从左到右、从上到下依次排列当前行放不下就换行。代码如下ComposablefunMyFlowLayout(modifier:ModifierModifier,horizontalGap:Dp8.dp,verticalGap:Dp8.dp,content:Composable()-Unit){Layout(modifiermodifier,contentcontent){measurables,constraints-valhGaphorizontalGap.roundToPx()//valvGapverticalGap.roundToPx()// 1. 测量所有子项把最小约束削减为 0防止子项被强迫撑满valplaceablesmeasurables.map{measurable-measurable.measure(constraints.copy(minWidth0,minHeight0))}// 2. 模拟逐行摆放计算出容器总宽高varx0vary0varrowHeight0vartotalWidth0vartotalHeight0for(placeableinplaceables){// 当前行放不下换行if(xplaceable.widthconstraints.maxWidth){x0yrowHeightvGap rowHeight0}totalWidthmaxOf(totalWidth,xplaceable.width)totalHeightmaxOf(totalHeight,yplaceable.height)rowHeightmaxOf(rowHeight,placeable.height)xplaceable.widthhGap}// 3. 提交尺寸真正摆放换行逻辑与第 2 步一致layout(totalWidth,totalHeight){x0y0rowHeight0for(placeableinplaceables){if(xplaceable.widthconstraints.maxWidth){x0yrowHeightvGap rowHeight0}placeable.placeRelative(x,y)// ⭐真正摆放rowHeightmaxOf(rowHeight,placeable.height)xplaceable.widthhGap}}}}ComposablefunMyFlowDemo(){MyFlowLayout(Modifier.padding(16.dp)){repeat(8){index-Text(text标签$index,modifierModifier.padding(4.dp).background(Color(0xFFE3F2FD),RoundedCornerShape(8.dp)).padding(horizontal10.dp,vertical6.dp))}}}调用方式也很简单classCustomViewActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)enableEdgeToEdge()setContent{KnownAndroidTheme{Scaffold(modifierModifier.fillMaxSize()){innerPadding-Column(modifierModifier.padding(innerPadding)){MyFlowDemo()}}}}}}效果如下七、实战2——带间距的Column再写一个更实用的自定义布局——一个支持自定义子元素间距的Column。ComposablefunSpacedColumn(modifier:ModifierModifier,verticalSpacing:Dp0.dp,horizontalAlignment:Alignment.HorizontalAlignment.Start,content:Composable()-Unit){Layout(modifiermodifier,contentcontent){measurables,constraints-valspacingPxverticalSpacing.roundToPx()// 测量所有子元素valplaceablesmeasurables.map{measurable-measurable.measure(constraints)}// 计算总高度valtotalHeightplaceables.fold(0){acc,placeable-accplaceable.heightspacingPx}-spacingPx// 找到最大宽度valmaxChildWidthplaceables.maxOfOrNull{it.width}?:0layout(widthconstraints.maxWidth,heighttotalHeight.coerceAtMost(constraints.maxHeight)){vary0placeables.forEach{placeable-valxwhen(horizontalAlignment){Alignment.Start-0Alignment.CenterHorizontally-(maxChildWidth-placeable.width)/2Alignment.End-maxChildWidth-placeable.widthelse-0}placeable.place(x,y)yplaceable.heightspacingPx}}}}ComposablefunSpacedColumnDemo(){SpacedColumn(verticalSpacing16.dp,horizontalAlignmentAlignment.CenterHorizontally){repeat(5){index-Box(modifierModifier.size(80.dp).background(Color.hsl(hueindex*60f,saturation0.7f,lightness0.5f),shapeRoundedCornerShape(8.dp))){Text(text${index1},colorColor.White,fontSize24.sp,modifierModifier.align(Alignment.Center))}}}}运行效果如下八、进阶和注意事项8.1 更强大的 SubcomposeLayoutLayout的局限在于测量之前所有子项必须已经组合完成。有些场景需要根据约束条件再去组合子项——比如BoxWithConstraints、LazyColumn的底层实现。这就轮到SubcomposeLayout出场了它能在测量阶段动态决定要组合哪些内容代价是逻辑更复杂、性能开销更大。它的工作方式很有意思在测量阶段拿到约束后再调用subcompose()按需组合出子项相当于把组合也变成了布局流程的一部分。感兴趣的读者朋友可以自己去官方了解本文不展开讲了。8.2 自定义布局的约定写自定义布局时有些约定要记得不要重复测量同一个 Measurable 在一个测量阶段里只能 measure() 一次重复调用直接抛异常。尊重最小约束父级给的 minWidth / minHeight 不要轻易忽略让内容决定尺寸当约束允许时容器尺寸应由子项测量结果推导避免在 measure 中读取 State在measurelambda 中读取了State当状态变化时会触发整个布局的重新测量使用remember缓存计算结果布局计算涉及复杂逻辑可以用remember缓存中间结果九、总结在自定义布局这个主题路线下文章介绍了 Android View 的弊端介绍了Compose用核心API——Layout如何完成自定义布局measure、layout、placeRelative还帮助理解constraints约束强调了单次测量最后介绍了Modifier.layout处理单个节点和带来两个实战示例。要强调的是在 Compose 中自定义布局的核心是Layout这个 Composable 函数。它把测量和放置合并到了一个 Lambda 表达式里——测量完立刻放置逻辑连贯一目了然。也要提醒自定义布局是“最后的手段不是第一选择”理解了机制、能看懂官方源码远比造一个轮子更有价值。十、参考资料https://developer.android.google.cn/develop/ui/compose/layouts/custom?hlzh-cnhttps://developer.android.google.cn/develop/ui/compose/layouts/custom?hlzh-cn#create-customhttps://googledownloads.cn/cn-devsite/l6rAoph5UgI.mp4https://github.com/android/compose-sampleshttps://gatti.dev/en/posts/compose-custom-layout/https://juejin.cn/post/6963262309699420173?searchId20260811130855F3D711244ADC0F3D22A3十一、往期系列文章Jetpack Compose Navigation 3.0 完全指南Jetpack Compose 使用Navigation实现导航Jetpack Compose 主题与样式Jetpack Compose 手势处理揭秘写给新手的 Jetpack Compose 动画手册Jetpack Compose 副作用Jetpack Compose 重组机制Jetpack Compose 状态管理指南Jetpack Compose 组件大观园Jetpack Compose 常用UI组件实战演练Jetpack Compose Modifier 修饰符完全指南从入门到精通Jetpack Compose 核心机制Composable与ModifierJetpack Compose 入门指南Jetpack Compose 前探

相关新闻

uC/OS-III RTOS入门:从LED闪烁任务理解多任务调度与移植

uC/OS-III RTOS入门:从LED闪烁任务理解多任务调度与移植

1. 从裸机到RTOS:点亮LED的思维跃迁很多嵌入式新手在学完单片机基础后,第一个项目往往是“点灯”。在裸机环境下,这很简单:配置GPIO,拉高或拉低电平,一个while(1)循环搞定。但当你开始接触RTOS(…

2026/8/18 10:10:56 阅读更多 →
DDD微服务拆分实战:电商系统架构演进指南

DDD微服务拆分实战:电商系统架构演进指南

1. 微服务拆分实战:从单体到领域驱动的架构演进十年前我刚入行时,单体架构还是主流选择。直到第一次参与电商大促,眼睁睁看着因为一个优惠券模块的BUG导致整个系统崩溃,我才真正理解微服务拆分的必要性。今天以电商系统为例&#…

2026/8/18 10:09:55 阅读更多 →
免费开源的GTA5线上小助手实测:十分钟从下载到驰骋洛圣都

免费开源的GTA5线上小助手实测:十分钟从下载到驰骋洛圣都

免费开源的GTA5线上小助手实测:十分钟从下载到驰骋洛圣都 【免费下载链接】GTA5OnlineTools GTA5线上小助手 项目地址: https://gitcode.com/gh_mirrors/gt/GTA5OnlineTools 深夜一点,洛圣都的警笛还在你耳边打转。任务进度卡死在四成&#xff0c…

2026/8/18 10:09:55 阅读更多 →

最新新闻

S 曲线加减速算法完整详解(C# 原生实现,无第三方库)

S 曲线加减速算法完整详解(C# 原生实现,无第三方库)

基本概念基础物理定义s(位移):单位为 m/pulse(脉冲),表示物体位置的变化量。在伺服系统中通常以脉冲数计量(例如:1000 pulse 对应 0.1m 位移)。v(速度&#x…

2026/8/18 12:04:17 阅读更多 →
Rikkahub实战指南:零门槛玩转AI角色扮演,轻松导入角色卡

Rikkahub实战指南:零门槛玩转AI角色扮演,轻松导入角色卡

在探索AI聊天工具时,你是否厌倦了复杂的模型部署、繁琐的环境配置和令人头疼的参数调整?特别是当你只想快速导入一个心仪的角色卡,开启一段轻松有趣的对话时,却总被技术门槛劝退。今天,我们将聚焦于一款名为 Rikkahub…

2026/8/18 12:04:17 阅读更多 →
ComfyUI_essentials 插件完整使用指南:如何用 6 大模块补齐 ComfyUI 缺失节点

ComfyUI_essentials 插件完整使用指南:如何用 6 大模块补齐 ComfyUI 缺失节点

ComfyUI_essentials 插件完整使用指南:如何用 6 大模块补齐 ComfyUI 缺失节点 【免费下载链接】ComfyUI_essentials 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI_essentials ComfyUI_essentials 是一套专为 ComfyUI 设计的节点扩展插件&#xff0…

2026/8/18 12:04:17 阅读更多 →
那些年的QQ空间说说还能找回来吗?免费导出全部历史记录的完整攻略

那些年的QQ空间说说还能找回来吗?免费导出全部历史记录的完整攻略

那些年的QQ空间说说还能找回来吗?免费导出全部历史记录的完整攻略 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 深夜翻到旧手机截图,突然想起自己十年前在QQ空…

2026/8/18 12:04:17 阅读更多 →
Betaflight 飞控配置全指南:从固件刷写到黑匣子调优一文搞定

Betaflight 飞控配置全指南:从固件刷写到黑匣子调优一文搞定

Betaflight 飞控配置全指南:从固件刷写到黑匣子调优一文搞定 【免费下载链接】betaflight-configurator Cross platform configuration and management application for the Betaflight firmware 项目地址: https://gitcode.com/gh_mirrors/be/betaflight-configu…

2026/8/18 12:04:17 阅读更多 →
AI 时代的内容交付神器:ShareOne 深度解析

AI 时代的内容交付神器:ShareOne 深度解析

在生成式 AI 爆发的今天,我们经常使用 Claude、ChatGPT 或 Cursor 来生成精美的 HTML 前端网页、SVG 动画、Markdown 报告、PDF 方案或 PPT 课件。然而,如何将这些 AI 生成的临时成果快速、体面地交付给客户、老板或朋友,一直是困扰用户的痛点…

2026/8/18 12:03:10 阅读更多 →

日新闻

告别逐帧截图:用 extract-video-ppt 快速提取视频中的 PPT 并一键导出 PDF

告别逐帧截图:用 extract-video-ppt 快速提取视频中的 PPT 并一键导出 PDF

告别逐帧截图:用 extract-video-ppt 快速提取视频中的 PPT 并一键导出 PDF 【免费下载链接】extract-video-ppt extract the ppt in the video 项目地址: https://gitcode.com/gh_mirrors/ex/extract-video-ppt 如果你还停留在"看网课 不停暂停 截图 …

2026/8/18 0:00:57 阅读更多 →
思源宋体TTF一站式上手:7个字重免费商用,从下载到上线的完整走查

思源宋体TTF一站式上手:7个字重免费商用,从下载到上线的完整走查

思源宋体TTF一站式上手:7个字重免费商用,从下载到上线的完整走查 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 你是不是也经历过这种时刻:设计稿里…

2026/8/18 0:00:58 阅读更多 →
华硕笔记本控制权回收指南:GHelper 如何用一个 10MB 文件替代 Armoury Crate

华硕笔记本控制权回收指南:GHelper 如何用一个 10MB 文件替代 Armoury Crate

华硕笔记本控制权回收指南:GHelper 如何用一个 10MB 文件替代 Armoury Crate 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, …

2026/8/18 0:00:59 阅读更多 →

周新闻

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

如果你是一名开发者,最近可能已经感受到了AI大模型正在从“玩具”变成“生产力工具”的强烈信号。从代码补全到智能Agent,从本地部署到云端API,我们正处在一个技术栈快速重构的节点。然而,面对层出不穷的模型、框架和工具&#xf…

2026/8/18 9:15:35 阅读更多 →
工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/18 9:06:28 阅读更多 →
【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和…

2026/8/18 9:04:56 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/17 18:54:37 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/17 18:55:16 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/17 18:55:55 阅读更多 →