鸿蒙Divider 分割线组件完全指南:内容分组、视觉分区与自定义样式
Divider 分割线组件完全指南:内容分组、视觉分区与自定义样式
本文基于 HarmonyOS(ArkTS 声明式开发范式,API 12 / 5.0.0)写作,所有示例均可在 DevEco Studio 模拟器中验证。配套演示工程位于本文同级目录
ohos/,包含完整可运行的EntryAbility.ets与Index.ets。
一、引言
打开任何一款 App 的个人中心,你会看到订单、优惠券、地址、设置、关于……十来个入口挤在一个页面里。是什么让它们看起来"各有归属"而不是一锅粥?答案往往是几条不起眼的线——Divider 分割线。
分割线是界面设计里最"克制"的元素:它不抢颜色、不抢面积、不抢注意力,只是安静地告诉用户"上面是一组,下面是一组"。它承担着内容分组与视觉分区的双重职责:分组,是把同类的功能框在一起;分区,是把不同类的内容隔开。没有它,列表会变成密不透风的文字墙;有了它,扫一眼就能定位目标入口——这就是分割线的存在价值。
但分割线也是最容易被用错的元素。颜色太深像"铁丝网",宽度太粗像"栅栏",满屏皆线反而比没有线更乱。业界有个共识:分割线是间距方案的补充,不是替代。能用间距(Blank、margin、space)表达的分组就不该画线;必须画线时,1vp 宽、接近背景色的浅灰足矣。真正专业的页面,分割线应该"细到几乎看不见,却又恰好被看见"。
ArkUI 的 Divider 从 API 8 起提供,能力集中:vertical 切换横竖方向,strokeWidth 控制线宽,color 控制颜色,API 11 起还有 lineCap 控制端点形状。至于"带文字的分割线",组件本身不支持,需要用"线-文字-线"三件套自己拼。本文的路线是:先讲透 Divider 的 API 与默认行为,用决策流程图说明"什么时候画线、画什么样的线",再给出完整演示工程,覆盖个人中心组合布局、带文字分割线、垂直分割线三个场景,最后谈模拟器验证、常见问题与视觉分层设计原则。
二、环境准备
Divider 属于 ArkUI 基础组件,API 8 起可用;本文用到的 lineCap 从 API 11 起支持。推荐 API 12 及以上环境,可放心使用全部能力。
| 项目 | 推荐配置 | 说明 |
|---|---|---|
| DevEco Studio | 5.0 及以上 | 需支持 API 12 的 SDK |
| HarmonyOS SDK | 5.0.0(12) | compatibleSdkVersion 与之对应 |
| 设备 | Phone 模拟器或真机 | 本文以模拟器验证为主 |
| 工程类型 | Stage 模型 + ArkTS | EntryAbility 继承 UIAbility |
工程落地路径与前几篇一致,两种方式任选:
- 方式一:在 DevEco Studio 新建 Empty Ability 工程,直接写 ArkTS 原生页面。本文演示工程即采用这种方式。
- 方式二:在已有的 Flutter·鸿蒙壳工程里,把
ohos/entry/src/main/ets/下的页面与组件放进原生工程。这种方式下EntryAbility通常继承自FlutterAbility,演示组件的代码不受影响。
本文配套工程目录结构如下(关键文件已给出):
ohos/
├── AppScope/app.json5
├── build-profile.json5
└── entry/src/main/
├── module.json5
└── ets/
├── entryability/EntryAbility.ets
├── pages/Index.ets
├── model/ProfileModel.ets
└── components/*.ets
若你用的是方式二(Flutter 壳),只需关注
pages/Index.ets、model/ProfileModel.ets与components/下的组件代码,其余配置沿用原工程即可。
三、核心 API 与原理解析
3.1 构造与基本属性
Divider 与 LoadingProgress 一样是无参构造,全部能力来自链式属性:
Divider()
.vertical(false) // false 水平(默认),true 垂直
.strokeWidth(1) // 线宽,单位 vp
.color('#EEEEEE') // 线色
.lineCap(LineCapStyle.BUTT) // 端点形状,API 11+
| 属性 | 类型 | 默认值 | 作用 |
|---|---|---|---|
vertical |
boolean | false | 水平还是垂直 |
strokeWidth |
number/string/Resource | 1 | 线宽 |
color |
ResourceColor | 主题色 | 线色 |
lineCap |
LineCapStyle | BUTT | 端点形状:BUTT/ROUND/SQUARE |
默认行为有三个细节值得注意:默认是水平分割线,且默认占满父容器宽度;默认线宽 1vp,这是"细"的基准;默认颜色跟随主题,浅色模式下接近浅灰,深色模式下接近深灰。这意味着"什么都不配"的 Divider() 已经是一个合格的默认分割线,新手最常见的错误反而是把它配得太重。
3.2 垂直分割线:Row 里的"短竖线"
vertical(true) 让分割线竖起来,但它有两个使用前提:
Row({ space: 12 }) {
Text('关注')
Divider().vertical(true).strokeWidth(1).color('#EEEEEE').height(28)
Text('粉丝')
Divider().vertical(true).strokeWidth(1).color('#EEEEEE').height(28)
Text('获赞')
}
其一,必须放在 Row 里——垂直分割线的"长度"依赖父容器的行方向布局;其二,必须显式给 height——垂直线的可见长度由 height 决定,不给高度它可能撑满整个容器或塌缩成看不见的 0。实践中常见做法是给一个比容器短的高度(如容器 72,线高 28),形成"短竖线分段"的效果。同理,水平分割线在 Column 里通常要给 layoutWeight(1) 或固定宽度来占满剩余空间。
3.3 带文字分割线:"线-文字-线"三件套
Divider 本身不支持文字,但"最近访问""热门推荐"这类带标题的分割线是内容分区的高频需求。标准做法是三个组件拼一行:
Row({ space: 12 }) {
Divider().strokeWidth(1).color('#EEEEEE').layoutWeight(1)
Text('最近访问').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#666666')
Divider().strokeWidth(1).color('#EEEEEE').layoutWeight(1)
}
核心是 layoutWeight(1):两侧线各占一半剩余宽度,文字居中,天然对称。这个三件套值得封装成通用组件(演示工程里的 TitleDivider),传入 title 即可复用。文字的视觉权重决定分区的显眼程度:Medium 字重 + 中性色是"分区标题"的常见选择,比正文弱、比线条强。
3.4 分割线与间距的配合
这是分割线使用中最重要的一课:分割线是间距的补充,不是替代。同样是区分两块内容,有三种手段,强度递增:
| 手段 | 强度 | 适用场景 |
|---|---|---|
间距(space/margin/Blank) |
最弱 | 同组内容之间的留白 |
| 分割线 | 中等 | 不同分组之间的边界 |
卡片背景(backgroundColor) |
最强 | 需要"整体"感知的独立模块 |
判定逻辑可以收敛成一张决策图:
演示工程的个人中心就是这套逻辑的完整落地:组内行之间用"缩进分割线"(左侧留白 56,只隔内容不隔图标),两个功能组用卡片背景自然区分,组与组之间不画整行线——线只在它该在的地方出现。
3.5 一条线只做一件事
分割线有个容易被忽略的原则:每条线只承担一个职责。列表行间的线负责"分行",缩进到图标之后;模块间的线负责"分区",铺满整行;统计项之间的线负责"分栏",短到只剩 28vp。如果把"分行"和"分区"都交给同一条整行线,视觉上会分不清层级。分割线的数量也有个朴素上限——页面上有 n 个分组,就只需要 n-1 条线:
[
L = n - 1
]
个人中心有账户、设置两个分组,两条以上的整行分割线就是噪音。线越多,分组越"碎",信息密度感知反而下降。
3.6 与相关方案的选型对比
| 方案 | 方向 | 强度 | 说明 |
|---|---|---|---|
Divider |
横/竖 | 中 | 组件级分割线,可定制宽/色/端点 |
List 的 divider 属性 |
横 | 中 | 列表内置分隔,省去逐行手写 |
| 间距 | 无 | 弱 | space/margin/Blank,最克制 |
| 卡片背景 | 无 | 强 | 独立模块的容器感 |
选型一句话:列表内分行用 List.divider 或缩进 Divider,模块间分区用卡片与间距,明确的"两栏/两段"边界才用整行分割线。
四、完整代码实现
下面给出演示工程的完整可运行代码。工程以 Tabs 组织三个模块:个人中心(组合布局)、带文字分割线、垂直分割线。数据模型 ProfileModel.ets 提供菜单、推荐与按钮组数据。
4.1 入口:EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private readonly TAG: string = 'DividerGuideAbility';
onCreate(want: object, launchParam: object): void {
hilog.info(0x0000, this.TAG, '%{public}s', 'Ability onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, this.TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, this.TAG, '%{public}s', 'Succeeded in loading the content.');
});
}
onForeground(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onForeground'); }
onBackground(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onBackground'); }
onDestroy(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onDestroy'); }
onWindowStageDestroy(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onWindowStageDestroy'); }
}
4.2 数据模型:ProfileModel.ets
export class MenuItem {
icon: string;
label: string;
constructor(icon: string, label: string) {
this.icon = icon;
this.label = label;
}
}
export const ACCOUNT_MENU: MenuItem[] = [
new MenuItem('🧾', '我的订单'),
new MenuItem('🎫', '优惠券'),
new MenuItem('📍', '收货地址'),
new MenuItem('⭐', '我的收藏'),
];
export const SETTINGS_MENU: MenuItem[] = [
new MenuItem('🔔', '消息通知'),
new MenuItem('🛡️', '隐私设置'),
new MenuItem('🔐', '账号与安全'),
new MenuItem('ℹ️', '关于我们'),
];
export const RECENT_VISITS: string[] = ['鸿蒙开发指南', '声明式UI基础', '组件库速查', '状态管理'];
export const HOT_RECOMMENDS: string[] = ['元服务入门', '多端适配', '性能优化', '上架流程'];
export const ORDER_TABS: string[] = ['全部', '待付款', '待发货', '待收货'];
4.3 主页面:Index.ets
import { PersonalDemo } from '../components/PersonalDemo';
import { SectionDividerDemo } from '../components/SectionDividerDemo';
import { VerticalDividerDemo } from '../components/VerticalDividerDemo';
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
build() {
Column() {
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex }) {
TabContent() { PersonalDemo() }.tabBar('个人中心')
TabContent() { SectionDividerDemo() }.tabBar('带文字分割线')
TabContent() { VerticalDividerDemo() }.tabBar('垂直分割线')
}
.vertical(false)
.scrollable(true)
.barMode(BarMode.Scrollable)
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
}
4.4 个人中心:PersonalDemo.ets
这是本文的核心场景——组合布局:用户卡、统计行、两个功能分组,分割线与间距、卡片背景各司其职。
import { MenuItem, ACCOUNT_MENU, SETTINGS_MENU } from '../model/ProfileModel';
@Component
export struct PersonalDemo {
build() {
Scroll() {
Column({ space: 16 }) {
Text('个人中心')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('92%')
.textAlign(TextAlign.Start)
Row({ space: 12 }) {
Text('👨💻')
.fontSize(44)
.width(64)
.height(64)
.textAlign(TextAlign.Center)
.borderRadius(32)
.backgroundColor('#EAF2FF')
Column({ space: 4 }) {
Text('Harmony 开发者')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('让每一行代码都有价值')
.fontSize(13)
.fontColor('#999999')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('编辑')
.height(32)
.fontSize(13)
.fontColor('#0A59F7')
.backgroundColor('#EAF2FF')
.borderRadius(16)
}
.width('92%')
.padding(16)
.borderRadius(14)
.backgroundColor(Color.White)
.border({ width: 1, color: '#F0F0F0' })
Row() {
this.statItem('128', '关注')
Divider().vertical(true).strokeWidth(1).color('#EEEEEE').height(28)
this.statItem('356', '粉丝')
Divider().vertical(true).strokeWidth(1).color('#EEEEEE').height(28)
this.statItem('1024', '获赞')
}
.width('92%')
.height(72)
.borderRadius(14)
.backgroundColor(Color.White)
.border({ width: 1, color: '#F0F0F0' })
Column() {
ForEach(ACCOUNT_MENU, (item: MenuItem, index: number) => {
this.menuRow(item)
if (index < ACCOUNT_MENU.length - 1) {
Divider()
.strokeWidth(1)
.color('#F2F2F2')
.margin({ left: 56, right: 0 })
}
}, (item: MenuItem) => item.label)
}
.width('92%')
.borderRadius(14)
.backgroundColor(Color.White)
.border({ width: 1, color: '#F0F0F0' })
Column() {
ForEach(SETTINGS_MENU, (item: MenuItem, index: number) => {
this.menuRow(item)
if (index < SETTINGS_MENU.length - 1) {
Divider()
.strokeWidth(1)
.color('#F2F2F2')
.margin({ left: 56, right: 0 })
}
}, (item: MenuItem) => item.label)
}
.width('92%')
.borderRadius(14)
.backgroundColor(Color.White)
.border({ width: 1, color: '#F0F0F0' })
}
.width('100%')
.padding({ top: 16, bottom: 24 })
}
.width('100%')
.height('100%')
}
@Builder
statItem(num: string, label: string) {
Column({ space: 2 }) {
Text(num)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(label)
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
}
@Builder
menuRow(item: MenuItem) {
Row({ space: 12 }) {
Text(item.icon)
.fontSize(20)
.width(36)
.textAlign(TextAlign.Center)
Text(item.label)
.fontSize(15)
.fontColor('#333333')
Blank()
Text('›')
.fontSize(16)
.fontColor('#CCCCCC')
}
.width('100%')
.height(52)
.padding({ left: 16, right: 16 })
}
}
布局的三层逻辑值得拆开看:统计行用垂直分割线分三栏,线高 28 短于容器 72,形成"分段"感;组内行间用 margin({ left: 56 }) 缩进分割线,只分隔文字不切割图标,这是列表页的标准"缩进分割线";组与组之间完全不画线,靠卡片自身的边界与 16 的间距区分——L = n - 1 在这里等于 0。
4.5 带文字分割线:SectionDividerDemo.ets
"线-文字-线"三件套封装成通用组件 TitleDivider:
@Component
export struct TitleDivider {
title: string = '';
build() {
Row({ space: 12 }) {
Divider()
.strokeWidth(1)
.color('#EEEEEE')
.layoutWeight(1)
Text(this.title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#666666')
Divider()
.strokeWidth(1)
.color('#EEEEEE')
.layoutWeight(1)
}
.width('100%')
}
}
使用时一行即得一个带标题的分区:
TitleDivider({ title: '最近访问' })
// ……最近访问的内容……
TitleDivider({ title: '热门推荐' })
// ……热门推荐的内容……
文字颜色比线条深、比正文浅,Medium 字重让标题"有存在感但不抢戏"。layoutWeight(1) 保证两条线严格等长,文字永远居中。
4.6 垂直分割线:VerticalDividerDemo.ets
订单状态按钮组的分栏写法:
Row() {
ForEach(ORDER_TABS, (item: string, index: number) => {
Text(item)
.fontSize(15)
.fontColor(this.selected === index ? '#0A59F7' : '#666666')
.fontWeight(this.selected === index ? FontWeight.Bold : FontWeight.Normal)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(44)
.onClick(() => {
this.selected = index;
})
if (index < ORDER_TABS.length - 1) {
Divider()
.vertical(true)
.strokeWidth(1)
.color('#EEEEEE')
.height(20)
}
}, (item: string, index: number) => item + index.toString())
}
.width('92%')
.height(48)
.borderRadius(12)
.backgroundColor(Color.White)
按钮用 layoutWeight(1) 均分宽度,分割线夹在中间、高度 20 短于容器 48。ForEach 的键生成函数把 index 拼进键里,避免同名按钮(如两个"全部")键冲突。选中态用 @State selected 驱动文字颜色与字重,验证时点击切换即可看到高亮移动。
4.7 模块配置要点
module.json5 声明 EntryAbility 与 pages/Index 路由,main_pages.json 指向 pages/Index,字符串与颜色资源位于 resources/base/element/。与通用 ArkTS 工程完全一致,不再赘述。
五、模拟器运行与效果展示
5.1 编译运行步骤
- 用 DevEco Studio 打开本文配套
ohos/目录; - 在
entry/src/main/resources/base/media/放入名为icon.png的图标(与module.json5中$media:icon对应); - 顶部选择 Phone 模拟器(或连接真机),点击 Run;
- 应用启动后进入
Index页面,顶部 Tab 可在三个演示间切换。
5.2 预期效果截图



5.3 交互验证
- 按钮组切换:在"垂直分割线"Tab 点击不同状态按钮,文字高亮(蓝色加粗)随之移动,Toast 提示当前选中项;
- 分区阅读:在"个人中心"Tab 观察三层分隔手段——统计行的垂直短线、菜单行的缩进线、两个分组的卡片边界,明确"一条线只做一件事";
- 标题分区:在"带文字分割线"Tab 滚动浏览,标题线与内容的归属关系一目了然;
- 缩进对照:对比组内缩进线与组间无线的差异,理解
L = n - 1的分组纪律。
六、调试与常见问题
问题 1:垂直分割线不显示或撑满整个容器。
垂直分割线必须放在 Row 里并显式指定 height。没给高度时它可能塌缩成 0,也可能撑满容器;给了 height(如 20-28)才有"短竖线分段"的效果。
问题 2:水平分割线没有占满整行。
水平分割线在 Column 中默认撑满父宽度,但若父组件给了 padding 或内容不足,线会随之缩短。需要通栏时去掉父容器 padding,或给 Divider 显式 width('100%')。
问题 3:分割线太黑太粗,像栅栏。
视觉分层的铁律:线宽保持 1vp,颜色贴近背景(如白底用 #EEEEEE/#F2F2F2)。需要更强的分组边界时,优先加间距或卡片背景,而不是把线加粗加深。
问题 4:带文字分割线的文字不居中。
文字两侧的 Divider 必须都加 layoutWeight(1),且父 Row 去掉 justifyContent 干扰。三件套封装成 TitleDivider 后不会再有这个问题。
问题 5:lineCap 不生效。LineCapStyle 从 API 11 起支持,API 10 及以下没有该属性。用圆点(ROUND)等特殊端点前先确认 compatibleSdkVersion。
问题 6:深色模式下分割线"看不见"。
写死浅灰色在深色主题里会消失。把 color 换成主题资源($r('app.color.divider'))并按深浅主题分别取值,或交给系统默认色。
无障碍建议: 分割线是纯装饰元素,Divider 不应被读屏聚焦;带文字分割线的文字部分承担分区语义,TitleDivider 的 Text 保持可读即可,不要给整行容器加多余的无障碍文本。
七、总结与扩展
分割线的使用要点浓缩成四条:
-
先间距后画线
-
能用
space/margin/Blank表达的分组就不画线,分割线是补充不是替代; 一条线一件事
- 组内分行用缩进线,模块分区靠卡片与间距,明确的横/竖边界才用整行或短竖线; 轻描淡写
- 线宽保持 1vp、颜色贴近背景,需要强度时加间距和卡片而不是加粗加深; 数量克制
- n 个分组只需 n-1 条线,线越多页面越碎。
好的分割线细到几乎看不见,却又恰好被看见。
往深走,有三条值得继续的路:
- 主题资源化:把线色、线宽收敛进
resources,按深浅主题与品牌色统一管理,页面里不出现魔法值; - 列表内置分隔:
List自带divider属性({ strokeWidth, color, startMargin, endMargin }),大量行分隔时优先用它,性能与一致性都好于逐行手写; - 卡片化布局:把"分割线 + 间距"的分层逻辑应用到整页——首屏用卡片+间距的"无框感",深链路页面用分割线压缩纵向空间,形成两套互补的视觉体系。
分割线是界面设计里最不起眼、也最能体现克制力的元素。它教会开发者一件事:视觉分区的本质不是"画线",而是"分清层级"——想清楚谁跟谁是一组,线画在哪里就顺理成章了。
更多推荐

所有评论(0)