华为HarmonyOS NEXT 开发一个简单的聊天界面

675 阅读4分钟

一、先说说遇到的问题

  • HarmonyOS ArkTS 组件里面并没有css伪类这个概念,所以我研究聊天气泡弹窗的三角符号的时候浪费了不少时间。
  • 然后是组件定位 文档里面 position 绝对定位 markAnchor 锚点 offset 相对定位这些都是设置 x y 没有web前端那样可以设置bottom,right来得直接高效。
  • 把聊天输入框放在底部一开始用position实现发现确实不好用,最后改成了Stack堆叠的方式实现,发现效果还不错。

二、开始实现聊天界面

1.先上最终的效果图吧。

1111111.png

2.创建项目可以参考我之前的文章 构建第一个ArkTS应用
3.在Index.ets页面创建构造消息实例

class MsgType {
  avatar:string
  id:number
  content:string
  type:number

  constructor(avatar: string,id: number, content: string, type: number) {
    this.avatar = avatar;
    this.id = id;
    this.content = content;
    this.type = type;
  }
}



4.创建消息对话列表

利用 Flex 组件 direction 属性 FlexDirection.RowReverse 当是自己发送的消息type等于2 时 将头像和消息气泡反转



class MsgType {
  avatar:string
  id:number
  content:string
  type:number

  constructor(avatar: string,id: number, content: string, type: number) {
    this.avatar = avatar;
    this.id = id;
    this.content = content;
    this.type = type;
  }
}


@Entry
@Component
struct Index {
  controller: TextInputController = new TextInputController()
  private avatar:string="https://img2.baidu.com/it/u=283928093,2746724785&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800"
  @State msgText:string="";
  @State listData: Array<MsgType> = [
    {
      avatar:"https://q3.itc.cn/q_70/images03/20240519/1b4e7565b46241b68288ae482412a866.jpeg",
      id:1,
      content:"真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录",
      type:1,
    },
    {
      avatar:"https://q3.itc.cn/q_70/images03/20240519/1b4e7565b46241b68288ae482412a866.jpeg",
      id:2,
      content:"真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录",
      type:1,
    },
    {
      avatar:"https://img2.baidu.com/it/u=283928093,2746724785&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800",
      id:3,
      content:"真实一条测试记录真实一",
      type:2,
    }
  ]

  
   
  build() {
    Column(){
      List({ space: 20, initialIndex: 0 }) {
        ForEach(this.listData, (item:MsgType) => {
          ListItem() {
            Flex({
              direction:item.type==2?FlexDirection.RowReverse:FlexDirection.Row
            }){
              Image(item.avatar)
                .alt($r('app.media.icon'))
                .objectFit(ImageFit.Cover)
                .width(46)
                .height(46)
                .borderRadius(50)
                .borderWidth(1)
                .borderColor('#fffffff')

              Row(){
                Image($r('app.media.ic_arrow1'))
                  .width(16)
                  .height(16)
                  .fillColor(item.type==1?'#ffffff':'#ff92e0b7')
                  .objectFit(ImageFit.Cover)
                  .position({
                    x:item.type==1?-10:'97%',
                    y:2
                  })
                    // 旋转箭头
                  .rotate({
                    x: 0,
                    y: 0,
                    z: 1,
                    centerX: '50%',
                    centerY: '50%',
                    angle: item.type==1?180:0
                  })
                Row(){
                  Text(item.content)
                    .fontSize(14)
                    .fontColor('#333333')
                }
                .padding({right:10,left:10})
              }
              .justifyContent(FlexAlign.Start)
              .align(Alignment.Start)
              .padding({
                top:10,
                bottom:10
              })
              .width('65%')
              .margin({left:item.type==1?15:0,right:item.type==1?0:15})
              .backgroundColor(item.type==1 ? '#ffffff' : '#ff92e0b7')
              .borderRadius(5)
            }
            .width('100%')
            .padding({
              top:10,
              right:10,
              left:10
            })
          }.onClick(()=>{
            console.log('111')
          })
        }, (item) => item.id)
      }.zIndex(1)
      .width('100%')
      .height('100%')
      .padding({bottom:80})
    }
  }
}
5.创建发送消息输入框组件
//   Column(){
//     Flex({ direction: FlexDirection.Row,justifyContent:FlexAlign.SpaceBetween,
//       alignItems:ItemAlign.Center }){
//       Row() {
//         TextInput({text: this.msgText,  placeholder: '请输入聊天内容', controller: this.controller })
//           .placeholderColor(Color.Grey)
//           .placeholderFont({ size: 14, weight: 400 })
//           .caretColor(Color.Blue)
//           .height(35)
//           .fontSize(14)
//           .margin({left:15})
//           .fontColor(Color.Black)
//           .onChange((value: string) => {
//             this.msgText = value
//           })
//       }
//       .flexGrow(1)
//       Button('发送')
//         .margin(15)
//         .fontSize(16)
//         .width(100)
//         .height(35)
//         .backgroundColor('#ff64e394')
//         .onClick(() => {
//           this.send()
//         })
//     }
//     .width('100%')
//     .backgroundColor('#ffffff')
//   }
//   .width('100%')
//   .zIndex(100)
6.利用 Stack alignContent Alignment.BottomEnd 属性将发送消息栏放在页面底部

这里需要注意的是需要 给 List 组件设置 和 发送消息对话框 设置 zIndex 属性 而且需要给list 组件设置高度100%,否则List也是底部对齐 完整代码如下



class MsgType {
  avatar:string
  id:number
  content:string
  type:number

  constructor(avatar: string,id: number, content: string, type: number) {
    this.avatar = avatar;
    this.id = id;
    this.content = content;
    this.type = type;
  }
}


@Entry
@Component
struct Index {
  controller: TextInputController = new TextInputController()
  private avatar:string="https://img2.baidu.com/it/u=283928093,2746724785&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800"
  @State msgText:string="";
  @State listData: Array<MsgType> = [
    {
      avatar:"https://q3.itc.cn/q_70/images03/20240519/1b4e7565b46241b68288ae482412a866.jpeg",
      id:1,
      content:"真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录",
      type:1,
    },
    {
      avatar:"https://q3.itc.cn/q_70/images03/20240519/1b4e7565b46241b68288ae482412a866.jpeg",
      id:2,
      content:"真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录真实一条测试记录",
      type:1,
    },
    {
      avatar:"https://img2.baidu.com/it/u=283928093,2746724785&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800",
      id:3,
      content:"真实一条测试记录真实一",
      type:2,
    }
  ]

  // 发送信息
  send(){
    if(!this.msgText){
      return;
    }
    this.listData.push(
        new MsgType(this.avatar,this.listData.length+2, this.msgText, 2)
    );
    setTimeout(()=>{
      this.msgText = null
    },100)
  }
  onPageShow(){

  };
  build() {
    Stack({alignContent : Alignment.BottomEnd}){
      List({ space: 20, initialIndex: 0 }) {
        ForEach(this.listData, (item:MsgType) => {
          ListItem() {
            Flex({
              direction:item.type==2?FlexDirection.RowReverse:FlexDirection.Row
            }){
              Image(item.avatar)
                .alt($r('app.media.icon'))
                .objectFit(ImageFit.Cover)
                .width(46)
                .height(46)
                .borderRadius(50)
                .borderWidth(1)
                .borderColor('#fffffff')

              Row(){
                Image($r('app.media.ic_arrow1'))
                  .width(16)
                  .height(16)
                  .fillColor(item.type==1?'#ffffff':'#ff92e0b7')
                  .objectFit(ImageFit.Cover)
                  .position({
                    x:item.type==1?-10:'97%',
                    y:2
                  })
                    // 旋转箭头
                  .rotate({
                    x: 0,
                    y: 0,
                    z: 1,
                    centerX: '50%',
                    centerY: '50%',
                    angle: item.type==1?180:0
                  })
                Row(){
                  Text(item.content)
                    .fontSize(14)
                    .fontColor('#333333')
                }
                .padding({right:10,left:10})
              }
              .justifyContent(FlexAlign.Start)
              .align(Alignment.Start)
              .padding({
                top:10,
                bottom:10
              })
              .width('65%')
              .margin({left:item.type==1?15:0,right:item.type==1?0:15})
              .backgroundColor(item.type==1 ? '#ffffff' : '#ff92e0b7')
              .borderRadius(5)
            }
            .width('100%')
            .padding({
              top:10,
              right:10,
              left:10
            })
          }.onClick(()=>{
            console.log('111')
          })
        }, (item) => item.id)
      }.zIndex(1)
      .width('100%')
      .height('100%')
      .padding({bottom:80})
      //容器位于最底部
      Column(){
        Flex({ direction: FlexDirection.Row,justifyContent:FlexAlign.SpaceBetween,
          alignItems:ItemAlign.Center }){
          Row() {
            TextInput({text: this.msgText,  placeholder: '请输入聊天内容', controller: this.controller })
              .placeholderColor(Color.Grey)
              .placeholderFont({ size: 14, weight: 400 })
              .caretColor(Color.Blue)
              .height(35)
              .fontSize(14)
              .margin({left:15})
              .fontColor(Color.Black)
              .onChange((value: string) => {
                this.msgText = value
              })
          }
          .flexGrow(1)
          Button('发送')
            .margin(15)
            .fontSize(16)
            .width(100)
            .height(35)
            .backgroundColor('#ff64e394')
            .onClick(() => {
              this.send()
            })
        }
        .width('100%')
        .backgroundColor('#ffffff')
      }
      .width('100%')
      .zIndex(100)
    }.width('100%')
     .height('100%')
    .backgroundColor('#f7f7f7').padding({ top: 5 })
  }
}

写在最后

华为HarmonyOS NEXT bug 还是不少,需要不断优化改进,后续我也会花更多时间学习。