OpenHarmony开发者论坛

标题: OpenHarmony Text: 扶我起来 [打印本页]

作者: zmtzawqlp    时间: 3 天前
标题: OpenHarmony Text: 扶我起来
[md]## 前言

> OpenHarmony Text: 扶我起来!

对于文本来说,文本的溢出效果,即超出之后显示 `...`,在业务当中是经常碰到的。下面我们看看 `原生` 以及 `web` 端的支持情况。(如有不对,望指正。)

* 溢出效果的自定义,即希望溢出的效果不是单调的 `...`,而且可以指定任何效果。

| 平台        | ellipsis 自定义                                                         |
| ----------- | ----------------------------------------------------------------------- |
| android     | 不支持                                                                  |
| Ios         | 不支持                                                                  |
| web         | 不支持                                                                  |
| flutter     | 不支持([ExtendedText](https://juejin.cn/post/6955095562215489573) 支持) |
| OpenHarmony | `ellipsis`                                                              |

* 溢出效果的位置,即在开头,中间,还是结尾。

| 平台        | 开头                                                                    | 中间                                                                    | 结尾                           |
| ----------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------ |
| android     | `android:ellipsize = "start"  `                                        | `android:ellipsize = "middle"  `                                        | `android:ellipsize = "end" `  |
| Ios         | `NSLineBreakByTruncatingHead`                                           | `NSLineBreakByTruncatingMiddle`                                         | `NSLineBreakByTruncatingTail`  |
| web         | `text-overflow: ellipsis clip`                                          | 不支持                                                                  | `text-overflow: clip ellipsis` |
| flutter     | 不支持([ExtendedText](https://juejin.cn/post/6955095562215489573) 支持) | 不支持([ExtendedText](https://juejin.cn/post/6955095562215489573) 支持) | `TextOverflow.ellipsis`        |
| OpenHarmony | `EllipsisMode.START`                                                    | `EllipsisMode.MIDDLE`                                                   | `EllipsisMode.END`             |

光从支持情况来看,OpenHarmony 已经吊打其他平台了。 但是,支持也是有局限的。

* `ellipsis` 只支持字符串修改,不是任意的组件。
* `EllipsisMode.START` 和 `EllipsisMode.MIDDLE` 仅在单行超长文本生效。

[Flutter Text: 扶我起来](https://juejin.cn/post/6955095562215489573) 3年前,在 `Flutter` 上面基于 `Canvas` 实现了溢出效果的自定义和溢出效果位置的设置。这一次我把它们带到了OpenHarmony平台。

| ![text_demo.png](https://forums-obs.openharmony.c ... efr5ze5ds1f7be6.png "text_demo.png") |![overflow.png](https://forums-obs.openharmony.c ... u9tcosiaigotltl.png "overflow.png") |
| --- | --- |
|  |  |



## 安装

`ohpm install @candies/extended_text`

## 使用

完整例子: https://github.com/HarmonyCandie ... ets/pages/Index.ets

### 参数

| 参数                   |                              类型                               |                     描述                     |
| :--------------------- | :-------------------------------------------------------------: | :------------------------------------------: |
| text                   |                             string                              |                  字符串内容                  |
| textSpan               |                           InlineSpan                            |            用于创建特殊文本的基类            |
| joinZeroWidthSpace     |                             boolean                             | 是否添加零宽度的空白,对应英文等换行更加紧凑 |
| paragraphStyle         | text.ParagraphStyle (import { text } from "@kit.ArkGraphics2D") |                  文本的样式                  |
| overflowWidget         |                       TextOverflowWidget                        |               用于定义溢出效果               |
| specialTextSpanBuilder |                     SpecialTextSpanBuilder                      |               用于创建特殊文本               |
| fontCollection         |                       text.FontCollection                       |                  自定义字体                  |

```typescript
import { ColorUtils, ExtendedText } from '@candies/extended_text'
import { MySpecialTextSpanBuilder } from '../text/special/MySpecialTextSpanBuilder';

@Entry
@Component
struct Index {
  private specialTextSpanBuilder: MySpecialTextSpanBuilder = new MySpecialTextSpanBuilder();
  context: Context = getContext(this);
  content: string = MySpecialTextSpanBuilder.content;
  @State joinZeroWidthSpace: boolean = false;

  build() {
    Column() {
      ExtendedText({
        text: this.content,
        specialTextSpanBuilder: this.specialTextSpanBuilder,
        paragraphStyle: {
          textStyle: {
            color: ColorUtils.resourceColorTo2DColor($r('sys.color.font'), this.context),
            fontSize: vp2px(18),
          },
        },
        joinZeroWidthSpace: this.joinZeroWidthSpace,
      })
    }
  }
}
```

### InlineSpan

用于创建特殊文本的基类

| 参数       |      类型      |                    描述                    |
| :--------- | :------------: | :----------------------------------------: |
| style      | text.TextStyle |                 文本的样式                 |
| actualText |     string     | 该特殊文本的真实文本(不一定等于显示的文本) |
| start      |     number     |       该特殊文本位于整个文本中的位置       |

```typescript
export interface InlineSpanOptions {
  style?: text.TextStyle;
  actualText?: string;
  start?: number;
}
```

#### TextSpan

继承于 `InlineSpan` ,用于显示纯文本。

| 参数     |         类型         |               描述               |
| :------- | :-------------------: | :-------------------------------: |
| text     |        string        |            显示的文案            |
| children | `Array` | 作为子节点,类型是 `InlineSpan` |

```typescript
export interface TextSpanOptions extends InlineSpanOptions {
  text?: string;
  children?: Array<InlineSpan>;
}
```

#### PlaceholderSpan

用于占位符的 `Span`

| 参数           |           类型           |       描述       |
| :------------- | :-----------------------: | :--------------: |
| align          | text.PlaceholderAlignment | 占位符的对齐方式 |
| baseline       |     text.TextBaseline     | 占位符的基线类型 |
| baselineOffset |          number          | 位符的基线偏移量 |

```typescript
export interface PlaceholderSpanOptions extends InlineSpanOptions {
  /**
   * Alignment mode of placeholder.
   * @type { PlaceholderAlignment }
   * @syscap SystemCapability.Graphics.Drawing
   * @since 12
   */
  align?: text.PlaceholderAlignment;

  /**
   * Baseline of placeholder.
   * @type { TextBaseline }
   * @syscap SystemCapability.Graphics.Drawing
   * @since 12
   */
  baseline?: text.TextBaseline;

  /**
   * Baseline offset of placeholder.
   * @type { number }
   * @syscap SystemCapability.Graphics.Drawing
   * @since 12
   */
  baselineOffset?: number;
}
```

#### WidgetSpan

继承于 `PlaceholderSpan` ,用于显示显示组件。

| 参数        |            类型            |                   描述                   |
| :---------- | :------------------------: | :--------------------------------------: |
| builder     | WrappedBuilder<[ESObject]> |    用于显示组件的 `WrappedBuilder`    |
| builderArgs |          ESObject          | 用于显示组件的 `WrappedBuilder` 的参数 |
| hide        |          boolean          |            是否需要显示该组件            |

```typescript
export interface WidgetSpanOptions extends PlaceholderSpanOptions {
  builder: WrappedBuilder<[ESObject]>;
  builderArgs: ESObject;
  hide?: boolean,
}
```

注意: `buildHyperlink` 只能是全局的 `@Builder`

```typescript
@Builder
export function buildHyperlink(builderArgs: ESObject) {
  Row(){
     Hyperlink(builderArgs[0], builderArgs[1])
  }
}

  return new WidgetSpan({
    builder: wrapBuilder<[ESObject]>(buildHyperlink),
    builderArgs: [href, content],
    style: {
      fontSize: vp2px(18), color: extended_text.ColorUtils.resourceColorTo2DColor(Color.Blue),
      fontWeight: text.FontWeight.W600,
    },
    actualText: this.toString(),
    start: this.start,
  });
```

### OverflowWidget

用于自定义文本的溢出效果。

* 目前官方支持修改 `ellipsis` ,但是只能是字符串
* 目前官方支持 `ellipsisMode`,但是只支持单行文本

| 参数        |            类型            |                     描述                     |
| :---------- | :------------------------: | :------------------------------------------: |
| builder     | WrappedBuilder<[ESObject]> |    用于显示溢出组件的 `WrappedBuilder`    |
| builderArgs |          ESObject          | 用于显示溢出组件的 `WrappedBuilder` 的参数 |
| position    |    TextOverflowPosition    |   用于控制溢出组件的位置(start,middle,end)   |

```typescript
export enum TextOverflowPosition {
  start,
  middle,
  end,
}

export interface TextOverflowWidgetOptions {
  builder: WrappedBuilder<[ESObject]>;
  position?: TextOverflowPosition;
  builderArgs?: ESObject;
}
```

### SpecialTextSpanBuilder

帮助将字符串文本快速转换为特殊的 `InlineSpan`

#### SpecialText

下面的例子告诉你怎么创建一个 `$xxx$`

具体思路是对字符串进行进栈遍历,通过判断 `flag` 来判定是否是一个特殊字符。
例子:`$xxx$` ,以 `$` 开头并且以 `$` 结束,我们就认为它是一个 `$xxx$` 的特殊文本

```typescript
export class DollarText extends extended_text.SpecialText {
  static flag: string = '$';

  constructor(context: Context, start?: number, textStyle?: text.TextStyle,) {
    super(DollarText.flag, DollarText.flag, context, start, textStyle);
  }

  finishText(): extended_text.InlineSpan {
    let text = this.getContent();
    return new TextSpan({
      text: text,
      style: {
        fontSize: vp2px(18), color: extended_text.ColorUtils.resourceColorTo2DColor(Color.Orange),
      },
      actualText: this.toString(),
      start: this.start,
    });
  }
}
```

#### 特殊文本 Builder

创建属于你自己规则的 `Builder`,上面说了你可以继承 `SpecialText` 来定义各种各样的特殊文本。

* `build` 方法中,是通过具体思路是对字符串进行进栈遍历,通过判断 `flag` 来判定是否是一个特殊文本。
  感兴趣的,可以看一下 `SpecialTextSpanBuilder` 里面 `build` 方法的实现,当然你也可以写出属于自己的 `build` 逻辑
* `createSpecialText` 通过判断 `flag` 来判定是否是一个特殊文本

```typescript
import * as extended_text from '@candies/extended_text'
import { text } from "@kit.ArkGraphics2D"
import { DollarText } from './DollarText';
import { EmojiText } from './EmojiText';
import { LinkText } from './LinkText';

export class MySpecialTextSpanBuilder extends extended_text.SpecialTextSpanBuilder {
  createSpecialText(flag: string, index: number, context: Context,
    textStyle?: text.TextStyle | undefined): extended_text.SpecialText | null {
    if (this.isStart(flag, EmojiText.flag)) {
      return new EmojiText(context, index - (EmojiText.flag.length - 1), textStyle,);
    } else if (this.isStart(flag, DollarText.flag)) {
      return new DollarText(context, index - (DollarText.flag.length - 1), textStyle);
    } else if (this.isStart(flag, LinkText.flag)) {
      return new LinkText(context, index - (LinkText.flag.length - 1), textStyle);
    }
    return null;
  }
}
```

### RegExpSpecialTextSpanBuilder

当然,也提供了通过正则的方式,创建特殊文本的方式。

#### RegExpSpecialText

下面的例子告诉你怎么创建一个 `$xxx$`

你只需要继承 `RegExpSpecialText` , 并且写出来对应的正则表达式即可。

```typescript
import * as extended_text from '@candies/extended_text'
import { TextSpan } from '@candies/extended_text';
import { text } from "@kit.ArkGraphics2D"


export class RegExpDollarText extends extended_text.RegExpSpecialText {
  get regExp(): RegExp {
    return new RegExp('\\$(.+?)\\$', 'g');
  }
  finishText(start: number,
    match: RegExpExecArray,
    context: Context,
    textStyle?: text.TextStyle,): extended_text.InlineSpan {
    let text = match[0];
    return new TextSpan({
      text: text.replaceAll('$', ''),
      style: {
        fontSize: vp2px(18), color: extended_text.ColorUtils.resourceColorTo2DColor(Color.Orange),
      },
      actualText: this.toString(),
      start: start,
    });
  }
}
```

#### 特殊文本 Builder

将上一步创建的 `RegExpSpecialText`,放到 `regExps` 当中即可。

```typescript
import { RegExpSpecialTextSpanBuilder } from '@candies/extended_text';
import { RegExpEmojiText } from './EmojiText';
import { RegExpDollarText } from './DollarText';
import { RegExpLinkText } from './LinkText';


export class MyRegExpSpecialTextSpanBuilder extends RegExpSpecialTextSpanBuilder {
  get regExps() {
    return [
      new RegExpDollarText(),
      new RegExpEmojiText(),
      new RegExpLinkText(),
    ];
  }
}
```

## 实现

### ParagraphBuilder

实现的前提是对文本的计算,`Flutter` 平台是 `TextPainter`, OpenHarmony平台对应的 `api` 是 [ParagraphBuilder](https://developer.huawei.com/con ... V5#paragraphbuilder), 通过 [ParagraphStyle](https://developer.huawei.com/con ... t-V5#paragraphstyle) 和 [FontCollection](https://developer.huawei.com/con ... t-V5#fontcollection) 创建一个 `ParagraphBuilder`。

```typescript
let myTextStyle: text.TextStyle = {
    color: { alpha: 255, red: 255, green: 0, blue: 0 },
    fontSize: 33,
  };
  let myParagraphStyle: text.ParagraphStyle = {
    textStyle: myTextStyle,
    align: text.TextAlign.END,
  };
  let fontCollection = new text.FontCollection();
  let paragraphGraphBuilder = new text.ParagraphBuilder(myParagraphStyle, fontCollection);
```

#### ParagraphStyle

`ParagraphStyle` 的参数如下,各个平台基本一致。

| 名称               | 类型                                                                                                                                | 只读 | 可选 | 说明                                              |
| :----------------- | :---------------------------------------------------------------------------------------------------------------------------------- | :--- | :--- | :------------------------------------------------ |
| textStyle          | [TextStyle](https://developer.huawei.com/con ... s-text-V5#textstyle)                   | 是   | 是   | 作用于整个段落的文本样式,默认为初始的TextStyle。 |
| textDirection      | [TextDirection](https://developer.huawei.com/con ... xt-V5#textdirection)           | 是   | 是   | 文本方向,默认为LTR。                             |
| align              | [TextAlign](https://developer.huawei.com/con ... s-text-V5#textalign)                   | 是   | 是   | 文本对齐方式,默认为START。                       |
| wordBreak          | [WordBreak](https://developer.huawei.com/con ... s-text-V5#wordbreak)                   | 是   | 是   | 断词类型,默认为BREAK_WORD。                      |
| maxLines           | number                                                                                                                              | 是   | 是   | 最大行数限制,整数,默认为1e9。                   |
| breakStrategy      | [BreakStrategy](https://developer.huawei.com/con ... xt-V5#breakstrategy)           | 是   | 是   | 断行策略,默认为GREEDY。                          |
| strutStyle         | [StrutStyle](https://developer.huawei.com/con ... -text-V5#strutstyle)                 | 是   | 是   | 支柱样式,默认为初始的StrutStyle。                |
| textHeightBehavior | [TextHeightBehavior](https://developer.huawei.com/con ... #textheightbehavior) | 是   | 是   | 文本高度修饰符模式,默认为ALL。                   |

#### FontCollection

`FontCollection` 可以用于获取全局字体实例或者加载自定义字体。

* 获取应用全局FontCollection的实例

```typescript
static getGlobalInstance(): FontCollection
```

* 同步接口,将路径对应的文件,以 `name` 作为使用的别名,加载成自定义字体。其中参数 `name` 对应的值需要在[TextStyle](https://developer.huawei.com/con ... s-text-V5#textstyle)中的 `fontFamilies` 属性配置,才能显示自定义的字体效果。支持的字体文件格式包含:`ttf`、`otf`。

```typescript
loadFontSync(name: string, path: string | Resource): void
```

* 清理字体排版缓存(字体排版缓存本身设有内存上限和清理机制,所占内存有限,如无内存要求,不建议清理)。

```typescript
clearCaches(): void
```

#### 插入文字

用于向正在构建的文本段落中插入具体的文本字符串。

```typescript
addText(text: string): void
```

#### 插入占位符

用于在构建文本段落时插入占位符,这是我们用来插入 `WidgtSpan` 对应的组件。

```typescript
addPlaceholder(placeholderSpan: PlaceholderSpan): void
```

#### 插入具体的符号

用于向正在构建的文本段落中插入具体的符号。

```typescript
addSymbol(symbolId: number): void
```

#### 更新文本的样式

```typescript
pushStyle(textStyle: TextStyle): void

popStyle(): void
```

更新当前文本块的样式 ,直到对应的 [popStyle](https://developer.huawei.com/con ... cs-text-V5#popstyle) 操作被执行,会还原到上一个文本样式。

就是说如果我们某个时刻 `push` 了一个新的 `style`,那么之后的样式都将应用到 `addText` , `addPlaceholder`, `addSymbol` 方法,直到我们调用 `popStyle`。

#### 完成段落的构建过程

```typescript
build(): Paragraph
```

用于完成段落的构建过程,生成一个可用于后续排版渲染的段落对象。

### Paragraph

通过 `ParagraphGraphBuilder` ,我们创建了对应的 [Paragraph](https://developer.huawei.com/con ... s-text-V5#paragraph))。

```typescript
let paragraph = paragraphGraphBuilder.build();
```

这些 `api` 都很熟悉,各个平台基本一致。

![屏幕截图2024-11-17180153.png](https://forums-obs.openharmony.c ... s5kparpek6ookoo.png "屏幕截图 2024-11-17 180153.png")


#### layoutSync

`layoutSync` 传入宽度,之后我们就获取文本的各种数据。

#### 计算溢出

`didExceedMaxLines` 和 `getHeight` `2` 个方法组合,就可以判断该文本是否溢出。

* 如果容器的高度小于文本高度,或者文本超出了限制的行数,属于溢出。
* 如果容器的宽度小于文本宽度,属于溢出。

```typescript
_didVisualOverflow(paragraph: text.Paragraph, constraint: ConstraintSizeOptions): boolean {
  let textSize: SizeResult = {
    width: px2vp(paragraph.getMaxWidth()),
    height: px2vp(paragraph.getHeight()),
  };
  let size: SizeResult = {
    width: constraint.maxWidth! as number,
    height: constraint.maxHeight! as number,
  }
  let textDidExceedMaxLines =
    paragraph.didExceedMaxLines();
  let
    didOverflowHeight =
      size.height < textSize.height || textDidExceedMaxLines;
  let
    didOverflowWidth = size.width < textSize.width;
  let hasVisualOverflow = didOverflowWidth || didOverflowHeight;
  return hasVisualOverflow;
}
```

### 绘制到画布

在画布上以坐标点 (x, y) 为左上角位置绘制文本。

```typescript
paint(canvas: drawing.Canvas, x: number, y: number): void
```

### 布局

上面我们讲了在OpenHarmony上面我们怎么创建文本,计算文本,绘制文本。接下来,我们需要处理的是,我们怎么获取到容器的宽高,`WidgetSpan` 的大小获取,以及布局。我们整个布局时由一个 `NodeContainer` 和 `ExtendedParagraph` 组成,一个负责绘制文本,一个负责计算 `OverflowWidget` 和文本中的 `WidgetSpan` 的位置以及放置它们。

`OverflowWidget` 和文本中的 `WidgetSpan` ,都在 `ExtendedParagraph` 中布局计算绘制,而 `NodeContainer` 绘制文本的过程中,我们需要将 `OverflowWidget` 组件所在区域的文本裁剪掉。

```typescript
build(){
  Stack(){
    NodeContainer(this.myNodeController)
    ExtendedParagraph()
  }
}
```

#### NodeContainer

```typescript
build(){
  Stack(){
    NodeContainer(this.textNodeController)
  }
}
```

[NodeContainer](https://developer.huawei.com/con ... ts-nodecontainer-V5) 用于绘制文本。

主要的过程是: 当需要绘制的时候,将计算好的 `Paragraph` 和 `overflowClipRects` 带入 `TextRenderNode` ,然后添加到 `TextNodeController` 中。`overflowClipRects` 即为需要裁剪的文本区域,这个区域放置了 `OverflowWidget`。

```typescript
class TextRenderNode extends RenderNode {
  constructor(paragraph: text.Paragraph, overflowClipRects: Array<common2D.Rect>) {
    super();
    this.paragraph = paragraph;
    this.overflowClipRects = overflowClipRects;
  }

  paragraph: text.Paragraph;
  overflowClipRects: Array<common2D.Rect>;

  async draw(context: DrawContext) {
    if (this.overflowClipRects.length != 0) {
      context.canvas.saveLayer();

      for (let index = 0; index < this.overflowClipRects.length; index++) {
        const overflowClipRect = this.overflowClipRects[index];
        context.canvas.clipRect(overflowClipRect, drawing.ClipOp.DIFFERENCE);
      }
    }
    this.paragraph.paint(context.canvas, 0, 0);
  
    if (this.overflowClipRects.length != 0) {
      context.canvas.restore();
    }
  }
}

class TextNodeController extends NodeController {
  private rootNode: FrameNode | null = null;

  makeNode(uiContext: UIContext): FrameNode {
    return this.rootNode ??= new FrameNode(uiContext)
  }

  addNode(node: RenderNode): void {
    if (this.rootNode == null) {
      return
    }
    const renderNode = this.rootNode.getRenderNode()
    if (renderNode != null) {
      renderNode.appendChild(node)
    }
  }

  clearNodes(): void {
    if (this.rootNode == null) {
      return
    }
    const renderNode = this.rootNode.getRenderNode()
    if (renderNode != null) {
      renderNode.clearChildren()
    }
  }
}
```

#### 自定义组件的自定义布局

OpenHarmony中,可以通过自定义布局代码,来重新布局子组件的位置。

[自定义组件的自定义布局-自定义组件-ArkTS组件-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者](https://developer.huawei.com/con ... 5#onplacechildren10)

以下是一个简单的例子, `onMeasureSize` 用于计算得到子组件的大小,`onPlaceChildren` 用于将子组件放置到对应的位置。

```typescript
@Component
export struct CustomLayout {
  @Builder
  buildChildren() {
    ForEach([1, 2, 3], (item: number, index: number) => {
      Text('S' + item)
        .fontSize(20)
        .width(60 + 10 * index)
        .height(100)
        .borderWidth(2)
        .margin({ left: 10 })
        .padding(10)
    })
  }

  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>,
    constraint: ConstraintSizeOptions): SizeResult {

    for (let index = 0; index < children.length; ++index) {
      let child = children[index];
      let childResult: MeasureResult = child.measure({
        minHeight: constraint.minHeight,
        minWidth: constraint.minWidth,
        maxWidth: constraint.maxWidth,
        maxHeight: constraint.maxHeight
      })
    }

    // 根据自己的情况返回整个容器的最终大小
    return {
      width: 100,
      height: 100,
    };
  }

  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
    for (let index = 0; index < children.length; ++index) {
      let child = children[index];
      let x = 0;
      let y = 0;
      // 如果不想显示子组件,将它布局到较偏的位置,达到不显示的目的
      // child.layout({ x: Infinity, y: Infinity });
      child.layout({ x: x, y: y })
    }
  }

  build() {
    this.buildChildren()
  }
}
```

##### buildChildren

在 `buildChildren` 方法中返回,需要布局的子组件。这里包括文本全部的占位组件,以及溢出效果组件。

在我们这里,需要布局的是文本中的 `WidgetSpan` 和 `OverflowWidget` 。

```typescript
@Builder
builder() {
  ForEach([...WidgetSpan.extractFromInlineSpan(this.text),
    ...(this.overflowWidget == undefined ? [] : [this.overflowWidget])], (item: WidgetBuilder, index: number) => {
    item.builder.builder(item.builderArgs)
  })
};
```

##### onMeasureSize

```typescript
onMeasureSize?(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions): SizeResult
```

`ArkUI` 框架会在自定义组件确定尺寸时,将该自定义组件的节点信息和尺寸范围通过 `onMeasureSize` 传递给该开发者。不允许在 `onMeasureSize` 函数中改变状态变量。

* 首先我们将子组件都 `measure`,获取到它们的真实大小。

```
let childCount = this.overflowWidget != null ? children.length - 1 : children.length;

let index = 0;
let dimensions = new Array<MeasureResult>();
for (; index < childCount; index++) {
  const child = children[index];
  let childResult: MeasureResult = child.measure({})
  let margin = child.getMargin();
  dimensions.push({
    width: vp2px(childResult.width + margin.start + margin.end),
    height: vp2px(childResult.height + margin.top + margin.bottom),
  });
}
```

* 通过 `InlineSpan ` `build` 方法,将文本和 `WidgetSpan` 占位添加到 `paragraphGraphBuilder` 中。

```typescript
this.text?.build(paragraphGraphBuilder, dimensions);
let paragraph = paragraphGraphBuilder.build();
paragraph.layoutSync(vp2px(maxWidth));
```

* 接下来就是通过 `paragraph`,来计算是否溢出,根据用户的设置,计算出来 `OverflowWidget` 组件所在的位置。
* 将计算好的 `paragraph` 和需要裁剪的区域,回调出去。

```
this.onDrawText(paragraph, this._overflowClipRects);
```

##### onPlaceChildren

```typescript
onPlaceChildren?(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions):void
```

`ArkUI` 框架会在自定义组件布局时,将该自定义组件的子节点自身的尺寸范围通过 `onPlaceChildren` 传递给该自定义组件。不允许在 `onPlaceChildren` 函数中改变状态变量。

```typescript
onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
  if (children.length == 0) {
    return;
  }

  let paragraph = this._paragraph!;
  let rects = paragraph.getRectsForPlaceholders();
  let index = 0;

  for (; index < rects.length; index++) {
    let rect = rects[index].rect;
    rect = {
      left: px2vp(rect.left),
      right: px2vp(rect.right),
      top: px2vp(rect.top),
      bottom: px2vp(rect.bottom),
    }
    let child = children[index];
    let margin = child.getMargin();
    let x = rect.left - margin.start;
    let y = rect.top - margin.top;

    if ((this._overflowWidgetRect != null && RectUtils.hasIntersection(rect, this._overflowWidgetRect!)) ||
      // hide widget
      (rect.right == rect.left && rect.top == rect.bottom)
    ) {
      // 不显示
      x = Infinity;
      y = Infinity;
    }
    child.layout({ x: x, y: y });
  }

  if (this.overflowWidget != null) {
    // overflow widget
    let child = children[children.length-1];
    if (this._overflowWidgetRect != null) {
      child.layout({
        x: this._overflowWidgetRect.left,
        y: this._overflowWidgetRect.top,
      })
    } else {
      // move out the screen
      child.layout({ x: Infinity, y: Infinity })
    }
  }
}
```

* 通过 `paragraph.getRectsForPlaceholders`,获取到全部的占位组件的位置,通过 `layout` 方法将它们放置到对应的位置。
* 放置 `overflowWidget` 组件。

### 文本溢出算法

通过[二分查找](https://github.com/HarmonyCandie ... /Paragraph.ets#L431),找到 `不溢出` 和 `溢出` 临界点 `X`。

| 开头      | 中间                                  | 结尾                     |
| --------- | ------------------------------------- | ------------------------ |
| `[0,X]` | `[m,X]`,`m` 为中间文本的索引位置 | 文本不需要改变,无需计算 |

该 `Range` 范围内的文本将不会显示。

比如 `abcdef`, 我们找到的 `Range` 为  `[2,3]` ,即最终显示 `ab...ef`。

考虑以后还要支持选择和复制,所以我们这里不能简单丢掉 `cd`。我们将会保存当前 `span` 的开始 `index` 和实际的文本。

> 你见到的并不是真实的

```typescript
TextSpan(
   text: 'abef',
   actualText: 'abcdef',
   start: 0,
  );
```

## 一些注意的点

### `Canvas`和组件单位不一致

`Canvas` 中的宽高,字号,距离等单位为 `px` , 组件中的宽高,字号,距离等单位为 `vp`。

* `px2vp` `Canvas` 到组件,我们需要调用 `px2vp` 进行转换。
* `vp2px` 组件 到 `Canvas`,我们需要调用 `vp2px` 进行转换。

### 属性不触发刷新

属性不在 `build` 方法体里面,当属性变化的时候不会触发刷新。

```typescript
@Watch('reBuildTextSpan') @Prop specialTextSpanBuilder?: SpecialTextSpanBuilder;
@Watch('reBuildTextSpan') @Prop text?: string;
@Watch('reBuildTextSpan') @Prop textSpan?: InlineSpan;
@Watch('reBuildTextSpan') @Prop joinZeroWidthSpace: boolean = false;
private _building: boolean = false;

reBuildTextSpan(propName: string) {
  if (!this._building) {
    this.myNodeController.clearNodes();
    this._building = true;
    this._refreshTag = !this._refreshTag;
  }
}

@State private _refreshTag: boolean = false;
```

我们使用新的一个属性 `_refreshTag`来控制刷新,反复设置来触发 `ui` 的重新刷新。

```typescript
build() {
  Stack() {
    NodeContainer(this.myNodeController)
    if (this._refreshTag) {
      ExtendedParagraph({
        overflowWidget: this.overflowWidget,
        paragraphStyle: this.paragraphStyle,
        fontCollection: this.fontCollection,
        text: this._buildTextSpan(),
        onDrawText: (paragraph, overflowClipRects: Array<common2D.Rect>) => {
          this.myNodeController.clearNodes();
          let node = new MyRenderNode(paragraph, overflowClipRects);

          node.size = {
            width: paragraph.getMaxWidth(),
            height: paragraph.getHeight(),
          }
          this.myNodeController.addNode(node);
          this._building = false;
        }
      }).align(Alignment.Top).width('100%')
    } else {
      ExtendedParagraph({
        overflowWidget: this.overflowWidget,
        paragraphStyle: this.paragraphStyle,
        fontCollection: this.fontCollection,
        text: this._buildTextSpan(),
        onDrawText: (paragraph, overflowClipRects: Array<common2D.Rect>) => {
          this.myNodeController.clearNodes();
          let node = new MyRenderNode(paragraph, overflowClipRects);

          node.size = {
            width: paragraph.getMaxWidth(),
            height: paragraph.getHeight(),
          }
          this.myNodeController.addNode(node);
          this._building = false;
        }
      }).align(Alignment.Top).width('100%')
    }
  }
}
```

## 结语

如果只是牵扯到 `Canvas` ,不同平台的迁移还是蛮简单的。目前,[Harmony Candies](https://github.com/HarmonyCandies) 主要维护的是OpenHarmony组件以及 `Flutter` OpenHarmony插件。

* [OpenHarmony组件](https://ohpm.openharmony.cn/#/cn ... age=1&q=candies) 在 https://ohpm.openharmony.cn/#/cn ... age=1&q=candies
* [Flutter OpenHarmony插件](https://pub-web.flutter-io.cn/pu ... andies.com/packages) 在 https://pub-web.flutter-io.cn/pu ... andies.com/packages

欢迎更多的开发者加入我们,不管你之前是做什么的,社区期待着你的加入。

爱 `OpenHarmony`,爱 `糖果`,欢迎加入[Harmony Candies](https://github.com/HarmonyCandies),一起生产可爱的OpenHarmony小糖果 QQ 群: 981630644

关注微信公众号 `糖果代码铺` ,获取更多 `OpenHarmony/Flutter` 动态信息。

![candies.png](https://forums-obs.openharmony.c ... vlsnw4ifsic5ikk.png "candies.png")


## 相关阅读

* [Flutter Text: 扶我起来](https://juejin.cn/post/6955095562215489573)
* [Flutter Love OpenHarmony - 掘金 (juejin.cn)](https://juejin.cn/post/7281948788483489804)
* [不是OpenHarmony ArkUI 不会写,而是 Flutter 更有性价比 - 掘金 (juejin.cn)](https://juejin.cn/post/7329110277172985908)
* [Flutter OpenHarmony化 在一起 就可以 - 掘金 (juejin.cn)](https://juejin.cn/post/7364698043910930443)
* [Flutter到OpenHarmony,不是有手就行吗? (下拉刷新) - 掘金 (juejin.cn)](https://juejin.cn/post/7313161940283998260)
* [Flutter到OpenHarmony,不是有手就行吗? (列表加载更多) - 掘金 (juejin.cn)](https://juejin.cn/post/7314189771378458659)
* [Flutter到OpenHarmony,不是有手就行吗? (仿掘金点赞按钮) - 掘金 (juejin.cn)](https://juejin.cn/post/7330945097403220002)
* [OpenHarmony,满天星光,共赴璀璨星河  - 掘金 (juejin.cn)](https://juejin.cn/post/7384992816907042825)
* [OpenHarmony Next 1000万点赞以内最好的图片裁剪组件](https://juejin.cn/post/7435658656714276916)

```

```

[/md]




欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/) Powered by Discuz! X3.5