oh-topic-editor: OpenHarmony HarmonyOS平台上基于RichEditor实现的支持添加话题、@用户的文本编辑组件

news2024/11/28 12:30:22

需求

在App开发中,我们常常会遇到发布文章、评论的时候需要添加话题或者@用户的需求,就像微博那样。这在Android、iOS或者其他平台上都有现成的组件可供使用,但是HarmonyOS NEXT作为一个新兴平台,三方库实在匮乏,连微博鸿蒙版App本身都没完全实现这个功能。要想实现这个功能,那就必须手搓轮子了。撸起袖子,一把梭,干就完了。

效果图

在这里插入图片描述

实现原理

ArkUI组件中,文本输入组件主要有:TextInput、TextArea和RichEditor。TextInput是单行文本输入框,TextArea是多行文本输入框,两者都不支持多Span、也不支持多种样式的文本。故而我们只能选用RichEditor富文本编辑组件来实现需求。

RichEditor支持TextSpan、ImageSpan、SymbolSpan和BuilderSpan,查看相关API就知道BuilderSpan最适合实现我们的添加话题和@用户的需求。但是坑爹的是文档提到:

不支持通过getSpans,getSelection,onSelect,aboutToDelete获取builderSpan信息。

我们知道,富文本编辑器的操作其实十分复杂,完全由我们手动记录和维护BuilderSpan和关联的实体对象的关系会非常复杂。我们需要知道哪个BuilderSpan对应哪个用户实体或者话题实体、BiulderSpan什么时候添加了、删除等等,需要监听RichEditor的一系列回调事件,极容易出错。

对于调用者来说,最简单的无疑是通过一个getSpans接口获取到所有Span的信息,无奈官方声明不支持。实际测试中发现,RichEditorController的getSpans方法返回的数组中包含了代表BuilderSpan的RichEditorImageSpanResult且其valueResourceStr属性为空串,只是无法获取BuilderSpan的详细信息。那么如果我们能在添加BuilderSpan的时候记录下它的信息,然后在getSpans方法中替换原来的RichEditorImageSpanResult,问题就解决了。

那么问题就变成了如何把getSpans返回的RichEditorImageSpanResult替换成对应的其他对象呢。虽说代表BuilderSpan的RichEditorImageSpanResult实例没有包含BuilderSpan的具体信息,但是至关重要的是它的顺序(数组下标)是按序排列的。所以我们只需要按序获取到BuilderSpan的信息,然后按序替换即可。

由于RichEditorController的addBuilderSpan方法中,如何构建BuilderSpan是由开发者自行控制的,那么我们可以给每个BuilderSpan的组件都添加一个唯一ID,在getSpans的时候再通过 componentUtils.getRectangleById(id) 获取到各个BuilderSpan的坐标和宽高,通过组件位置判断出它们的顺序就大功告成了!

实现如下:

import { componentUtils } from '@kit.ArkUI'

@Component
export struct TopicEditor {
  controller: TopicEditorController = new TopicEditorController()
  /** 是否点两次删除才把TopicSpan删除 */
  @Prop doubleDelete: boolean = true
  /** 为了方便灵活地控制各种样式、设置选项,RichEditor通过BuilderParam传入,由调用者自行创建 */
  @BuilderParam richEditorBuilder: (controller: RichEditorController, aboutToDeleteCallback: Callback<RichEditorDeleteValue, boolean>) => void

  /** 删除监听 */
  private aboutToDeleteCallback = (val: RichEditorDeleteValue) => {
    // 处理双击删除,只有一个Span的时候才可能是删除TopicSpan
    if(this.doubleDelete && val.richEditorDeleteSpans.length == 1) {
      let span = val.richEditorDeleteSpans[0]
      let editorController = this.controller.getRichEditorController()
      let selection = editorController.getSelection()
      // 判断是否TopicSpan以及当前是否被选中,如果选中了直接删除,未选中则选中但不删除
      if(isTopicSpan(span) && !(selection.selection.length == 2 && selection.selection[0] == span.spanPosition.spanRange[0] && selection.selection[1] == span.spanPosition.spanRange[1])) {
        let spanRange: number[] = span.spanPosition.spanRange
        editorController.setSelection(spanRange[0], spanRange[1])
        return false
      }
    }
    return true
  }

  build() {
    this.richEditorBuilder(this.controller.getRichEditorController(), this.aboutToDeleteCallback)
  }
}

/** 判断是否TopicSpan */
function isTopicSpan(span: RichEditorTextSpanResult | RichEditorImageSpanResult) : boolean {
  return span['imageStyle'] != undefined && (span['valueResourceStr'] == '' || span['valueResourceStr'] == ' ')
}

export interface TopicSpan {
  id: string
  builder: CustomBuilder
}

/**
 * TopicEditor控制器,通过它添加TopicSpan和TextSpan,以及获取最终结果
 */
export class TopicEditorController {
  private internal = new RichEditorController()
  
  private topicIds : string[] = []
  
  /** 清空 */
  clear() {
    this.topicIds = []
    this.internal.deleteSpans()
  }

  /** 暴露RichEditorController以便灵活控制RichEditor */
  getRichEditorController(): RichEditorController {
		return this.internal
  }

  /**
   * 添加TopicSpan
   * @param span 话题、用户信息,ID必须唯一
   * @param offset 添加位置
   */
  addTopicSpan(span: TopicSpan, offset?: number) {
    this.topicIds.push(span.id)
    this.internal.addBuilderSpan(span.builder, { offset })
  }

  /**
   * 同RichEditorController
   * @param value
   * @param options
   * @returns 
   */
  addTextSpan(value: string, options?: RichEditorTextSpanOptions | undefined): number {
    return this.internal.addTextSpan(value, options)
  }

  /**
   * 获取当前内容,数组每个元素代表一个Span,调用者可以根据返回结果和业务逻辑组合成最终内容
   */
  getSpans(): TopicSpanResult[] {
    let topicSpanInfos: TopicSpanInfo[] = []
    for (let id of this.topicIds) {
      let componentInfo = componentUtils.getRectangleById(id)
      
      // 宽高为0代表已经被删除了
      if(componentInfo.size.width == 0 && componentInfo.size.height == 0) {
        continue
      }
      topicSpanInfos.push({ id, componentInfo })
    }

    // 根据组件位置排序
    topicSpanInfos = topicSpanInfos.sort((a, b) => {
      if(a.componentInfo.windowOffset.y >= b.componentInfo.windowOffset.y + b.componentInfo.size.height) {
        return 1
      }
      if(a.componentInfo.windowOffset.y + a.componentInfo.size.height <= b.componentInfo.windowOffset.y) {
        return -1
      }
      return a.componentInfo.windowOffset.x - b.componentInfo.windowOffset.x
    })

    // 按序替换得到最终结果
    let results : TopicSpanResult[] = []
    let spans = this.internal.getSpans()
    let index = 0
    for (let span of spans) {
      if(isTopicSpan(span)) {
        results.push({
          value: topicSpanInfos[index++].id,
          isTopicSpan: true
        })
      } else {
        let textSpan = span as RichEditorTextSpanResult
        results.push({
          value: textSpan.value,
          isTopicSpan: false
        })
      }
    }
    return results
  }
}

interface TopicSpanInfo {
  id: string
  componentInfo : componentUtils.ComponentInfo
}

export interface TopicSpanResult {
  /** isTopicSpan为true时,这是TopicSpan的id,为false时,是TextSpan的文本 */
  value: string
  /** 此Span是否是TopicSpan */
  isTopicSpan: boolean
}

局限性及注意事项

只支持TextSpan和TopicSpan,加入其他Span可能会导致未知异常。
另外,该实现思路未得到生产环境验证。

成品

GITHUB: https://github.com/sahooz/oh-topic-editor
OpenHarmony仓库审核中,后续补上

更多

我开发的其他鸿蒙库:

  1. oh-crop: OpenHarmony/HarmonyOS上的简单的图片剪裁库,可用于头像剪裁等常见场景。
  2. oh-date-picker: OpenHarmony/HarmonyOS平台日期选择器增强版。

我的博客:https://blog.xinyanruanjian.com/

我的公众号:程序员吹白

鸿蒙开发交流QQ群:546723002

开源协议

MIT

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2192310.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot中,接口签名,通用方案,以确保接口的安全性

1. 为什么需要接口签名&#xff1f; 接口签名目的&#xff1a;防止第三方伪造请求。请求伪造&#xff1a;未经授权的第三方构造合法用户的请求来执行不希望的操作。转账接口示例&#xff1a;展示了如果接口没有安全措施&#xff0c;第三方可以轻易伪造请求&#xff0c;例如将资…

用户在网页上输入一个网址,它整个页面响应的流程是什么?

目录 一、流程的大致过程 二、流程的详细分析 1. 浏览器先分析超链接中的URL 2. DNS解析 3. 建立TCP连接 建立连接&#xff08;三次握手&#xff09; HTTP中的请求报文 4. 浏览器发送HTTP请求 5. 服务器处理请求并发送响应 HTTP的响应报文 6. 浏览器接收响应 7. 渲…

After-kaoyan

知乎 - 安全中心 有态度&#xff0c;有回应&#xff0c;有温度&#xff0c;是跟双鱼相处的基础 我今天跟大家泄漏一个秘密&#xff0c;这个秘密也很简单&#xff0c;就是我每次遇到困难险阻时候我从不退缩&#xff0c;我也不会想着&#xff1a;“算了吧&#xff0c;我做不到&a…

基于Springboot+Vue的零食批发商仓库管理系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 在这个…

Python调试技巧:高效定位与修复问题

Python调试技巧&#xff1a;高效定位与修复问题 在Python编程过程中&#xff0c;调试是不可避免的重要环节。无论是刚接触编程的初学者还是经验丰富的开发者&#xff0c;都可能会遇到代码运行不符合预期的情况。高效的调试技巧不仅能帮助我们快速找到问题&#xff0c;还能减少…

Graphiti:如何让构建知识图谱变得更快、更具动态性?

扩展大语言模型数据提取&#xff1a;挑战、设计决策与解决方案 Graphiti 是一个用于构建和查询动态、时间感知的知识图谱的 Python 库。它可以用于建模复杂、不断演变的数据集&#xff0c;并确保 AI 智能体能够访问它们完成非平凡任务所需的数据。它是一个强大的工具&#xff…

9个微服务最佳实践

1⃣分离数据存储&#xff1a;独立数据库&#xff0c;提升灵活性。 2⃣代码成熟度一致&#xff1a;质量稳定&#xff0c;避免技术债务 3⃣独立构建流程&#xff1a;独自构建&#xff0c;快速部署。 4⃣单一职责原则&#xff1a;业务功能单一&#xff0c;简化维护。 5⃣容器化部署…

Android车载——VehicleHal初始化(Android 11)

1 概述 VehicleHal是AOSP中车辆服务相关的hal层服务。它主要定义了与汽车硬件交互的标准化接口和属性管理&#xff0c;是一个独立的进程。 2 进程启动 VehicleHal相关代码在源码树中的hardware/interfaces/automotive目录下 首先看下Android.bp文件&#xff1a; cc_binary …

大模型公司对标:360

公司档案 360成立于2005年&#xff0c;初期以提供免费的杀毒软件“360安全卫士”而迅速获得市场认可&#xff0c;并逐渐发展成为一家提供全面互联网安全解决方案的企业。2015年成立人工智能研究院&#xff0c;开展人工智能技术探索&#xff0c;成为国内布局研究开发人工智能较…

Oracle 表空间异构传输

已经有了表空间的数据文件&#xff0c;和元数据dump文件&#xff0c;如何把这个表空间传输到异构表空间中&#xff1f; 查询异构传输平台信息&#xff1a; COLUMN PLATFORM_NAME FORMAT A40 SELECT PLATFORM_ID, PLATFORM_NAME, ENDIAN_FORMAT FROM V$TRANSPORTABLE_PLATFORM O…

教育技术革新:SpringBoot在线教育系统开发指南

6系统测试 6.1概念和意义 测试的定义&#xff1a;程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为&#xff1a; 目的&#xff1a;发现程序的错误&#xff1b; 任务&#xff1a;通过在计算机上执行程序&#xff0c;暴露程序中潜在的错误。 另一个…

计算机找不到vcomp140.dll,无法继续执行代码如何解决,有什么好的修复方法

1. vcomp140.dll 简介 1.1 定义 vcomp140.dll 是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;它属于 Microsoft Visual C 2015 Redistributable Package 的一部分。该文件为应用程序提供了 OpenMP 并行框架所需的运行时支持&#xff0c;允许开发者编写并发和多…

【Verilog学习日常】—牛客网刷题—Verilog进阶挑战—VL25

输入序列连续的序列检测 描述 请编写一个序列检测模块&#xff0c;检测输入信号a是否满足01110001序列&#xff0c;当信号满足该序列&#xff0c;给出指示信号match。 模块的接口信号图如下&#xff1a; 模块的时序图如下&#xff1a; 请使用Verilog HDL实现以上功能&#x…

论文笔记:微表情欺骗检测

整理了AAAI2018 Deception Detection in Videos 论文的阅读笔记 背景模型实验可视化 背景 欺骗在我们的日常生活中很常见。一些谎言是无害的&#xff0c;而另一些谎言可能会产生严重的后果。例如&#xff0c;在法庭上撒谎可能会影响司法公正&#xff0c;让有罪的被告逍遥法外。…

电脑获得高级管理员权限(Windows10 专业版)

电脑获得高级管理员权限(Windows10 专业版) 请谨慎操作 通常我们在删除一些文件时&#xff0c;会提示权限不足&#xff0c;删除不了文件 我们可以打开组策略编辑器将当前用户修改为高级管理员权限 Windows10获取高级管理员权限 首先打开本地组策略编辑器(cmd输入gpedit.msc)其…

20分钟写一个链表

目录 前言1.带头结点的循环双链表1.1 链表的分类、线性表的对比1.2 双链表基本操作代码实现1.2.1 初始化1.2.2 销毁、打印链表 总结 前言 有一个学长在面试的时候被问到这样一个问题&#xff0c;“你可以用20分钟写一个链表吗&#xff1f;”学长第一反应是&#xff0c;至少要一…

传统图像处理Opencv分割不同颜色的夹子

任务要求&#x1f349; 1. 计算图像中夹子的总数。 2. 分别计算不同颜色夹子的个数。 3. 使用以下方法适应三张图片&#xff0c;并在每张图像上显示结果&#xff1a; - 阈值方法 - HSV颜色空间 - 连通域分析 - 形态学图像处理 - Canny边缘检测 4. 在结果中显示计…

北交大研究突破:塑料光纤赋能低成本无摄像头AR/VR眼动追踪技术

北交大研究&#xff1a;探索无摄像头低成本AR/VR眼动追踪新路径 在AR/VR技术领域&#xff0c;眼动追踪作为一项关键技术&#xff0c;对于提升用户体验、优化渲染效率具有重要意义。然而&#xff0c;传统的眼动追踪方案多依赖于高成本的摄像头&#xff0c;这不仅增加了设备的制造…

学习资料库系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;观看记录管理&#xff0c;基础数据管理&#xff0c;论坛信息管理&#xff0c;公告信息管理&#xff0c;轮播图信息 微信端账号功能包括&#xff1a;系统首页&#xff0c;阅读资…

性能学习5:性能测试的流程

一.需求分析 二.性能测试计划 1&#xff09;测什么&#xff1f; - 项目背景 - 测试目的 - 测试范围 - ... 2&#xff09;谁来测试 - 时间进度与分工 - 交付清单 - ... 3&#xff09;怎么测 - 测试策略 - ... 三.性能测试用例 四.性能测试执行 五.性能分析和调优 六…