HarmonyOS鸿蒙Next中如何将kotlin自定义高亮文本组件转换成ArkTS

HarmonyOS鸿蒙Next中如何将kotlin自定义高亮文本组件转换成ArkTS

class HighlightTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    companion object {
        const val HIGHLIGHT_RECT = 0 // 矩形高亮
        const val HIGHLIGHT_ROUND = 1 // 圆形高亮
    }

    init {
        paint.style = Paint.Style.FILL
    }

    private data class HighlightInfo(
      val highlightText: String,
      val highlightColor: Int,
      val highlightStyle: Int,
      var matchAll: Boolean,
    )
    private val highlights = mutableListOf<HighlightInfo>()

    fun addHighlight(text: String, color: Int, style: Int, matchAll: Boolean) {
      highlights.add(HighlightInfo(text, color, style, matchAll))
      invalidate()
    }

    fun clearHighlights() {
      highlights.clear()
      invalidate()
    }


  override fun onDraw(canvas: Canvas) {
    val content = text.toString()
    // 获取文字的Paint参数
//    val textPaint = getPaint()
    // 获取文字的度量信息
//    val fontMetrics = textPaint.fontMetrics

    // 遍历所有高亮区域
    for (highlight in highlights) {
      var searchStart = 0
      while (searchStart < content.length) {
        // 直接查找完整匹配,包含换行符
        val matchStart = content.indexOf(highlight.highlightText, searchStart)
        if (matchStart == -1) break

        val matchEnd = matchStart + highlight.highlightText.length

        // 找到匹配,绘制高亮
        val highlightViewLayout = layout
        if (highlightViewLayout != null) {
          // 获取起始位置所在的行
          val startLine = highlightViewLayout.getLineForOffset(matchStart)
          // 获取结束位置所在的行
          val endLine = highlightViewLayout.getLineForOffset(matchEnd - 1)

          // 遍历每一行进行绘制
          for (line in startLine..endLine) {
            // 计算当前行需要高亮的起始和结束位置
            val lineStart = highlightViewLayout.getLineStart(line)
            val lineEnd = highlightViewLayout.getLineEnd(line)

            // 计算当前行实际需要高亮的范围
            val highlightLineStart = maxOf(matchStart, lineStart)
            // 调整行尾位置,如果是换行符则不包含它
            val adjustedLineEnd = if (content.getOrNull(lineEnd - 1) == '\n') {
              lineEnd - 1
            } else {
              lineEnd
            }
            val highlightLineEnd = minOf(matchEnd, adjustedLineEnd)

            // 只有当当前行包含需要高亮的内容时才绘制
            if (highlightLineStart <= highlightLineEnd) {
              // 获取当前行文字的水平位置
              val startX = highlightViewLayout.getPrimaryHorizontal(highlightLineStart)
              // 使用当前行的实际结束位置
              val endX = if (highlightLineEnd == lineEnd) {
                // 如果是行尾,使用行宽
                highlightViewLayout.getLineWidth(line).toFloat()
              } else {
                highlightViewLayout.getPrimaryHorizontal(highlightLineEnd)
              }
              val lineTop = highlightViewLayout.getLineTop(line)
              val lineBottom = highlightViewLayout.getLineBottom(line)

              paint.color = highlight.highlightColor

              // 添加适当的padding
              // 计算文字的实际高度
              val lineHeight = lineBottom - lineTop
//              MyLogger.d("lxp", "startX: $startX endX: $endX lineTop: $lineTop lineBottom: $lineBottom")
//              MyLogger.d("lxp", "fontMetrics: ascent: ${fontMetrics.ascent} descent:${fontMetrics.descent} top: ${fontMetrics.top} bottom: ${fontMetrics.bottom}")

              when (highlight.highlightStyle) {
                HIGHLIGHT_RECT -> {
                  val verticalPadding = lineHeight * 0.45f  // 可以调整这个值来控制padding
                  canvas.drawRect(
                    startX + paddingLeft,
                    lineTop.toFloat() + verticalPadding,
                    endX + paddingLeft,
                    lineBottom.toFloat(),
                    paint
                  )
                }

                HIGHLIGHT_ROUND -> {
                  val horizontalPadding = paddingLeft
                  // 遍历每个字符,分别绘制圆形背景
                  for (i in highlightLineStart until highlightLineEnd) {
                    val charStart = highlightViewLayout.getPrimaryHorizontal(i)
                    val charEnd = highlightViewLayout.getPrimaryHorizontal(i + 1)
                    val charWidth = charEnd - charStart
                    val centerX = (charStart + charEnd) / 2
                    val centerY = (lineTop + lineBottom) / 2f

                    val radius = (charWidth * 1.1f) / 2f

                    canvas.drawCircle(
                      centerX + horizontalPadding,
                      centerY,
                      radius,
                      paint
                    )
                  }
                }
              }
            }
          }
          if (highlight.matchAll.not()) {
            //只匹配第一个
           break
          }
          searchStart = matchEnd
        } else {
          break
        }
      }

    }
    super.onDraw(canvas)
  }
}

更多关于HarmonyOS鸿蒙Next中如何将kotlin自定义高亮文本组件转换成ArkTS的实战教程也可以访问 https://www.itying.com/category-93-b0.html

3 回复

搞张效果图比放代码强

更多关于HarmonyOS鸿蒙Next中如何将kotlin自定义高亮文本组件转换成ArkTS的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,将Kotlin自定义高亮文本组件转换为ArkTS,需使用ArkUI声明式语法。Kotlin的View组件对应ArkTS的@Component自定义组件。文本高亮功能可通过Text组件的decoration属性设置,例如使用DecorationStyle的type属性指定下划线,color属性设置颜色。若需更复杂的高亮逻辑,可在自定义组件内使用@State装饰器管理状态,结合条件渲染或样式绑定实现动态高亮效果。直接重构为ArkTS组件即可,无需涉及Java或C语言。

在HarmonyOS Next中,将Kotlin自定义高亮文本组件转换为ArkTS,核心是利用ArkUI的声明式UI和自定义组件能力。原Kotlin View的onDraw绘制逻辑需要转换为ArkTS的@Builder绘制函数。

以下是关键转换步骤和ArkTS实现要点:

1. 组件结构定义 使用@Component定义自定义组件,通过@Prop@State接收高亮配置。

@Component
export struct HighlightText {
  @State message: string = '';
  private highlights: HighlightInfo[] = [];

  // 高亮信息结构
  class HighlightInfo {
    text: string;
    color: ResourceColor;
    style: HighlightStyle;
    matchAll: boolean;
  }

  enum HighlightStyle {
    RECT,
    ROUND
  }
}

2. 绘制逻辑转换aboutToAppear@Builder中实现文本测量和高亮区域计算:

@Builder
private DrawHighlight(canvas: Canvas) {
  this.highlights.forEach((highlight) => {
    // 1. 使用TextMetrics测量文本
    const textMetrics = new TextMetrics(this.message);
    
    // 2. 查找匹配位置(支持matchAll逻辑)
    let searchStart = 0;
    while (searchStart < this.message.length) {
      const matchStart = this.message.indexOf(highlight.text, searchStart);
      if (matchStart === -1) break;
      
      const matchEnd = matchStart + highlight.text.length;
      
      // 3. 计算行位置和绘制区域
      const lineInfo = textMetrics.getLineInfoForOffset(matchStart);
      // ... 跨行处理逻辑
      
      // 4. 根据style绘制
      if (highlight.style === HighlightStyle.RECT) {
        canvas.drawRect({
          // 矩形坐标计算
        });
      } else {
        // 圆形绘制逻辑
        canvas.drawCircle({
          // 圆形坐标计算
        });
      }
      
      if (!highlight.matchAll) break;
      searchStart = matchEnd;
    }
  });
}

3. 布局与绘制集成 在自定义组件的build()方法中嵌入Canvas进行绘制:

build() {
  Column() {
    // 底层:高亮绘制
    Canvas(this.DrawHighlight)
      .width('100%')
      .height('100%')
    
    // 上层:文本显示(使用透明背景)
    Text(this.message)
      .textAlign(TextAlign.Start)
      .fontSize(16)
      .backgroundColor(Color.Transparent)
  }
}

4. API暴露 提供与Kotlin版本对应的方法:

// 添加高亮
addHighlight(text: string, color: ResourceColor, 
             style: HighlightStyle, matchAll: boolean): void {
  this.highlights.push(new HighlightInfo(text, color, style, matchAll));
  this.updateHighlight(); // 触发UI更新
}

// 清除高亮
clearHighlights(): void {
  this.highlights = [];
  this.updateHighlight();
}

private updateHighlight(): void {
  // 通过状态变更触发重绘
}

关键差异处理:

  • 文本测量:使用ArkUI的TextMetrics替代Android的Layout.getLineForOffset
  • 坐标计算:Canvas坐标系基于组件左上角,需结合padding计算
  • 性能优化:跨行高亮时注意绘制区域合并,避免过度绘制
  • 响应式更新:通过@State管理高亮数据,数据变更自动触发重绘

注意事项:

  1. ArkTS Canvas绘制是声明式的,每次数据变更都会重新执行@Builder函数
  2. 复杂文本布局建议使用TextLayout相关API进行精确测量
  3. 圆形高亮时需考虑字符间距和字体特性

这种转换保持了原有功能的核心逻辑,同时遵循了ArkUI的声明式编程范式。实际实现时需要根据具体文本样式(字体、大小、对齐方式)调整坐标计算。

回到顶部