文章目录前言手写进度条的结构两段 Column 拼接多段分类占比条完整代码flexBasis 和 width 百分比的区别clip 的必要性小结前言ArkUI 自带的Progress组件用起来方便但定制空间有限只支持单色、颜色不能随数据动态变化、圆角样式有限。真实项目里经常碰到支出分类占比条这种需求——每个分类一种颜色宽度按百分比多个分类拼在一行。用Progress搞不定用 Flex 手写反而更直接。手写进度条的结构两段 Column 拼接最简单的进度条是两段有色段 灰色段。Row(){// 有色段已完成Column().width(${percentage}%).height(8).backgroundColor(color).borderRadius({topLeft:4,bottomLeft:4})// 灰色段未完成Column().layoutWeight(1)// 填满剩余.height(8).backgroundColor(#E5E7EB).borderRadius({topRight:4,bottomRight:4})}.width(100%)有色段用width百分比设置宽度灰色段用layoutWeight(1)填满剩余。多段分类占比条多个分类按比例拼在同一行每段不同颜色Row(){ForEach(categories,(cat){Column().flexBasis(${cat.percentage}%)// ← flexBasis 百分比.height(10).backgroundColor(cat.color)})}.width(100%).borderRadius(5).clip(true)// ← 裁掉超出圆角的部分用flexBasis而不是width因为 Flex 子项默认不超出父容器加在一起刚好 100% 时不会溢出。完整代码interfaceCategoryItem{name:stringamount:numbercolor:stringicon:string}interfaceMonthExpense{month:stringtotal:numberbudget:numbercategories:CategoryItem[]}EntryComponentstruct PcProgressBarPage{StateselectedMonthIndex:number0monthData:MonthExpense[][{month:2024年10月,total:14800,budget:15000,categories:[{name:餐饮,amount:3200,color:#EF4444,icon:},{name:交通,amount:1800,color:#F59E0B,icon:},{name:购物,amount:4600,color:#8B5CF6,icon:️},{name:娱乐,amount:2200,color:#3B82F6,icon:},{name:其他,amount:3000,color:#6B7280,icon:},]},{month:2024年11月,total:12400,budget:15000,categories:[{name:餐饮,amount:2800,color:#EF4444,icon:},{name:交通,amount:1400,color:#F59E0B,icon:},{name:购物,amount:3600,color:#8B5CF6,icon:️},{name:娱乐,amount:1800,color:#3B82F6,icon:},{name:其他,amount:2800,color:#6B7280,icon:},]},{month:2024年12月,total:18600,budget:15000,categories:[{name:餐饮,amount:4200,color:#EF4444,icon:},{name:交通,amount:2200,color:#F59E0B,icon:},{name:购物,amount:6800,color:#8B5CF6,icon:️},{name:娱乐,amount:3400,color:#3B82F6,icon:},{name:其他,amount:2000,color:#6B7280,icon:},]}]getCurrentMonth():MonthExpense{constmonththis.monthData[this.selectedMonthIndex]if(month){returnmonth}returnthis.monthData[0]}getbudgetUsagePercent():number{returnMath.min(100,Math.floor(this.getCurrentMonth().total/this.getCurrentMonth().budget*100))}getisOverBudget():boolean{returnthis.getCurrentMonth().totalthis.getCurrentMonth().budget}getCategoryPercent(cat:CategoryItem):number{returnMath.floor(cat.amount/this.getCurrentMonth().total*100)}formatMoney(amount:number):string{return¥${amount.toLocaleString()}}BuildermultiColorBar(categories:CategoryItem[]){Row(){ForEach(categories,(cat:CategoryItem){Column().flexBasis(${this.getCategoryPercent(cat)}%).height(12).backgroundColor(cat.color)})}.width(100%).height(12).borderRadius(6).clip(true)}BuildersingleBar(percentage:number,color:string,height:number8){Row(){Column().width(${percentage}%).height(height).backgroundColor(color).borderRadius({topLeft:height/2,bottomLeft:height/2,topRight:percentage100?height/2:0,bottomRight:percentage100?height/2:0}).animation({duration:400,curve:Curve.EaseOut})Column().layoutWeight(1).height(height).backgroundColor(#F3F4F6).borderRadius({topRight:height/2,bottomRight:height/2,topLeft:0,bottomLeft:0})}.width(100%).borderRadius(height/2).clip(true)}BuildercategoryRow(cat:CategoryItem){Column({space:6}){Row(){Row({space:8}){Text(cat.icon).fontSize(14)Text(cat.name).fontSize(13).fontColor(#374151).fontWeight(FontWeight.Medium)}.layoutWeight(1)Row({space:8}){Text(this.formatMoney(cat.amount)).fontSize(13).fontColor(#1F2937).fontWeight(FontWeight.Medium)Text(${this.getCategoryPercent(cat)}%).fontSize(11).fontColor(cat.color).padding({left:6,right:6,top:2,bottom:2}).backgroundColor(${cat.color}20).borderRadius(8)}}.width(100%).alignItems(VerticalAlign.Center)this.singleBar(this.getCategoryPercent(cat),cat.color,6)}.width(100%).padding({top:4,bottom:4})}build(){Scroll(){Column({space:20}){// 标题 月份切换Row(){Text(支出分析).fontSize(22).fontWeight(FontWeight.Bold).fontColor(#111827).layoutWeight(1)Row({space:4}){ForEach(this.monthData,(m:MonthExpense,idx:number){Text(m.month.slice(5)).fontSize(12).fontColor(this.selectedMonthIndexidx?Color.White:#6B7280).padding({left:10,right:10,top:6,bottom:6}).backgroundColor(this.selectedMonthIndexidx?#3B82F6:#F3F4F6).borderRadius(8).onClick((){this.selectedMonthIndexidx})})}}.width(100%)// 总预算进度Column({space:12}){Row(){Column({space:4}){Text(${this.getCurrentMonth().month}支出).fontSize(12).fontColor(#6B7280)Text(this.formatMoney(this.getCurrentMonth().total)).fontSize(24).fontWeight(FontWeight.Bold).fontColor(this.isOverBudget?#EF4444:#111827)}.layoutWeight(1).alignItems(HorizontalAlign.Start)Column({space:4}){Text(月度预算).fontSize(12).fontColor(#6B7280).alignSelf(ItemAlign.End)Text(this.formatMoney(this.getCurrentMonth().budget)).fontSize(16).fontColor(#6B7280).alignSelf(ItemAlign.End)}}.width(100%)// 总进度条this.singleBar(this.budgetUsagePercent,this.isOverBudget?#EF4444:#10B981,12)Row(){Text(this.isOverBudget?⚠️ 已超预算:剩余${this.formatMoney(this.getCurrentMonth().budget-this.getCurrentMonth().total)}).fontSize(12).fontColor(this.isOverBudget?#EF4444:#10B981)Text(${this.budgetUsagePercent}%).fontSize(12).fontColor(#6B7280)}.width(100%).justifyContent(FlexAlign.SpaceBetween)}.padding(20).backgroundColor(Color.White).borderRadius(16).shadow({radius:8,color:#08000000})// 分类占比条多色Column({space:12}){Text(分类占比).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#374151)this.multiColorBar(this.getCurrentMonth().categories)// 图例Flex({wrap:FlexWrap.Wrap}){ForEach(this.getCurrentMonth().categories,(cat:CategoryItem){Row({space:4}){Row().width(10).height(10).borderRadius(2).backgroundColor(cat.color)Text(${cat.name}${this.getCategoryPercent(cat)}%).fontSize(11).fontColor(#6B7280)}.margin({right:16,bottom:6})})}.width(100%)}.padding(20).backgroundColor(Color.White).borderRadius(16).shadow({radius:8,color:#08000000})// 分类详情列表Column({space:4}){Text(分类明细).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#374151).padding({bottom:12})ForEach(this.getCurrentMonth().categories,(cat:CategoryItem){this.categoryRow(cat)Divider().strokeWidth(1).color(#F9FAFB).margin({top:4,bottom:4})})}.padding(20).backgroundColor(Color.White).borderRadius(16).shadow({radius:8,color:#08000000})}.padding({left:32,right:32,top:32,bottom:32}).constraintSize({minWidth:600,maxWidth:860}).margin({left:auto,right:auto})}.width(100%).height(100%).backgroundColor(#F9FAFB)}}flexBasis 和 width 百分比的区别在这个场景里两者效果差不多但有一个细节差异width(20%)是最终宽度Flex 伸缩不影响它flexBasis(20%)是初始尺寸如果还有flexGrow子项会继续增长对于分类占比条所有分类的百分比加起来 100%刚好填满容器不需要flexGrow。用哪个都一样flexBasis语义上更准确“基准尺寸”配合 Flex 容器。clip 的必要性多色占比条用borderRadius让两端圆角但各个分类的小 Column 没有设圆角它们的直角会超出父容器的圆角范围视觉上露出方角。.clip(true)让父容器裁掉超出自身圆角的内容Row(){/* 各分类色块 */}.borderRadius(6).clip(true)// ← 这行解决方角露出的问题小结手写进度条的两种形式单色进度条用有色段 灰色段两段拼接多色占比条用ForEachflexBasis百分比。两种都不需要 Canvas纯 Flex Column 实现样式完全可控。