• Lv0
    粉丝4

积分140 / 贡献0

提问1答案被采纳0文章10

[经验分享] OpenHarmony Text: 扶我起来 原创 精华

zmtzawqlp 显示全部楼层 发表于 3 天前

前言

OpenHarmony Text: 扶我起来!

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

  • 溢出效果的自定义,即希望溢出的效果不是单调的 ...,而且可以指定任何效果。
平台 ellipsis 自定义
android 不支持
Ios 不支持
web 不支持
flutter 不支持(ExtendedText 支持)
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 支持) 不支持(ExtendedText 支持) TextOverflow.ellipsis
OpenHarmony EllipsisMode.START EllipsisMode.MIDDLE EllipsisMode.END

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

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

Flutter Text: 扶我起来 3年前,在 Flutter 上面基于 Canvas 实现了溢出效果的自定义和溢出效果位置的设置。这一次我把它们带到了OpenHarmony平台。

text_demo.png overflow.png

安装

ohpm install @candies/extended_text

使用

完整例子: https://github.com/HarmonyCandies/extended_text/blob/main/entry/src/main/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 自定义字体
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 该特殊文本位于整个文本中的位置
export interface InlineSpanOptions {
  style?: text.TextStyle;
  actualText?: string;
  start?: number;
}

TextSpan

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

参数 类型 描述
text string 显示的文案
children Array 作为子节点,类型是 InlineSpan
export interface TextSpanOptions extends InlineSpanOptions {
  text?: string;
  children?: Array<InlineSpan>;
}

PlaceholderSpan

用于占位符的 Span

参数 类型 描述
align text.PlaceholderAlignment 占位符的对齐方式
baseline text.TextBaseline 占位符的基线类型
baselineOffset number 位符的基线偏移量
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 是否需要显示该组件
export interface WidgetSpanOptions extends PlaceholderSpanOptions {
  builder: WrappedBuilder<[ESObject]>;
  builderArgs: ESObject;
  hide?: boolean,
}

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

@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)
export enum TextOverflowPosition {
  start,
  middle,
  end,
}

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

SpecialTextSpanBuilder

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

SpecialText

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

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

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 来判定是否是一个特殊文本
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 , 并且写出来对应的正则表达式即可。

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 当中即可。

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平台对应的 apiParagraphBuilder, 通过 ParagraphStyleFontCollection 创建一个 ParagraphBuilder

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 作用于整个段落的文本样式,默认为初始的TextStyle。
textDirection TextDirection 文本方向,默认为LTR。
align TextAlign 文本对齐方式,默认为START。
wordBreak WordBreak 断词类型,默认为BREAK_WORD。
maxLines number 最大行数限制,整数,默认为1e9。
breakStrategy BreakStrategy 断行策略,默认为GREEDY。
strutStyle StrutStyle 支柱样式,默认为初始的StrutStyle。
textHeightBehavior TextHeightBehavior 文本高度修饰符模式,默认为ALL。

FontCollection

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

  • 获取应用全局FontCollection的实例
static getGlobalInstance(): FontCollection
  • 同步接口,将路径对应的文件,以 name 作为使用的别名,加载成自定义字体。其中参数 name 对应的值需要在TextStyle中的 fontFamilies 属性配置,才能显示自定义的字体效果。支持的字体文件格式包含:ttfotf
loadFontSync(name: string, path: string | Resource): void
  • 清理字体排版缓存(字体排版缓存本身设有内存上限和清理机制,所占内存有限,如无内存要求,不建议清理)。
clearCaches(): void

插入文字

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

addText(text: string): void

插入占位符

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

addPlaceholder(placeholderSpan: PlaceholderSpan): void

插入具体的符号

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

addSymbol(symbolId: number): void

更新文本的样式

pushStyle(textStyle: TextStyle): void

popStyle(): void

更新当前文本块的样式 ,直到对应的 popStyle 操作被执行,会还原到上一个文本样式。

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

完成段落的构建过程

build(): Paragraph

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

Paragraph

通过 ParagraphGraphBuilder ,我们创建了对应的 Paragraph)。

let paragraph = paragraphGraphBuilder.build();

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

屏幕截图2024-11-17180153.png

layoutSync

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

计算溢出

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

  • 如果容器的高度小于文本高度,或者文本超出了限制的行数,属于溢出。
  • 如果容器的宽度小于文本宽度,属于溢出。
_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) 为左上角位置绘制文本。

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

布局

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

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

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

NodeContainer

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

NodeContainer 用于绘制文本。

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

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开发者

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

@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 方法中返回,需要布局的子组件。这里包括文本全部的占位组件,以及溢出效果组件。

在我们这里,需要布局的是文本中的 WidgetSpanOverflowWidget

@Builder
builder() {
  ForEach([...WidgetSpan.extractFromInlineSpan(this.text),
    ...(this.overflowWidget == undefined ? [] : [this.overflowWidget])], (item: WidgetBuilder, index: number) => {
    item.builder.builder(item.builderArgs)
  })
};
onMeasureSize
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 中。
this.text?.build(paragraphGraphBuilder, dimensions);
let paragraph = paragraphGraphBuilder.build();
paragraph.layoutSync(vp2px(maxWidth));
  • 接下来就是通过 paragraph,来计算是否溢出,根据用户的设置,计算出来 OverflowWidget 组件所在的位置。
  • 将计算好的 paragraph 和需要裁剪的区域,回调出去。
this.onDrawText(paragraph, this._overflowClipRects);
onPlaceChildren
onPlaceChildren?(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions):void

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

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 组件。

文本溢出算法

通过二分查找,找到 不溢出溢出 临界点 X

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

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

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

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

你见到的并不是真实的

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

一些注意的点

Canvas和组件单位不一致

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

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

属性不触发刷新

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

@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 的重新刷新。

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 主要维护的是OpenHarmony组件以及 Flutter OpenHarmony插件。

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

OpenHarmony,爱 糖果,欢迎加入Harmony Candies,一起生产可爱的OpenHarmony小糖果 QQ 群: 981630644

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

candies.png

相关阅读

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部