Skip to content

Latest commit

 

History

History
233 lines (184 loc) · 6.84 KB

File metadata and controls

233 lines (184 loc) · 6.84 KB

Custom Style Implementation Guide

Implementing a custom style involves the following steps. We use an @ mention styled string as an example of how users can implement their custom styles:

1. Define Custom Style Interface

Create an interface extending the core Extend class to declare style-specific properties. This serves as the contract for your custom style's data structure.

For @ mentions: Implement AtNodeExtend with necessary properties.

import { Extend } from 'rich_text_core'

export interface AtNodeExtend extends Extend {
  name: string
  hasBackground: boolean
}

2. Create Style Node Class

Extend the base Node class and implement your custom interface. This class will manage style state and property accessors.

For @ mentions: Build AtNode with necessary getter/setter methods.

import { Node } from 'rich_text_core'

export class AtNode extends Node implements AtNodeExtend {
  private _name: string = ""
  private _hasBackground: boolean = true

  constructor() {
    super()
  }

  public get hasBackground(): boolean {
    return this._hasBackground
  }

  public set hasBackground(value: boolean) {
    this._hasBackground = value
  }

  public get name(): string {
    return this._name
  }

  public set name(name: string) {
    this._name = name
  }
}

3. Declare Styled String Producer Interface

Define an interface requiring produceStyledString method implementation. This decouples style rendering logic from core text processing.

For @ mentions: Use IAtNodeStyledStringProducer with typed parseContext parameter.

import { Node } from 'rich_text_core';

export interface IAtNodeStyledStringProducer<T extends Node> {
  produceStyledString(parseContext: T): MutableStyledString;
}

4. Implement Custom Span Renderer

Subclass CustomSpan and override onMeasure/onDraw to handle layout and rendering. Include visual customization logic like background drawing.

For @ mentions: Create an implementation AtSpan, inheriting from CustomSpan.

import { common2D, drawing } from '@kit.ArkGraphics2D';
import measure from '@ohos.measure'

export class AtSpan extends CustomSpan {
  private textArea?: SizeOptions = undefined
  private width: number = 0
  private height: number = 0
  private word: string = ""
  hasBackground?: boolean = false
  content: string = ""

  constructor(content: string, hasBackground?: boolean) {
    super();
    this.content = content
    this.word = '@'.concat(content)
    this.textArea = getTextSize(this.word, '16vp')
    this.width = px2vp(Number(this.textArea.width))
    this.height = px2vp(Number(this.textArea.height))
    this.hasBackground = hasBackground
  }

  onMeasure(measureInfo: CustomSpanMeasureInfo): CustomSpanMetrics {
    return { width: this.width + px2vp(Number(25)), height: this.height + px2vp(Number(5)) }
  }

  onDraw(context: DrawContext, options: CustomSpanDrawInfo) {
    let canvas = context.canvas
    const brush = new drawing.Brush()

    this.drawBackground(canvas, brush, options)
    // Font settings
    const font = new drawing.Font()
    font.setSize(vp2px(16))
    const textBlob = drawing.TextBlob.makeFromString(this.word, font, drawing.TextEncoding.TEXT_ENCODING_UTF8)
    canvas.attachBrush(brush)

    brush.setColor(this.getTextBrushColor())
    canvas.attachBrush(brush)
    let startX = options.x + 5
    canvas.drawTextBlob(textBlob, startX,
      options.lineBottom - (vp2px(this.height + 4) - Number(getTextSize(this.word, '16vp').height)))
    canvas.detachBrush()
  }

  getTextBrushColor(): common2D.Color {
    if (this.hasBackground) {
      return {
        alpha: 255,
        red: 255,
        green: 255,
        blue: 255
      }
    } else {
      return {
        alpha: 255,
        red: 4,
        green: 66,
        blue: 210
      }
    }
  }

  drawBackground(canvas: drawing.Canvas, brush: drawing.Brush, options: CustomSpanDrawInfo) {
    if (this.hasBackground) {
      brush.setColor({
        alpha: 255,
        red: 23,
        green: 88,
        blue: 240
      });
      canvas.attachBrush(brush)
      let rect: common2D.Rect = {
        left: options.x,
        top: options.lineTop,
        right: options.x + Number(this.textArea?.width) + 20,
        bottom: options.lineBottom
      }
      let roundRect = new drawing.RoundRect(rect, vp2px(8), vp2px(8));
      canvas.drawRoundRect(roundRect)
      canvas.detachBrush()
    }
  }
}

export function getTextSize(content: string, fontSize: string): SizeOptions {
  const size = measure.measureTextSize({
    textContent: content,
    fontSize,
  })
  return size
}

5. Build Style Assembly Function

Create a factory method that constructs styled string segments by combining your Node and span implementations.

For @ mentions: Implement assembleAt to wrap AtNode data in AtSpan instances.

import { AtNode } from "../node/AtNode"
import { AtSpan } from "../span/AtSpan"

export function assembleAt(
  at: AtNode,
  baseString: MutableStyledString
) {
  let atSpan: AtSpan = new AtSpan(at.name, at.hasBackground)
  let atStr: MutableStyledString = new MutableStyledString(atSpan)
  baseString.appendStyledString(atStr)
}

6. Create Unified Style Producer

Develop a producer class that implements both your style-specific interface and the core BaseProducer. This bridges custom styles with the rendering pipeline.

For @ mentions: Use AtStyledStringProducer to coordinate assembleAt calls.

import { AtNode } from "../node/AtNode";
import { BaseProducer, RichTextExtendScope } from "rich_text_core";
import { assembleAt } from "../assemble/AtStyledString";
import { AtNodeExtend } from "../extend/AtNodeExtend";
import { IAtNodeStyledStringProducer } from "../produce/IAtNodeStyledStringProducer";

export class AtStyledStringProducer extends BaseProducer implements IAtNodeStyledStringProducer<AtNode>, RichTextExtendScope<AtNodeExtend> {
  produceStyledString(parseContext: AtNode): MutableStyledString {
    const atStyledString = new MutableStyledString("")
    assembleAt(parseContext, atStyledString)
    return atStyledString
  }
}

7. Implement Extension Entry Point

Subclass IExtensionRichText and override produceMutableStyledString to handle node type detection and producer delegation.

For @ mentions: Create AtExtensionRichText that activates for AtNode instances.

import { IExtensionRichText, Node } from "rich_text_core";
import { AtNode } from "./node/AtNode";
import { AtStyledStringProducer } from "./produce/AtStyledStringProducer";

export class AtExtensionRichText implements IExtensionRichText<AtNode> {
  produceMutableStyledString(node: Node): MutableStyledString | undefined{
    if (node instanceof AtNode) {
      const producer = new AtStyledStringProducer()
      return producer.produceStyledString(node)
    } else {
      return undefined
    }
  }
}