鸿蒙开发:实现AI打字机效果

前言

代码案例基于Api13。

目前哪个行业最火,非AI莫属,deepseek发布之后,可以说,又把AI推上了一个新高度,在和AI进行询问会话的时候,我们可以发现,AI的回答都是以流式的效果进行展示的,也就是类似于打字机的效果,那么针对这种效果在实际的开发中是如何实现的呢?

具体的效果,根据业务情况而定,有两种模式,一种主动的流式输出,也就是数据以流式的形式进行返回,前端直接用组件加载即可,第二种就是刻意的流式展示,也就是在拿到数据之后,前端实现流式输出,进行打字机展示。

打字机的效果,一般都是在会话聊天之中,也就是列表之中,在实际的开发中,还要兼顾到,流式输出的数据加载是否会影响性能,页面闪烁,最新的聊天信息可展示等问题。

主动的流式输出

在一般的AI会话中,实现一个流式输出,一般会采用SSE或者WebSocket协议,像OpenAI官网,DeepSeek官网是采用SSE协议,当然,在实际的开发中,大家可以选择自己适用的技术即可。客户端发送问题之后,服务端检索到内容,就会时时的返回内容,具体是返回连接式的内容,还是逐字返回,需要和服务端进行定义。连接式返回,客户端只管加载,逐字返回,需要客户端拼接。

TypeScript 复制代码
@Entry
@Component
struct Index {
  @State message?: string = ""
  intervalID?: number

  /**
   *AUTHOR:AbnerMing
   *INTRODUCE:模拟请求网络接口
   */
  doHttp(success: (message: string) => void) {
    let data = "具体的实现效果,根据业务情况而定,有两种模式,一种主动的流式输出,也就是数据以流式的形式进行返回,前端直接用组件加载即可,第二种就是刻意的流式展示,也就是在拿到数据之后,前端实现流式输出,进行打字机展示。"
    let position: number = 0
    //模拟请求流式输出
    this.intervalID = setInterval(() => {
      position = position + 2
      let message = data.substring(0, position)
      if (success != undefined) {
        success(message)
      }
      if (message.length >= data.length) {
        clearInterval(this.intervalID)
      }
    }, 100)
  }

  build() {
    Column() {
      Button("加载")
        .margin({ top: 10 })
        .onClick(() => {
          this.doHttp((message: string) => {
            this.message = message
          })
        })
      Text(this.message)
        .margin({ top: 20 })
        .width("100%")
    }.width("100%")
    .height("100%")
    .padding(10)
  }
}

我们可以看下演示的效果,实际的开发中,前端无须关注每次的返回字的长度。

被动的流式展示

所谓的被动,就是在已有数据的情况下,如何实现打字机的效果,这个比较的简单,无非是开启一个定时,以每隔多少时间,输出多少字为主,时间和输出字的长度都可以自己调节,简单案例如下,当然了,这种方式一般很少应用于实际的开发,不过在客户端有类似打字机效果的情况下可以使用。

TypeScript 复制代码
@Entry
@Component
struct Index {
  @State message?: string = ""
  intervalID?: number

  build() {
    Column() {
      Button("加载")
        .margin({ top: 10 })
        .onClick(() => {
          this.start()
        })
      Text(this.message)
        .margin({ top: 20 })
        .width("100%")
    }.width("100%")
    .height("100%")
    .padding(10)
  }

  private start() {
    let data = "具体的实现效果,根据业务情况而定,有两种模式,一种主动的流式输出,也就是数据以流式的形式进行返回,前端直接用组件加载即可,第二种就是刻意的流式展示,也就是在拿到数据之后,前端实现流式输出,进行打字机展示。"
    let position: number = 0
    this.intervalID = setInterval(() => {
      position = position + 2
      this.message = data.substring(0, position)
      if (this.message.length >= data.length) {
        clearInterval(this.intervalID)
      }
      console.log("======定时")
    }, 100)
  }
}

我们看下输出效果,是不是有那种打字机的效果了,需要注意的是,定时关闭。

列表打印机效果

以上的效果都是以一个Text组件展示的情况,在实际的开发中,更多的是以左右会话形式,这时需要考虑的就多一点,比如会话定位在底部,流式展示时,不让列表闪烁等等问题,那么都是需要考虑的。

下面简单的实现一下聊天会话模式,所有的数据都是模拟的,UI简单的绘制了一下,在实际的开发中,肯定比较精细。

TypeScript 复制代码
import { KeyboardAvoidMode } from '@kit.ArkUI'

@Observed
export class MessageBean {
  leftMessage?: string
  rightMessage?: string
}

@Component
struct ChatView {
  @ObjectLink messageBean: MessageBean;

  build() {
    Column() {
      if (this.messageBean.rightMessage != undefined) {
        Row() {
          Text(this.messageBean.rightMessage)
            .margin({ right: 10 })
          Image($r("app.media.startIcon"))
            .id("user_logo")
            .border({ color: Color.Red, width: 1, radius: 20 })
            .width(20)
            .height(20)
        }.width("100%")
        .justifyContent(FlexAlign.End)
        .padding({ right: 10 })
      } else {
        Row() {
          Image($r("app.media.startIcon"))
            .border({ color: Color.Red, width: 1, radius: 20 })
            .width(20)
            .height(20)
          Text(this.messageBean.leftMessage)
            .margin({ left: 10 })
        }.alignItems(VerticalAlign.Top)
        .padding({ right: 20 })
      }

    }
  }
}

@Entry
@Component
struct Index {
  private scroller: Scroller = new Scroller()
  @State sendMessage: string = ""
  @State messageList: MessageBean[] = []
  intervalID?: number
  private isEnd: boolean = true

  aboutToAppear(): void {
    this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
  }

  /**
   *AUTHOR:AbnerMing
   *INTRODUCE:模拟请求网络接口
   */
  doHttp(success: (message: string) => void) {
    let data = "具体的实现效果,根据业务情况而定,有两种模式,一种主动的流式输出,也就是数据以流式的形式进行返回,前端直接用组件加载即可,第二种就是刻意的流式展示,也就是在拿到数据之后,前端实现流式输出,进行打字机展示。"
    let position: number = 0
    //模拟请求流式输出
    this.intervalID = setInterval(() => {
      position = position + 2
      let message = data.substring(0, position)
      if (success != undefined) {
        success(message)
      }
      if (message.length >= data.length) {
        this.isEnd = true //模拟结束
        clearInterval(this.intervalID)
      }
    }, 100)
  }

  build() {
    RelativeContainer() {
      List({ space: 10, scroller: this.scroller }) {
        ForEach(this.messageList, (item: MessageBean) => {
          ListItem() {
            ChatView({ messageBean: item })
          }
        })
      }.margin({ top: 10 })
      .alignRules({
        top: {
          anchor: "__container__",
          align: VerticalAlign.Top
        },
        bottom: {
          anchor: "layout_send",
          align: VerticalAlign.Top
        }
      })

      RelativeContainer() {
        TextInput({ placeholder: "请输入问题" })
          .onChange((text) => {
            this.sendMessage = text
          })
          .alignRules({
            left: {
              anchor: "__container__",
              align: HorizontalAlign.Start
            },
            right: {
              anchor: "btn_send",
              align: HorizontalAlign.Start
            }
          })
        Button("发送")
          .id("btn_send")
          .margin({ left: 10 })
          .alignRules({
            right: {
              anchor: "__container__",
              align: HorizontalAlign.End
            }
          }).onClick(() => {
          //发送
          if (this.isEnd) {
            this.isEnd = false
            let bean = new MessageBean()
            bean.rightMessage = this.sendMessage
            this.messageList.push(bean)
            this.scroller.scrollEdge(Edge.Bottom)
            //模拟接口返回数据
            let leftBean = new MessageBean()
            this.messageList.push(leftBean)
            this.doHttp((content) => {
              leftBean.leftMessage = content
            })
          }

        })
      }
      .height(40)
      .id("layout_send")
      .padding({ left: 10, right: 10 })
      .backgroundColor(Color.White)
      .alignRules({
        bottom: {
          anchor: "__container__",
          align: VerticalAlign.Bottom
        }
      })
    }.width("100%")
    .height("100%")
  }
}

运行之后,我们简单测试一下:

一般会话列表的形式,有一点需要注意,那就是历史记录。

相关总结

需要注意的是,内容一般都是以markdown的形式输出,也就是真实的数据中,内容都是有样式的,比如加粗,图片,表格等等,所以,不能以单一的Text组件进行展示,需要针对markdown文本适配。

打字机的效果,更多的是在服务端的数据输出,客户端,最主要的是针对数据的渲染。

相关推荐
y = xⁿ9 小时前
MySQL八股知识合集
android·mysql·adb
IntMainJhy9 小时前
「Flutter三方库sqflite的鸿蒙化适配与实战指南:从入门到踩坑的本地数据库开发全记录」
数据库·flutter·华为·信息可视化·数据库开发·harmonyos
andr_gale10 小时前
04_rc文件语法规则
android·framework·aosp
祖国的好青年11 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴11 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭11 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首11 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
前端技术12 小时前
HarmonyOS开发:鸿蒙应用开发发展史
华为·harmonyos
zhangphil12 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin