跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略

2025-03-29 15:44:34
135次阅读
0个评论

一、引言:一起来啃源码,解锁HarmonyOS NEXT的“组件密码”!
嘿,小伙伴们!今天想和大家聊一个超实用的开源项目——IBest-UI,一个专为鸿蒙生态打造的轻量级UI组件库。如果你正在开发HarmonyOS NEXT应用,一定遇到过这些痛点:重复造轮子、适配多端界面费时费力、深浅模式切换麻烦……别急,IBest-UI就是来“救场”的!
它有多香?
● 轻量到飞起:核心代码精简,引入即用,绝不给你添负担。
● 主题随心换:深色模式?浅色模式?一行代码切换,适配鸿蒙元服务毫无压力。
● 功能小而美:从按钮到弹窗,从徽章到导航栏,覆盖高频场景,样式参考vant,使用过vant的,都知道vant样式有多好看! 但今天咱们不光是“用组件”,而是要打开引擎盖,看看里面的“黑科技”!我们发起一个源码共读计划,目标很简单:

  1. 拆解设计思想:比如TextEllipsis 文本省略,它是怎么实现文本省略的?
  2. 偷师IBest-UI:在源码中捕捉ArkTS的高阶用法,学习如何在HarmonyOS NEXT中用声明式UI开发“丝滑”应用。
  3. 边学边玩:欢迎随时抛出问题、提交PR,咱们一起让IBest-UI变得更强大! 无论你是想提升源码阅读能力,还是想摸透鸿蒙开发的门道,这个系列都会是你的“实战指南”。准备好和我一起挖宝了吗?Let’s go! 🚀
    二、准备工作 看一个开源项目,第一步应该是先看 README.md 再看贡献文档 github/CONTRIBUTING.md。
  4. 克隆源码

克隆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) } } 代码解释
  1. 导入必要的工具和类型 首先,代码导入了一些外部工具和类型: ● IBestStringNumber:用于定义字体大小等数值的类型。 ● getDefaultBaseStyle 和 IBEST_UI_NAMESPACE:从主题配置中加载全局样式。 ● CONTAINER_SIZE:定义容器宽度的常量。 ● 一些工具函数如 convertDimensionsWidthUnit、getComponentsInfo 等,用于处理单位转换、获取组件信息等。 这些工具和类型为后续的功能实现提供了基础支持,主要还是获取公共样式,方便做响应式
  2. 定义组件结构 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)) }
  3. 核心方法 组件实现了几个核心方法来处理文本省略和显示逻辑: (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 的函数,用于获取资源字符串。其功能是根据传入的参数类型,返回对应的字符串值。具体逻辑如下:
  1. 参数说明:
    ○ res: ResourceStr:输入参数,可能是字符串类型或资源 ID 类型。
  2. 逻辑判断:
    ○ 如果 res 是字符串类型(typeof res == 'string'),直接返回该字符串。
    ○ 如果 res 不是字符串类型,则调用 GlobalStore.context.resourceManager.getStringSync(res) 方法,将资源 ID 转换为实际的字符串值并返回。 ○
  3. 用途:
    ○ 该函数主要用于处理资源字符串的动态获取,支持直接传入字符串或资源 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 }
  1. 构建 UI build 方法定义了组件的 UI 结构: ● 如果文本高度超过最大高度且未展开,会在开头或结尾显示省略号。 ● 显示当前的文本内容(showText 或完整的 text)。 ● 如果启用了操作按钮(showAction),会显示“展开”或“收起”按钮,点击后切换展开状态。 5.控制流图

流程图说明:

  1. 初始化组件:加载全局样式、设置默认属性值。
  2. 判断文本是否超出高度:通过 measureText 方法测量文本高度,与最大允许高度比较。
  3. 截取文本:如果超出高度,调用 getTextByWidth 方法,根据省略位置(start、middle、end)逐步截取文本。
  4. 测量截取后文本的高度:每次截取后重新测量高度,直到满足高度要求。
  5. 设置最终显示文本:将截取后的文本赋值给 showText。
  6. 构建UI:根据 showText 和其他属性动态生成组件的 UI。 五. 总结 这次拆的是IBest-UI的TextEllipsis组件,这个组件的核心功能是:
  7. 根据容器宽度和行数自动截取文本,并支持不同的省略位置(开头、中间、结尾)。
  8. 提供“展开/收起”按钮,方便用户查看完整内容。
  9. 支持自定义字体大小、颜色、行高等样式。 通过这些功能,开发者可以轻松实现多行文本的省略效果,并提供良好的用户体验。 HarmonyOS NEXT开发想搞定长文本显示?这个组件直接抄作业!
收藏00

登录 后评论。没有帐号? 注册 一个。