往期推文全新看点

一多即时通讯场景概述

本文从目前流行的垂类市场中,选择即时通讯应用作为典型案例详细介绍 “一多” 在实际开发中的应用。一多即时通讯应用的核心功能为用户交互,主要包含对话聊天、通讯录,社交圈等交互功能。开发者在开发"一多"应用时,经常会遇见多端适配上的问题,本文选择了即时通讯应用的一个常见问题,提供了推荐的解决方案,开发者在"一多"开发中遇见同类问题时可以快速解决。

  • 聊天场景如何进行布局设计

当前系统的产品形态主要有手机、折叠屏、平板和2in1四种,下文的具体实践也将围绕这四种产品形态展开,同时将分别从UX设计、页面开发两个角度给出符合“一多”的参考样例,介绍“一多”即时通讯应用在开发过程中的最佳实践。

  • 架构设计章节介绍一多项目的三层架构,开发者可以去相关链接了解。
  • UX设计章节介绍即时通讯应用的聊天场景的交互逻辑,对于类似的设计要点,开发者可以直接拿来使用。
  • 页面开发章节主要介绍聊天场景的布局能力,介绍如何实现聊天场景,如何适配多设备。

架构设计

HarmonyOS的分层架构主要包括三个层次:产品定制层、基础特性层和公共能力层,为开发者构建了一个清晰、高效、可扩展的设计架构。

UX设计

一多即时通讯应用包含聊天、通讯录,社交圈等交互功能,其中聊天页包含分栏布局设计,因此这里给出聊天页的业务逻辑。

一多即时通讯场景包含以下设计能力:侧边导航、分栏布局。

页面开发

以聊天页为典型页面进行展开,聊天页中包含侧边导航与分栏布局的设计能力,本文着重介绍聊天页如何实现分栏布局。

布局能力

聊天页在不同断点下的UX效果如下,涉及的设计能力是侧边导航,分栏布局。侧边导航参考侧边导航,其中会有详细介绍。

在手机和折叠屏折叠状态设备上,受屏幕大小限制,不能实现分栏布局,需要通过点击或者其他方式跳转到另一个页面,但是在折叠屏展开状态、平板及2ni1产品中屏幕尺寸足够大,可以分栏显示不同的内容,为了使操作更加便捷,在IM对话页中使用分栏布局实现对话功能。

示意图如下:

示意图 sm md lg
设计能力点
效果图

从上表中,可以发现在sm断点下呈现的是聊天列表页,当点击某一条聊天记录的时候跳转到聊天详情页面;在md和lg断点下则左侧呈现聊天列表页,右侧呈现聊天详情页。

在一次开发,多端部署场景下,Navigation组件能够自动适配窗口,在窗口较大的场景下自动切换分栏展示效果。因此本文中分栏布局使用Navigation组件实现。

各个设备布局图如下所示:

示意图 sm md lg
效果图 参照上文表格
布局图

在Navigation组件中,定义了对话列表,出于封装的考虑,并不会在一个页面中去使用NavRouter组件做路由跳转,因此使用NavPathStack栈去做路由处理,所以定义了pageInfo去存储路由栈,当点击对话列表中一条对话信息时,向pageInfo中推送跳转路由。关系大致如下:

// features/home/src/main/ets/pages/Index.ets
Navigation(this.pageInfo) {
  if (this.currentPageIndex === 0) {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center }) {
      ConversationList({
        currentConversationUserName: $currentConversationUserName,
        currentContactUserName: $currentContactUserName
      })
        .flexGrow(1)
        .width('100%')
      HomeTab({ currentPageIndex: $currentPageIndex })
        .width(Adaptive.HomeTabWidth(this.currentBreakpoint))
        .height(Adaptive.HomeTabHeight(this.currentBreakpoint))
        .visibility(this.currentBreakpoint !== 'lg' ? Visibility.Visible : Visibility.None)
    }
    .padding({
      bottom: deviceInfo.deviceType !== '2in1' && this.currentBreakpoint !== 'lg' ? 28 : 0
    })
    .height('100%')

  } else if (this.currentPageIndex === 1) {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center }) {
      ContactsList({
        currentContactUserName: $currentContactUserName,
        currentConversationUserName: $currentConversationUserName,
        currentContactUserIcon: $currentContactUserIcon
      })
        .flexGrow(1)
        .width('100%')
      HomeTab({ currentPageIndex: $currentPageIndex })
        .width(Adaptive.HomeTabWidth(this.currentBreakpoint))
        .height(Adaptive.HomeTabHeight(this.currentBreakpoint))
        .visibility(this.currentBreakpoint !== 'lg' ? Visibility.Visible : Visibility.None)
    }
    .height('100%')
    .padding({
      bottom: deviceInfo.deviceType !== '2in1' && this.currentBreakpoint !== 'lg' ? 28 : 0
    })
  }
}

// features/home/src/main/ets/pages/ConversationList.ets
List() {
  ForEach(ConversationListData, (item: ConversationDataInterface, index: number) => {
    ListItem() {
      ConversationItem(item)
        .onClick(() => {
          if (this.pageInfo.size() > 1) {
            this.pageInfo.pop();
          }
          this.pageInfo.pushPath({ name: 'ConversationDetail' });
          this.currentConversationUserName = item.name;
          this.currentContactUserName = '';
          this.currentIndex = index;
        })
        .backgroundColor(this.currentIndex === index ? $r('app.color.conversation_clicked_bg_color') : Color.White)
    }
    .height(Adaptive.ContactItemHeight(this.currentBreakpoint))

  }, (item: ConversationDataInterface, index: number) => index + JSON.stringify(item))
}

此外需要注意Navigation的模式与宽度在不同设备下是有区分的,具体代码如下:

// features/home/src/main/ets/pages/Index.ets
@Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Builder
PageMap(name: string) {
  if (name === 'ConversationDetail') {
    ConversationDetail({ currentConversationUserName: this.currentConversationUserName, currentFeatureIndex: 1 });
  } else if (name === 'ConversationDetailNone') {
    ConversationDetailNone();
  } else if (name === 'ContactsDetail') {
    ContactsDetail({
      currentContactUserName: this.currentContactUserName,
      currentContactUserIcon: this.currentContactUserIcon
    });
  } else {
    ConversationDetailNone();
  }
}

build() {
  Column() {
    /**
     * Home and contacts page
     */
    Flex() {
      HomeTab({ currentPageIndex: $currentPageIndex })
        .width(Adaptive.HomeTabWidth(this.currentBreakpoint))
        .backgroundColor($r('app.color.background_color_grey_two'))
        .padding({
          top: 180,
          bottom: 180,
          left: 22
        })
        .visibility(this.currentBreakpoint === 'lg' ? Visibility.Visible : Visibility.None)
      Navigation(this.pageInfo) {
        if (this.currentPageIndex === 0) {
          Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center }) {
            ConversationList({
              currentConversationUserName: $currentConversationUserName,
              currentContactUserName: $currentContactUserName
            })
              .flexGrow(1)
              .width('100%')
            HomeTab({ currentPageIndex: $currentPageIndex })
              .width(Adaptive.HomeTabWidth(this.currentBreakpoint))
              .height(Adaptive.HomeTabHeight(this.currentBreakpoint))
              .visibility(this.currentBreakpoint !== 'lg' ? Visibility.Visible : Visibility.None)
          }
          .padding({
            bottom: deviceInfo.deviceType !== '2in1' && this.currentBreakpoint !== 'lg' ? 28 : 0
          })
          .height('100%')

        } else if (this.currentPageIndex === 1) {
          Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center }) {
            ContactsList({
              currentContactUserName: $currentContactUserName,
              currentConversationUserName: $currentConversationUserName,
              currentContactUserIcon: $currentContactUserIcon
            })
              .flexGrow(1)
              .width('100%')
            HomeTab({ currentPageIndex: $currentPageIndex })
              .width(Adaptive.HomeTabWidth(this.currentBreakpoint))
              .height(Adaptive.HomeTabHeight(this.currentBreakpoint))
              .visibility(this.currentBreakpoint !== 'lg' ? Visibility.Visible : Visibility.None)
          }
          .height('100%')
          .padding({
            bottom: deviceInfo.deviceType !== '2in1' && this.currentBreakpoint !== 'lg' ? 28 : 0
          })
        }
      }
      .hideTitleBar(true)
      .hideToolBar(true)
      .navBarWidth(this.currentBreakpoint === 'lg' ? '44.5%' : '50%')
      .navDestination(this.PageMap)
      .mode(this.currentBreakpoint === 'sm' ? NavigationMode.Stack : NavigationMode.Split)
      .width('100%')
    }
    .visibility(this.currentPageIndex === CurrentPage.HOME || this.currentPageIndex === CurrentPage.CONVERSATION ?
    Visibility.Visible : Visibility.None)

// features/home/src/main/ets/pages/ConversationList.ets
@Component
export struct ConversationList {
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
  @Link currentConversationUserName: string;
  @Link currentContactUserName: string;
  @State private currentIndex: number = 0;
  @Consume('pageInfo') pageInfo: NavPathStack;

  build() {
    Flex({ direction: FlexDirection.Column }) {
      HomeTopSearch({ title: '消息' })
        .height(Adaptive.ContactItemHeight(this.currentBreakpoint))
      List() {
        ForEach(ConversationListData, (item: ConversationDataInterface, index: number) => {
          ListItem() {
            ConversationItem(item)
              .onClick(() => {
                if (this.pageInfo.size() > 1) {
                  this.pageInfo.pop();
                }
                this.pageInfo.pushPath({ name: 'ConversationDetail' });
                this.currentConversationUserName = item.name;
                this.currentContactUserName = '';
                this.currentIndex = index;
              })
              .backgroundColor(this.currentIndex === index ? $r('app.color.conversation_clicked_bg_color') : Color.White)
          }
          .height(Adaptive.ContactItemHeight(this.currentBreakpoint))

        }, (item: ConversationDataInterface, index: number) => index + JSON.stringify(item))
      }
      .padding({
        bottom: deviceInfo.deviceType !== '2in1' && this.currentBreakpoint === 'lg' ? 28 : 0
      })
      .backgroundColor(Color.White)
      .width('100%')
      .height('100%')
    }

    .height('100%')
    .width('100%')
  }
}

// features/home/src/main/ets/pages/ConversationDetail.ets
@Component
export struct ConversationDetail {
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
  @Prop currentConversationUserName: string;
  @Prop currentFeatureIndex: number;
  @Consume('pageInfo') pageInfo: NavPathStack;

  build() {
    NavDestination() {
      Flex({ direction: FlexDirection.Column }) {
        ConversationDetailTopSearch({ currentConversationUserName: $currentConversationUserName, })
          .height(Adaptive.ContactItemHeight(this.currentBreakpoint))
        if (this.currentConversationUserName.length === 3) {
          ConversationDetailItem({
            receivedName: $currentConversationUserName,
            isReceived: true,
            content: HomeConstants.CONVERSATION_LIST_APPLET[0],
            isAppletMsg: true,
            currentFeatureIndex: $currentFeatureIndex
          })
          ConversationDetailItem({
            receivedName: $currentConversationUserName,
            isReceived: true,
            content: HomeConstants.CONVERSATION_LIST_APPLET[1],
            currentFeatureIndex: $currentFeatureIndex
          })
          ConversationDetailItem({
            receivedName: $currentConversationUserName,
            isReceived: !true,
            content: HomeConstants.CONVERSATION_LIST_APPLET[2],
            currentFeatureIndex: $currentFeatureIndex
          })
        } else {
          ConversationDetailItem({
            receivedName: $currentConversationUserName,
            isReceived: true,
            content: HomeConstants.CONVERSATION_LIST_DOCUMENT[0],
            isDocumentMsg: true,
            currentFeatureIndex: $currentFeatureIndex
          })
          ConversationDetailItem({
            receivedName: $currentConversationUserName,
            isReceived: true,
            content: HomeConstants.CONVERSATION_LIST_DOCUMENT[1],
            currentFeatureIndex: $currentFeatureIndex
          })
          ConversationDetailItem({
            receivedName: $currentConversationUserName,
            isReceived: !true,
            content: HomeConstants.CONVERSATION_LIST_DOCUMENT[2],
            currentFeatureIndex: $currentFeatureIndex
          })
        }
        Blank()
        ConversationDetailBottom()
      }
      .height('100%')
      .width('100%')
      .backgroundColor($r('app.color.background_color_grey'))
      .padding({
        bottom: deviceInfo.deviceType !== '2in1' ? 28 : 0
      })
    }
    .hideTitleBar(true)
  }
}

交互归一

不同类型的智能设备,系统已经针对不同的交互方式做了适配,实现了 交互归一 ,因此开发者无需额外关注用户不同的交互方式。

本场景包含的交互归一方式如下(以触控屏为例):

1、单指点击对应组件。
2、单指滑动List和Scroll组件。
3、走焦(参考一多开发实例(长视频))。

最后

总是有很多小伙伴反馈说:鸿蒙开发不知道学习哪些技术?不知道需要重点掌握哪些鸿蒙开发知识点? 为了解决大家这些学习烦恼。在这准备了一份很实用的鸿蒙全栈开发学习路线与学习文档给大家用来跟着学习。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植……等)技术知识点。

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN/733GH/overview

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):

在这里插入图片描述

《OpenHarmony源码解析》:

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

图片

OpenHarmony 设备开发学习手册:https://gitcode.com/HarmonyOS_MN/733GH/overview

图片

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐