跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略
一、引言:一起来啃源码,解锁HarmonyOS NEXT的“组件密码”!
嘿,小伙伴们!今天想和大家聊一个超实用的开源项目——IBest-UI,一个专为鸿蒙生态打造的轻量级UI组件库。如果你正在开发HarmonyOS NEXT应用,一定遇到过这些痛点:重复造轮子、适配多端界面费时费力、深浅模式切换麻烦……别急,IBest-UI就是来“救场”的!
它有多香?
● 轻量到飞起:核心代码精简,引入即用,绝不给你添负担。
● 主题随心换:深色模式?浅色模式?一行代码切换,适配鸿蒙元服务毫无压力。
● 功能小而美:从按钮到弹窗,从徽章到导航栏,覆盖高频场景,样式参考vant,使用过vant的,都知道vant样式有多好看! 但今天咱们不光是“用组件”,而是要打开引擎盖,看看里面的“黑科技”!我们发起一个源码共读计划,目标很简单:
- 拆解设计思想:比如TextEllipsis 文本省略,它是怎么实现文本省略的?
- 偷师IBest-UI:在源码中捕捉ArkTS的高阶用法,学习如何在HarmonyOS NEXT中用声明式UI开发“丝滑”应用。
- 边学边玩:欢迎随时抛出问题、提交PR,咱们一起让IBest-UI变得更强大! 无论你是想提升源码阅读能力,还是想摸透鸿蒙开发的门道,这个系列都会是你的“实战指南”。准备好和我一起挖宝了吗?Let’s go! 🚀
二、准备工作 看一个开源项目,第一步应该是先看 README.md 再看贡献文档 github/CONTRIBUTING.md。 - 克隆源码
克隆gitalb仓库
git clone git@github.com:ibestservices/ibest-ui.git
或者克隆gitee仓库
git clone git@gitee.com:ibestservices/ibest-ui.git
进入项目
cd ./ibest-ui
安装依赖
ohpm install 2. 查看目录结构 根据贡献文档,可以了解到目录结构 ├── entry # 例子hap包 │ └── src │ ├── main │ │ ├── ets │ │ │ ├── assets │ │ │ │ └── styles # 例子页面样式 │ │ │ ├── components # 例子组件 │ │ │ ├── entryability │ │ │ └── pages # 例子页面 │ │ └── resources │ │ ├── base
...
├── hvigor ├── library # 组件库 │ └── src │ └── main │ ├── ets │ │ ├── assets │ │ │ └── ets # 工具方法 │ │ ├── components # 组件目录 │ │ │ ├── button │ │ │ ├── cell │ │ │ └── ... │ │ └── theme-chalk # 样式变量 │ │ └── src │ └── resources # 组件库资源 │ ├── base
...
根据目录,可以了解到,开发的组件、修复bug,主要是在library里进行开发,entry/main/ets/pages里做组件例子页面。 全局样式变量在 library/src/theme-chalk/...里定义 三、demo 演示 可以通过快捷方式 Ctrl+Shift+N 转到文件,输入自己所想找到的文件
根据前面的目录结构,我们已经知道了entry是样例文件,那么我们要找的文件,就是在 entry\src\main\ets\pages\base\TextEllipsis.ets里 import router from '@ohos.router'; import { CONTAINER_SIZE, modeColor, SPACE } from '../../assets/styles/BaseStyle'; import { ComponentRouterParams } from '../../assets/global.type'; import ComponentShowContainer from '../../components/ComponentShowContainer'; import { IBestNavBar, IBestTextEllipsis } from '@ibestservices/ibest-ui'; @Entry @Component struct TextEllipsisPage { @State title: string = (router.getParams() as ComponentRouterParams).title || '' @State text: string = '人一生的大部分时间都是平淡无奇的。这也是我们身体养精蓄锐的必要条件。因为只有身心在泛起涟漪的生活中得到充分的修正,才能圆满的迎接人生的下一次高峰。' @State text1: string = "那一天我二十一岁,在我一生的黄金时代。我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消失,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。"
build() {
Column() {
IBestNavBar({
title: this.title,
isShowStatusBar: true,
onLeftClick: () => {
router.back()
}
})
List() {
ListItem() {
ComponentShowContainer({ title: '基础用法' }) {
IBestTextEllipsis({
text: this.text
})
}
}
ListItem() {
ComponentShowContainer({ title: '展开/收起' }) {
IBestTextEllipsis({
text: $r("app.string.app_desc"),
showAction: true
})
}
}
ListItem() {
ComponentShowContainer({ title: '自定义展示行数' }) {
Column({space: SPACE.SM}){
IBestTextEllipsis({
text: this.text1,
showAction: true,
rows: 3
})
Row(){
IBestTextEllipsis({
text: this.text1,
showAction: true,
rows: 3
})
}.width(200)
}
}
}
ListItem() {
ComponentShowContainer({ title: '自定义省略位置' }){
ComponentShowContainer({ title: '头部省略' }) {
IBestTextEllipsis({
text: this.text1,
showAction: true,
omitPosition: 'start'
})
}
ComponentShowContainer({ title: '中部省略' }) {
IBestTextEllipsis({
text: this.text1,
omitPosition: 'middle',
showAction: true,
rows: 2
})
}
}
}
ListItem() {
ComponentShowContainer({ title: '自定义省略内容' }) {
IBestTextEllipsis({
text: this.text,
omitContent: "•••"
})
}
}
ListItem() {
ComponentShowContainer({ title: '自定义操作样式' }) {
IBestTextEllipsis({
text: this.text,
showAction: true,
expandText: "平铺",
collapseText: "折叠",
actionColor: "#DB3131"
})
}
}
}
.layoutWeight(1)
.padding({
left: SPACE.SM,
right: SPACE.SM
})
}
.width(CONTAINER_SIZE.FULL)
.height(CONTAINER_SIZE.FULL)
.backgroundColor(modeColor.bg2)
}
} 运行下demo,看下demo使用效果
四、源码解析 快速找到源代码位置 可以通过快捷方式 Ctrl+Shift+N 转到文件,输入自己所想找到的文件
根据前面的目录结构,我们已经知道了entry是样例文件,library是组件库文件,那么我们要找的文件,就是在 library\src\main\ets\components\textEllipsis里 进到文件里,我们可以看到有两个文件
color.est文件定义了相关样式,可以看到,在 library\src\main\resources\base\element\color.json 读取样式,好处理全局样式 interface IBestTextEllipsisColorType { textColor: ResourceColor }
export const IBestTextEllipsisColor: IBestTextEllipsisColorType = { textColor: $r("app.color.ibest_text_color") } 接下来就看下主文件index.est(已添加相关注释) /**
- 导入必要的类型和工具函数,主要获取相关公共样式 */ import { IBestStringNumber } from '../../model/Global.type' import { getDefaultBaseStyle, IBEST_UI_NAMESPACE } from '../../theme-chalk/src' import { CONTAINER_SIZE } from '../../theme-chalk/src/container' import { IBestUIBaseStyleObjType } from '../../theme-chalk/src/index.type' import { convertDimensionsWidthUnit, getComponentsInfo, getResourceStr, getSizeByUnit, measureTextSize } from '../../utils/utils' import { IBestTextEllipsisColor } from './color'
/**
- 定义一个文本省略组件,支持多行省略、操作按钮等功能 / @Component export struct IBestTextEllipsis { /*
- 全局公共样式,从存储中加载默认主题样式 / @StorageLink(IBEST_UI_NAMESPACE) baseStyle: IBestUIBaseStyleObjType = getDefaultBaseStyle() /*
- 显示的文本内容 / @Prop @Watch("formatText") text: ResourceStr = "" /*
- 文字大小,默认为全局样式的中等字体大小 / @Prop textFontSize: IBestStringNumber = this.baseStyle.fontSizeMd as string /*
- 文字颜色,默认为组件定义的颜色 / @Prop textColor: ResourceColor = IBestTextEllipsisColor.textColor /*
- 行高,默认值为20px / @Prop lineHeight: IBestStringNumber = convertDimensionsWidthUnit(20) /*
- 展示的行数,默认为1行 / @Prop @Watch("formatText") rows: number = 1 /*
- 是否显示展开/收起操作按钮 / @Prop showAction: boolean = false /*
- 展开操作文案,默认为资源字符串 / @Prop expandText: ResourceStr = $r("app.string.ibest_text_expand") /*
- 收起操作文案,默认为资源字符串 / @Prop collapseText: ResourceStr = $r("app.string.ibest_text_collapse") /*
- 省略号内容,默认为“…” / @Prop omitContent: ResourceStr = "…" /*
- 操作文字颜色,默认为全局样式的主色 / @Prop actionColor: ResourceColor = this.baseStyle.primary /*
- 省略位置,可选值为"start"、"middle"、"end",默认为"end" / @Prop @Watch("formatText") omitPosition: "start" | "middle" | "end" = "end" /*
- 组件状态变量 / @State uniId: number = 0 // 唯一标识符 @State showText: string = "" // 当前显示的文本 @State isExpand: boolean = false // 是否展开 @State textWidth: number = 0 // 文本宽度 @State textHeight: number = 0 // 文本高度 @State maxLineHeight: number = 0 // 最大行高 /*
- 私有属性:获取UI上下文 */ private uiContext = this.getUIContext()
/**
- 定义省略号内容的构建器 */ @Builder OmitContent() { Span(this.omitContent) .fontColor(this.textColor) .fontSize(getSizeByUnit(this.textFontSize, true)) }
/**
- 组件即将显示时触发,初始化唯一ID并格式化文本 */ aboutToAppear(): void { this.uniId = this.getUniqueId() this.formatText() }
/**
- 获取文本字符串,从资源中解析文本内容
- @returns 文本字符串 */ getTextString(){ return getResourceStr(this.text) }
/**
- 获取省略号文本,从资源中解析省略号内容
- @returns 省略号文本 */ getOmitText(){ return getResourceStr(this.omitContent) }
/**
- 获取展开文本,从资源中解析展开操作文案
- @returns 展开文本 */ getExpandText(){ return getResourceStr(this.expandText) }
/**
-
格式化文本,根据容器宽度和行数计算显示文本 */ formatText(){ setTimeout(() => { // 获取组件宽度、文本高度和最大行高 this.textWidth = getComponentsInfo(this.uiContext,
ibest_text_${this.uniId}
).width this.textHeight = this.measureText(this.getTextString()) this.maxLineHeight = this.measureText(this.getTextString(), this.rows)// 如果文本高度超过最大行高,则截取文本 if (this.textHeight > this.maxLineHeight) { this.getTextByWidth() } else { this.showText = this.getTextString() } }, 0) }
/**
- 根据宽度截取文本,支持不同省略位置 */ getTextByWidth(){ let clipText = this.getTextString() let textHeight = this.textHeight let centerIndex = Math.floor(clipText.length / 2) let leftStr = clipText.slice(0, centerIndex) let rightStr = clipText.slice(centerIndex) let omitText = this.getOmitText() let expandText = this.getExpandText()
// 循环截取文本直到满足高度要求
while (textHeight > this.maxLineHeight) {
switch (this.omitPosition) {
case "start":
clipText = clipText.substring(1)
textHeight = this.measureText(omitText + clipText + (this.showAction ? expandText : ""))
break
case "middle":
leftStr = leftStr.substring(0, leftStr.length - 1)
rightStr = rightStr.substring(1)
textHeight = this.measureText(leftStr + omitText + rightStr + (this.showAction ? expandText : ""))
break
case "end":
clipText = clipText.substring(0, clipText.length - 1)
textHeight = this.measureText(clipText + (this.textHeight > this.maxLineHeight ? omitText : "") +
(this.showAction ? expandText : ""))
break
}
}
// 设置最终显示文本
this.showText = this.omitPosition == 'middle' ? leftStr + omitText + rightStr : clipText
}
/**
- 测量文本高度
- @param text 文本内容
- @param rows 最大行数
- @returns 文本高度 */ measureText(text: string, rows?: number): number { return measureTextSize(this.uiContext, { textContent: text, constraintWidth: this.textWidth, fontSize: getSizeByUnit(this.textFontSize, true), lineHeight: getSizeByUnit(this.lineHeight), maxLines: rows }).height }
/**
- 构建组件UI */ build() { Text() { // 根据省略位置和是否展开,动态显示省略号和操作按钮 if (this.textHeight > this.maxLineHeight && !this.isExpand && this.omitPosition == "start") { this.OmitContent() } Span(this.isExpand ? this.text : this.showText) .fontColor(this.textColor) .fontSize(getSizeByUnit(this.textFontSize, true)) if (this.textHeight > this.maxLineHeight && !this.isExpand && this.omitPosition == "end") { this.OmitContent() } if (this.showAction) { Span(this.isExpand ? this.collapseText : this.expandText) .fontColor(this.actionColor) .fontSize(getSizeByUnit(this.textFontSize, true)) .onClick(() => { this.isExpand = !this.isExpand }) } } .width(CONTAINER_SIZE.FULL) .lineHeight(getSizeByUnit(this.lineHeight)) .id(
ibest_text_${this.uniId}
) .visibility(this.showText ? Visibility.Visible : Visibility.Hidden) } } 代码解释
- 导入必要的工具和类型 首先,代码导入了一些外部工具和类型: ● IBestStringNumber:用于定义字体大小等数值的类型。 ● getDefaultBaseStyle 和 IBEST_UI_NAMESPACE:从主题配置中加载全局样式。 ● CONTAINER_SIZE:定义容器宽度的常量。 ● 一些工具函数如 convertDimensionsWidthUnit、getComponentsInfo 等,用于处理单位转换、获取组件信息等。 这些工具和类型为后续的功能实现提供了基础支持,主要还是获取公共样式,方便做响应式
- 定义组件结构 IBestTextEllipsis 是一个组件,它有以下几个主要部分: (1) 属性(Props) 组件通过 @Prop 定义了一些属性,用户可以通过这些属性自定义组件的行为和外观: ● text:要显示的文本内容。 ● textFontSize:文字大小,默认使用全局样式的中等字体大小。 ● textColor:文字颜色,默认是组件定义的颜色。 ● lineHeight:行高,默认值为 20px。 ● rows:展示的行数,默认为 1 行。 ● showAction:是否显示“展开/收起”按钮。 ● expandText 和 collapseText:分别是“展开”和“收起”的文案。 ● omitContent:省略号的内容,默认是“…”。 ● actionColor:操作按钮的文字颜色,默认是全局样式的主色。 ● omitPosition:省略位置,可选值为 "start"(开头省略)、"middle"(中间省略)、"end"(结尾省略),默认是 "end"。 ● (2) 状态(State) 组件通过 @State 定义了一些状态变量,用于保存组件运行时的数据: ● uniId:唯一标识符,用于区分多个组件实例。 ● showText:当前显示的文本内容。 ● isExpand:是否处于展开状态。 ● textWidth、textHeight、maxLineHeight:分别表示文本宽度、高度以及最大允许的高度。 (3) 私有属性 uiContext 是一个私有属性,用于获取组件的 UI 上下文信息。 (4) 构建器 OmitContent 是一个构建器,用于生成省略号的显示内容,包括文字颜色和字体大小。 /**
- 定义省略号内容的构建器 */ @Builder OmitContent() { Span(this.omitContent) .fontColor(this.textColor) .fontSize(getSizeByUnit(this.textFontSize, true)) }
- 核心方法 组件实现了几个核心方法来处理文本省略和显示逻辑: (1) aboutToAppear 当组件即将显示时,会调用这个方法: ● 生成一个唯一的 uniId。 ● 调用 formatText 方法对文本进行格式化处理。 (2) getResourceStr 从资源中解析出文本内容,返回一个字符串。 可以学习下,封装组件的时候,方便使用中决策传ResourceStr,或是string /**
- 获取Resource字符串 */ export function getResourceStr(res: ResourceStr): string { if(typeof res == 'string'){ return res } return GlobalStore.context.resourceManager.getStringSync(res) } 这段代码定义了一个名为 getResourceStr 的函数,用于获取资源字符串。其功能是根据传入的参数类型,返回对应的字符串值。具体逻辑如下:
- 参数说明:
○ res: ResourceStr:输入参数,可能是字符串类型或资源 ID 类型。 - 逻辑判断:
○ 如果 res 是字符串类型(typeof res == 'string'),直接返回该字符串。
○ 如果 res 不是字符串类型,则调用 GlobalStore.context.resourceManager.getStringSync(res) 方法,将资源 ID 转换为实际的字符串值并返回。 ○ - 用途:
○ 该函数主要用于处理资源字符串的动态获取,支持直接传入字符串或资源 ID 的场景,增强了代码的灵活性和可维护性。
(3) getTextString、 getOmitText 和 getExpandText 分别获取省略号文本和“展开”文案。 /**
- 获取文本字符串,从资源中解析文本内容
- @returns 文本字符串 */ getTextString(){ return getResourceStr(this.text) }
/**
- 获取省略号文本,从资源中解析省略号内容
- @returns 省略号文本 */ getOmitText(){ return getResourceStr(this.omitContent) }
/**
-
获取展开文本,从资源中解析展开操作文案
-
@returns 展开文本 / getExpandText(){ return getResourceStr(this.expandText) } (4) formatText 这是最关键的逻辑之一,用于根据容器宽度和行数计算显示的文本: ● setTimeout(()=>{},0)让DOM更新后计算尺寸(这波叫异步防抖!) ● 获取组件的宽度、文本高度以及最大允许的高度。 ● 如果文本高度超过最大高度,则调用 getTextByWidth 方法对文本进行截取。 ● 否则直接将原始文本赋值给 showText。 /*
-
格式化文本,根据容器宽度和行数计算显示文本 */ formatText(){ setTimeout(() => { // 获取组件宽度、文本高度和最大行高 this.textWidth = getComponentsInfo(this.uiContext,
ibest_text_${this.uniId}
).width this.textHeight = this.measureText(this.getTextString()) this.maxLineHeight = this.measureText(this.getTextString(), this.rows)// 如果文本高度超过最大行高,则截取文本 if (this.textHeight > this.maxLineHeight) { this.getTextByWidth() } else { this.showText = this.getTextString() } }, 0) } ○ getComponentsInfo获取组件真实宽度(动态容器也能Hold住!)
/** -
获取组件信息
-
@param {context} UIContext
-
@param {key} 组件id
-
/ export const getComponentsInfo = (context: UIContext, key: string): ComInfoType => { let comUtils: ComponentUtils = context.getComponentUtils() let info: componentUtils.ComponentInfo = comUtils.getRectangleById(key) return { width: px2vp(info.size.width), height: px2vp(info.size.height), localLeft: px2vp(info.localOffset.x), localTop: px2vp(info.localOffset.y), screenLeft: px2vp(info.screenOffset.x), screenTop: px2vp(info.screenOffset.y), windowLeft: px2vp(info.windowOffset.x), windowTop: px2vp(info.windowOffset.y) } } (5) getTextByWidth 根据指定的省略位置(omitPosition)对文本进行截取: ● 对于 "start",从开头逐步删除字符。 ● 对于 "middle",从中间逐步删除字符。 ● 对于 "end",从结尾逐步删除字符。 ● 每次删除后重新测量文本高度,直到满足高度要求。 /*
- 根据宽度截取文本,支持不同省略位置 */ getTextByWidth(){ let clipText = this.getTextString() let textHeight = this.textHeight let centerIndex = Math.floor(clipText.length / 2) let leftStr = clipText.slice(0, centerIndex) let rightStr = clipText.slice(centerIndex) let omitText = this.getOmitText() let expandText = this.getExpandText()
// 循环截取文本直到满足高度要求 while (textHeight > this.maxLineHeight) { switch (this.omitPosition) { case "start": clipText = clipText.substring(1) textHeight = this.measureText(omitText + clipText + (this.showAction ? expandText : "")) break case "middle": leftStr = leftStr.substring(0, leftStr.length - 1) rightStr = rightStr.substring(1) textHeight = this.measureText(leftStr + omitText + rightStr + (this.showAction ? expandText : "")) break case "end": clipText = clipText.substring(0, clipText.length - 1) textHeight = this.measureText(clipText + (this.textHeight > this.maxLineHeight ? omitText : "") + (this.showAction ? expandText : "")) break } }
// 设置最终显示文本 this.showText = this.omitPosition == 'middle' ? leftStr + omitText + rightStr : clipText } (6) measureText 测量文本的高度,传入参数包括文本内容、字体大小、行高等信息。 /**
- 测量文本高度
- @param text 文本内容
- @param rows 最大行数
- @returns 文本高度 */ measureText(text: string, rows?: number): number { return measureTextSize(this.uiContext, { textContent: text, constraintWidth: this.textWidth, fontSize: getSizeByUnit(this.textFontSize, true), lineHeight: getSizeByUnit(this.lineHeight), maxLines: rows }).height }
- 构建 UI build 方法定义了组件的 UI 结构: ● 如果文本高度超过最大高度且未展开,会在开头或结尾显示省略号。 ● 显示当前的文本内容(showText 或完整的 text)。 ● 如果启用了操作按钮(showAction),会显示“展开”或“收起”按钮,点击后切换展开状态。 5.控制流图
流程图说明:
- 初始化组件:加载全局样式、设置默认属性值。
- 判断文本是否超出高度:通过 measureText 方法测量文本高度,与最大允许高度比较。
- 截取文本:如果超出高度,调用 getTextByWidth 方法,根据省略位置(start、middle、end)逐步截取文本。
- 测量截取后文本的高度:每次截取后重新测量高度,直到满足高度要求。
- 设置最终显示文本:将截取后的文本赋值给 showText。
- 构建UI:根据 showText 和其他属性动态生成组件的 UI。 五. 总结 这次拆的是IBest-UI的TextEllipsis组件,这个组件的核心功能是:
- 根据容器宽度和行数自动截取文本,并支持不同的省略位置(开头、中间、结尾)。
- 提供“展开/收起”按钮,方便用户查看完整内容。
- 支持自定义字体大小、颜色、行高等样式。 通过这些功能,开发者可以轻松实现多行文本的省略效果,并提供良好的用户体验。 HarmonyOS NEXT开发想搞定长文本显示?这个组件直接抄作业!
- 0回答
- 0粉丝
- 0关注
- 跟着IBest-UI学HarmonyOS NEXT组件封装:Badge徽标
- 跟着IBest-UI学HarmonyOS NEXT组件封装:Watermark 水印
- HarmonyOS NEXT快速入手IBest-UI组件库
- 如何解决Text组件文本为内容中文、数字、英文混合时显示省略号截断异常
- HarmonyOs开发:导航tabs组件封装与使用
- HarmonyOS NEXT实战:自定义封装多种样式导航栏组件
- HarmonyOs开发:轮播图Banner组件封装与使用
- 【HarmonyOS Next开发】Tabs使用封装
- 【HarmonyOS Next开发】应用权限原理和封装
- 第十六课:HarmonyOS Next高级UI组件开发指南
- 11 【HarmonyOS NEXT】 仿uv-ui组件开发之Avatar组件深度剖析(二)
- 12 【HarmonyOS NEXT】 仿uv-ui组件开发之Avatar组件设计精髓(三)
- 13 【HarmonyOS NEXT】 仿uv-ui组件开发之Avatar组件进阶指南(四)
- 23.Harmonyos Next仿uv-ui 组件NumberBox 步进器组件基础用法
- 24.Harmonyos Next仿uv-ui 组件 NumberBox 步进器组件步长设置