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

一梦南柯2
发布于 2025-3-29 14:58
浏览
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

  1. 克隆源码
# 克隆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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  1. 查看目录结构

根据贡献文档,可以了解到目录结构

├── 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
# ...
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

根据目录,可以了解到,开发的组件、修复bug,主要是在library里进行开发,entry/main/ets/pages里做组件例子页面。

全局样式变量在 library/src/theme-chalk/...里定义

三、demo 演示

可以通过快捷方式 Ctrl+Shift+N 转到文件,输入自己所想找到的文件
跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区

根据前面的目录结构,我们已经知道了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)
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.

运行下demo,看下demo使用效果

跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区

四、源码解析

快速找到源代码位置

可以通过快捷方式 Ctrl+Shift+N 转到文件,输入自己所想找到的文件

跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区
根据前面的目录结构,我们已经知道了entry是样例文件,library是组件库文件,那么我们要找的文件,就是在 library\src\main\ets\components\textEllipsis

进到文件里,我们可以看到有两个文件

跟着IBest-UI学HarmonyOS NEXT组件封装: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")
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

接下来就看下主文件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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.

代码解释

1. 导入必要的工具和类型

首先,代码导入了一些外部工具和类型:

  • IBestStringNumber:用于定义字体大小等数值的类型。
  • getDefaultBaseStyleIBEST_UI_NAMESPACE:从主题配置中加载全局样式。
  • CONTAINER_SIZE:定义容器宽度的常量。
  • 一些工具函数如 convertDimensionsWidthUnitgetComponentsInfo 等,用于处理单位转换、获取组件信息等。

这些工具和类型为后续的功能实现提供了基础支持,主要还是获取公共样式,方便做响应式

2. 定义组件结构

IBestTextEllipsis 是一个组件,它有以下几个主要部分:

(1) 属性(Props)

组件通过 @Prop 定义了一些属性,用户可以通过这些属性自定义组件的行为和外观:

  • text:要显示的文本内容。
  • textFontSize:文字大小,默认使用全局样式的中等字体大小。
  • textColor:文字颜色,默认是组件定义的颜色。
  • lineHeight:行高,默认值为 20px。
  • rows:展示的行数,默认为 1 行。
  • showAction:是否显示“展开/收起”按钮。
  • expandTextcollapseText:分别是“展开”和“收起”的文案。
  • omitContent:省略号的内容,默认是“…”。
  • actionColor:操作按钮的文字颜色,默认是全局样式的主色。
  • omitPosition:省略位置,可选值为 “start”(开头省略)、“middle”(中间省略)、“end”(结尾省略),默认是 “end”。
    跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区

(2) 状态(State)

组件通过 @State 定义了一些状态变量,用于保存组件运行时的数据:

  • uniId:唯一标识符,用于区分多个组件实例。
  • showText:当前显示的文本内容。
  • isExpand:是否处于展开状态。
  • textWidthtextHeightmaxLineHeight:分别表示文本宽度、高度以及最大允许的高度。

(3) 私有属性

uiContext 是一个私有属性,用于获取组件的 UI 上下文信息。

(4) 构建器

OmitContent 是一个构建器,用于生成省略号的显示内容,包括文字颜色和字体大小。

  /**
   * 定义省略号内容的构建器
   */
  @Builder
  OmitContent() {
    Span(this.omitContent)
      .fontColor(this.textColor)
      .fontSize(getSizeByUnit(this.textFontSize, true))
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

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)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这段代码定义了一个名为 getResourceStr 的函数,用于获取资源字符串。其功能是根据传入的参数类型,返回对应的字符串值。具体逻辑如下:

  1. 参数说明
    • res: ResourceStr:输入参数,可能是字符串类型或资源 ID 类型。
  1. 逻辑判断
    • 如果 res 是字符串类型(typeof res == 'string'),直接返回该字符串。
    • 如果 res 不是字符串类型,则调用 GlobalStore.context.resourceManager.getStringSync(res) 方法,将资源 ID 转换为实际的字符串值并返回。
      跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区
  1. 用途
    • 该函数主要用于处理资源字符串的动态获取,支持直接传入字符串或资源 ID 的场景,增强了代码的灵活性和可维护性。

跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区

(3) getTextStringgetOmitTextgetExpandText

分别获取省略号文本和“展开”文案。

  /**
   * 获取文本字符串,从资源中解析文本内容
   * @returns 文本字符串
   */
  getTextString(){
    return getResourceStr(this.text)
  }

  /**
   * 获取省略号文本,从资源中解析省略号内容
   * @returns 省略号文本
   */
  getOmitText(){
    return getResourceStr(this.omitContent)
  }

  /**
   * 获取展开文本,从资源中解析展开操作文案
   * @returns 展开文本
   */
  getExpandText(){
    return getResourceStr(this.expandText)
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
(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)
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
    • 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)
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
(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
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
(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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

4. 构建 UI

build 方法定义了组件的 UI 结构:

  • 如果文本高度超过最大高度且未展开,会在开头或结尾显示省略号。
  • 显示当前的文本内容(showText 或完整的 text)。
  • 如果启用了操作按钮(showAction),会显示“展开”或“收起”按钮,点击后切换展开状态。

5.控制流图

跟着IBest-UI学HarmonyOS NEXT组件封装:TextEllipsis 文本省略-鸿蒙开发者社区

流程图说明:
  1. 初始化组件:加载全局样式、设置默认属性值。
  2. 判断文本是否超出高度:通过 measureText 方法测量文本高度,与最大允许高度比较。
  3. 截取文本:如果超出高度,调用 getTextByWidth 方法,根据省略位置(startmiddleend)逐步截取文本。
  4. 测量截取后文本的高度:每次截取后重新测量高度,直到满足高度要求。
  5. 设置最终显示文本:将截取后的文本赋值给 showText
  6. 构建UI:根据 showText 和其他属性动态生成组件的 UI。

五. 总结

这次拆的是IBest-UI的TextEllipsis组件,这个组件的核心功能是:

  1. 根据容器宽度和行数自动截取文本,并支持不同的省略位置(开头、中间、结尾)。
  2. 提供“展开/收起”按钮,方便用户查看完整内容。
  3. 支持自定义字体大小、颜色、行高等样式。

通过这些功能,开发者可以轻松实现多行文本的省略效果,并提供良好的用户体验。

HarmonyOS NEXT开发想搞定长文本显示?这个组件直接抄作业!

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报


回复
    相关推荐
    这个用户很懒,还没有个人简介
    帖子
    视频
    声望
    粉丝
    社区精华内容