【高心星出品】

文章目录

  • 仿微博文本折叠
    • 概述
    • 纯文本展开折叠
      • 场景描述
      • 实现原理
      • 开发步骤
    • 富文本展开折叠
      • 场景描述
      • 实现原理
      • 开发步骤

仿微博文本折叠

概述

列表中的博文、评论等复合型内容组件,在文本行数超过预设阈值时,触发“展开”“收起”的功能。内容收起时,如果有用“图片”展示“表情”的功能场景,由于图片出现的位置和大小都不固定,在收起展开时,截止到文字结尾的位置不好判断。

本文将介绍解决这一问题的基本逻辑和解决方案,帮助开发者使用系统自带模块,更简洁的解决问题。

在这里插入图片描述

纯文本展开折叠

场景描述

在示例列表中显示纯文本展开和收起功能,文本与按钮的显示变化。

  1. 文本中只有文字。
  2. 超出2行要能显示"…展开",展开后显示收起。

在这里插入图片描述

实现原理

需要计算出“…”前最后一个文字的索引和显示行高,以确定“收起”“展开”按钮的位置,其原理如图所示:

在这里插入图片描述

计算文本高度,结合按钮和“…”的宽度,计算收起文本最后一个文字的坐标,换算为对应内容索引,截断显示相应的内容。

分别添加“收起”“展开”按钮及交互,进行文本截断内容和全部内容展示的切换。

本示例使用measureTextSize()方法判断文字的高度,API20新增getParagraphs20+方法测算文本。getParagraphs()方法代码量更少,更直观的获取显示行数。

开发步骤

  1. 计算原始文本高度 。

    使用measureTextSize()方法来判断总体文字的高度。

    getIsExpanded() {
      let titleSize: SizeOptions = this.uiContext.getMeasureUtils().measureTextSize({
        textContent: this.textSectionAttribute.title, //The text content is calculated
        lineHeight: this.textSectionAttribute.lineHeight,
        constraintWidth: this.textSectionAttribute.constraintWidth, //The text layout width is calculated
        fontSize: this.textSectionAttribute.fontSize //The text font size is calculated
      });
      let height = this.getUIContext().px2vp(Number(titleSize.height));
      if (height <= this.textSectionAttribute.lineHeight * 2) {
        this.textModifier.needProcess = false;
        this.textModifier.title = this.textSectionAttribute.title;
        return;
      } else {
        this.textModifier.needProcess = true;
      }
      if (this.expanded) {
        this.collapseText();
      } else {
        this.expandText();
      }
    }
    

    代码逻辑走读:

    1. 获取文本尺寸

      • 使用 this.uiContext.getMeasureUtils().measureTextSize方法计算文本的尺寸。参数包括文本内容、行高、布局宽度和字体大小。
      • 结果存储在 titleSize变量中,特别是文本的高度。
    2. 转换高度单位

      • 使用 this.getUIContext().px2vp方法将文本高度从像素单位转换为视口单位。
    3. 判断是否需要处理

      • 如果文本高度小于等于两倍的行高,则不需要进一步处理,将 this.textModifier.needProcess设置为 false,并将标题设置为当前文本。
      • 否则,将 this.textModifier.needProcess设置为 true,表示需要进一步处理。
    4. 处理文本展开/折叠

      • 如果 this.expandedtrue,则调用 this.collapseText()方法折叠文本。

      • 否则,调用 this.expandText()方法展开文本。

  2. 计算文本收起高度(示例代码与步骤3同源)。

    使用measureTextSize()方法来判断两行文字的高度,当前为两行文字的高度。

    const minLinesTextSize: SizeOptions = uiContext?.getMeasureUtils().measureTextSize({
        textContent: text,
        fontSize: fontSize,
        maxLines: maxLines,
        wordBreak: WordBreak.BREAK_ALL,
        constraintWidth: textWidth
      });
      const minHeight: Length | undefined = minLinesTextSize.height;
    

    代码逻辑走读:

    1. 获取文本尺寸工具:通过 uiContext?.getMeasureUtils()获取一个测量工具对象。

    2. 测量文本尺寸

      调用measureTextSize方法,传入一系列参数来测量文本的尺寸。

      • textContent:要测量的文本内容。
      • fontSize:文本的字体大小。
      • maxLines:文本的最大行数。
      • wordBreak:文本的换行规则,这里设置为 BREAK_ALL,表示允许文本在任意位置断行。
      • constraintWidth:文本的限制宽度。
    3. 存储高度信息:将测量得到的高度信息存储在变量 minHeight中。如果高度未定义,minHeight将为 undefined

  3. 获取收起文本,显示收起展开按钮。

    减少接收文字字符数。当接收文字高度小于指定行数高度时,使文字显示两行收起。

    public static getShortText(textSectionAttribute: TextSectionAttribute, lastSpan: string): string {
      let text = TextUtils.getStringFromResource(textSectionAttribute.title);
      const minLinesTextSize: SizeOptions | undefined = uiContext?.getMeasureUtils().measureTextSize({
        textContent: text,
        fontSize: textSectionAttribute.fontSize,
        maxLines: textSectionAttribute.maxLines,
        wordBreak: WordBreak.BREAK_ALL,
        constraintWidth: textSectionAttribute.constraintWidth
      });
      const minHeight: Length | undefined = minLinesTextSize?.height;
      if (minHeight === undefined) {
        return '';
      }
      // Use the dichotomy to find strings that are exactly two lines in length
      let textStr: string[] = Array.from(text); //Split the string to avoid special characters and inconsistent sizes
      let leftCursor: number = 0;
      let rightCursor: number = textStr.length;
      let cursor: number = Math.floor(rightCursor / 2);
      let tempTitle: string = '';
      while (true) {
        tempTitle = text.substring(0, cursor) + suffix + lastSpan;
        const currentLinesTextSize: SizeOptions | undefined = uiContext?.getMeasureUtils().measureTextSize({
          textContent: tempTitle,
          fontSize: textSectionAttribute.fontSize,
          wordBreak: WordBreak.BREAK_ALL,
          constraintWidth: textSectionAttribute.constraintWidth
        });
        const currentLineHeight: Length | undefined = currentLinesTextSize?.height;
        if (currentLineHeight === undefined) {
          return '';
        }
        if (currentLineHeight > minHeight) {
          // The current character has exceeded two lines, continue to look to the left
          rightCursor = cursor;
          cursor = leftCursor + Math.floor((cursor - leftCursor) / 2);
        } else {
          // The current character is less than two lines, it may be OK, but you still need to look to the right
          leftCursor = cursor;
          cursor += Math.floor((rightCursor - cursor) / 2);
        }
        if (Math.abs(rightCursor - leftCursor) <= 1) {
          // The two pointers basically coincide, which means that they have been found
          break;
        }
      }
      return text.substring(0, cursor) + suffix;
    }
    

    代码逻辑走读:

    1. 初始化文本

      • 使用TextUtils.getStringFromResource方法从资源中获取文本内容,并将其存储在text变量中。
    2. 测量文本尺寸

      • 调用uiContext.getMeasureUtils().measureTextSize方法测量文本的尺寸,包括字体大小、最大行数、宽度限制等参数。
      • 存储测量结果的minHeight,如果测量失败(即minHeightundefined),则返回空字符串。
    3. 二分查找算法

      • 将文本字符串分割成字符数组textStr,并初始化左右指针leftCursorrightCursor
      • 使用二分查找算法在文本中查找合适的切割点。每次迭代中,计算中间位置cursor,并生成临时文本tempTitle
      • 再次调用uiContext.getMeasureUtils().measureTextSize方法测量tempTitle的尺寸。
      • 根据测量结果,调整左右指针的位置,以缩小搜索范围。
      • 当左右指针的差值小于等于1时,表明找到了合适的切割点,跳出循环。
    4. 返回结果

      • 返回切割后的文本字符串,并在末尾添加suffixlastSpan

富文本展开折叠

场景描述

在示例列表中显示富文本展开和收起功能,文本与按钮的显示变化。

  1. 文本中存在表情。
  2. 文本中文字存在不同颜色、字号。
  3. 超出3行要能显示"…展开",展开后显示收起。
  4. 关键字有超链接功能。

当前展示内容需要针对整个文本做截断并最终显示…和"展开"字眼,例如图片中的文本就比较长,需要在"潮声与你"的位置截断。该场景由于文本中有图片和不同字号的限制,使得计算截断文本的位置比较困难。

在这里插入图片描述

实现原理

需要计算出“…”前最后一个文字的索引和显示行高,以确定“收起”“展开”按钮的位置,其原理如图所示:

在这里插入图片描述

使用排版,计算实际需要收起内容的高度,结合按钮和“…”的宽度,计算收起文本最后一个文字的坐标,换算为对应内容索引,截断显示相应的内容。

分别添加“收起”“展开”按钮及交互,进行文本截断内容和全部内容展示的切换。

开发步骤

  1. 引用graphics.text解析文本、确定内容大小。

    设置文本解析规则,解析字符串。例如,图片显示位置、大小、文本显示位置、文本颜色、文本字号。

  2. 设置段落排版。

    创建ParagraphBuilder,初始化文本样式,指定文本大小和文本颜色。注意文本大小这里是传的px,需要用fp2px转换一下(转换时需要考虑字体设置的最大缩放比例和系统字体缩放比例,即选择min(sysFontScale,maxCustomFontScale))。

    let myTextStyle: text.TextStyle = {
      fontSize: uiContext?.fp2px(fontSize)
    };
    
    let myParagraphStyle: text.ParagraphStyle = {
      textStyle: myTextStyle,
      align: text.TextAlign.START,
      maxLines: 300, // Just specify a large enough number of rows
      breakStrategy: text.BreakStrategy.GREEDY,
      wordBreak: text.WordBreak.BREAK_WORD
    };
    
    let fontCollection = new text.FontCollection();
    let paragraphGraphBuilder = new text.ParagraphBuilder(myParagraphStyle, fontCollection);
    

    代码逻辑走读:

    1. 定义了一个名为 myTextStyleTextStyle对象,设置了字体大小。字体大小通过 uiContext?.fp2px(fontSize)进行转换,确保在不同设备上的一致性显示。
    2. 定义了一个名为myParagraphStyle的ParagraphStyle对象,配置了文本样式、对齐方式、最大行数、换行策略和单词断裂策略。
      • textStyle属性设置为 myTextStyle,应用了之前定义的文本样式。
      • align属性设置为 text.TextAlign.START,使文本左对齐。
      • maxLines属性设置为 300,定义了文本最多显示的行数,以防止过多的内容溢出显示区域。
      • breakStrategy属性设置为 text.BreakStrategy.GREEDY,定义了文本在达到最大行数时如何进行换行。
      • wordBreak属性设置为 text.WordBreak.BREAK_WORD,允许在单词中进行换行。
    3. 创建了一个空的 FontCollection对象,用于管理字体集合。
    4. 创建了一个 ParagraphBuilder对象,传入了 myParagraphStylefontCollection,用于构建段落。
  3. 添加占位符,指定样式。

    根据第一步骤解析出来的内容,如果是图片的话,就用addPlaceholder(),添加一张图片占位符,需要指定这张图片的大小(单位px),旁边的文字排版方式,文字基线位置等信息。

    paragraphGraphBuilder.addPlaceholder({
      width: item.imgWidth,
      height: item.imgHeight,
      align: text.PlaceholderAlignment.BOTTOM_OF_ROW_BOX,
      baseline: text.TextBaseline.IDEOGRAPHIC,
      baselineOffset: 0
    });
    

    代码逻辑走读:

    1. 调用 paragraphGraphBuilder.addPlaceholder方法,开始添加占位符。
    2. 设置占位符的宽度为 item.imgWidth,高度为 item.imgHeight
    3. 指定占位符的对齐方式为 text.PlaceholderAlignment.BOTTOM_OF_ROW_BOX,这意味着占位符将与行盒的底部对齐。
    4. 设置占位符的基线为 text.TextBaseline.IDEOGRAPHIC,这通常用于对齐中日文字的基线。
    5. 设定基线偏移量为 0,表示占位符的基线与其顶部对齐。

    如果是文字的话,就使用addText(),添加一段文本,添加这段文本之前可以重新通过pushStyle()方法指定这段文本的字体大小和颜色。

    paragraphGraphBuilder.pushStyle({
      fontSize: fontSize,
    });
    paragraphGraphBuilder.addText(item.content);
    

    代码逻辑走读:

    1. 调用pushStyle方法,传入一个对象{fontSize: fontSize},该对象指定了文本的字体大小。这一步是为了设置文本的样式。
    2. 调用addText方法,传入item.content,该方法将文本内容添加到图形构建器中。这一步是为了在图形构建器中添加实际的文本内容。

    如果需要使用之前的文本样式,可以通过popStyle()把当前样式pop出去。

    上面添加的文字大小和图片占位符的大小要同Text控件展示的时候的大小一致,否则会导致计算不准确。

  4. 预排版。

    全部添加完成之后,使用paragraph的layoutSync()方法预先排版,传递的大小单位也为px。这个layoutSync()传递宽度要同展示的时候的Text文本宽度一致,否则计算出来的和展示的时候肯定不一致。

    let paragraph = paragraphGraphBuilder.build();
    paragraph.layoutSync(textMaxWidth);
    

    代码逻辑走读:

    1. 构建段落图形对象
      • 调用 paragraphGraphBuilder.build()方法,创建一个段落图形对象并赋值给变量 paragraph。这个方法的具体实现细节不在此代码片段中展示,但其作用是生成一个段落图形对象。
    2. 同步布局段落图形
      • 调用 paragraph.layoutSync(textMaxWidth)方法,对刚刚创建的段落图形对象进行同步布局。textMaxWidth是一个参数,表示文本的最大宽度,布局过程会根据这个宽度来调整段落图形的显示。
  5. 计算截断坐标。

    计算三个点之前的最后一个文字的坐标。设这个字符变量为lastWord。经过paragraph的排版之后,就可以得到这段文本真实的渲染数据了。先通过paragraph.getLineCount(),计算出来一共排版了多少行,如果超过了自己要设定的行数,或者getLineCount()的行数和自己设定的maxLine一致,但是最后一行的宽度+之前计算出来的widthMore,超过了第四步骤设定的最大宽度,则说明需要截断。

    计算lastWord文字的Y坐标,通过getLineHeight()获取每一行的高度加起来,其中最后一行高度需要加一半的高度。

    for (let i = 0; i < textSectionAttribute.maxLines; i++) {
      y += i === textSectionAttribute.maxLines - 1 ? paragraph.getLineHeight(i) / 2 : paragraph.getLineHeight(i);
    }
    

    代码逻辑走读:

    1. 代码首先初始化一个变量 y用于存储总高度。

    2. 然后通过 for循环遍历 textSectionAttribute.maxLines指定的行数。

    3. 在循环中,通过条件判断

      i === textSectionAttribute.maxLines - 1

      来决定当前行的高度。

      • 如果是最后一行,则高度为 paragraph.getLineHeight(i) / 2
      • 否则,高度为 paragraph.getLineHeight(i)
    4. 每次循环结束时,将计算出的行高度累加到 y上。

    5. 最终,y存储了文本段落的总高度。

  6. 计算lastWord的X坐标。

    if (paragraph.getLineWidth(textSectionAttribute.maxLines - 1) + Number(widthMore) >
    textSectionAttribute.constraintWidth) {
      x = textSectionAttribute.constraintWidth - Number(widthMore);
    } else {
      x = paragraph.getLineWidth(textSectionAttribute.maxLines - 1)
    }
    

    代码逻辑走读:

    1. 首先,代码通过调用paragraph.getLineWidth方法获取文本段落的宽度,该宽度与文本段落的最大行数相关。
    2. 然后,代码将获取到的宽度与额外宽度(Number(widthMore))相加,并与文本段落的约束宽度进行比较。
    3. 如果相加的结果大于约束宽度,则将x设置为约束宽度减去额外宽度。
    4. 如果相加的结果不大于约束宽度,则将x设置为文本段落的宽度。
  7. 转换坐标对应索引。

    计算lastWord的展示索引位置。拿到lastWord的x与y坐标之后,通过getGlyphPositionAtCoordinate()拿到这个坐标的文字所在段落的索引,这个就是最终文字展示的索引。

    // The conversion coordinates correspond to the index
    let positionWithAffinity = paragraph.getGlyphPositionAtCoordinate(x, y);
    let index = 0;
    if (positionWithAffinity.affinity === text.Affinity.UPSTREAM) {
      index = positionWithAffinity.position;
    } else {
      index = positionWithAffinity.position + 1;
    }
    

    代码逻辑走读:

    1. 获取字形位置

      • 调用 paragraph.getGlyphPositionAtCoordinate(x, y)方法,根据提供的坐标 (x, y) 获取字形位置信息,并存储在 positionWithAffinity变量中。
    2. 初始化索引

      • 初始化 index变量为 0,用于存储最终的索引值。
    3. 确定索引值

      • 检查positionWithAffinity.affinity的值:

        • 如果 affinitytext.Affinity.UPSTREAM,说明坐标在字符的前面,索引值为 positionWithAffinity.position
        • 否则,说明坐标在字符的后面,索引值为 positionWithAffinity.position + 1
  8. 添加“展开”“收起”按钮。

    显示展开按钮时使用上面获取的shortContentArray数组数据来渲染,完全展示的时候使用RichTextModel里的textArray数组数据来渲染。判断两种情况分别显示按钮。

    if (this.textModifier.needProcess && !this.textModifier.exceedOneLine) {
      Span(this.lastSpanAttribute.content[0])
        .fontColor(this.lastSpanAttribute.color)
    } else if (this.textModifier.exceedOneLine) {
      Span(this.lastSpanAttribute.content[1])
        .fontColor(this.lastSpanAttribute.color)
    }
    

    代码逻辑走读:

    1. 首先检查 this.textModifier.needProcess是否为真且 this.textModifier.exceedOneLine 是否为假。

      • 如果条件满足,则创建一个Span对象,内容为this.lastSpanAttribute.content[0],并设置其字体颜色为this.lastSpanAttribute.color
    2. 如果上述条件不满足,则检查 this.textModifier.exceedOneLine是否为真。

      • 如果条件满足,则创建一个Span对象,内容为this.lastSpanAttribute.content[1],并设置其字体颜色为this.lastSpanAttribute.color

    this.textModifier.needProcess 是否为真且 this.textModifier.exceedOneLine是否为假。

    • 如果条件满足,则创建一个Span对象,内容为this.lastSpanAttribute.content[0],并设置其字体颜色为this.lastSpanAttribute.color
    1. 如果上述条件不满足,则检查 this.textModifier.exceedOneLine是否为真。

      • 如果条件满足,则创建一个Span对象,内容为this.lastSpanAttribute.content[1],并设置其字体颜色为this.lastSpanAttribute.color
    2. 如果上述两个条件都不满足,则不执行任何操作。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐