一、这玩意儿是啥

股票类应用的目的是让用户更加便捷地办理金融业务。常见的有银行理财、股票、基金等类型的应用和业务场景,核心场景有数据查看、股票交易等。

股票类应用有以下特点:

丰富的信息聚合。

图表数据高效展示。

便捷高效的交互方式。

此类型的应用在多端设备的使用过程中,不仅要保障用户在办理金融业务的过程中正常使用,还要尽可能提升大屏幕的交互效率。

本文以目前流行的垂类市场中的股票类应用作为典型案例,详细介绍"一多"在实际开发中的应用,主要涵盖自选股和个股详情两个典型页面,展示其在直板机、双折叠、三折叠、阔折叠、平板五种产品形态上的"一次开发,多端部署"。

二、UX咋设计

以下是股票界面在直板机、阔折叠、双折叠设备及平板上的UX设计图。

自选股页面

页面主要包含:页面底部/侧边页签、标题、股票指数、股票列表-工具栏、股票列表。

个股详情页

页面主要包含:顶部标题、行情列表数据、分时Tab、曲线图和柱状图以及股票实时交易列表、讨论Tab、底部交易操作行。

半模态-股票交易弹窗

断点设计

股票类应用通常用于直板机、双折叠、三折叠、阔折叠和平板设备。以下为这些常用设备和屏幕尺寸(横向/纵向断点)的股票类应用适配策略:

阔折叠外屏(sm/md)、直板机(sm/lg):基础视图。

双折叠展开态、阔折叠横向展开(md):相较于直板机,双折叠在展开状态下,开发者可利用更大的屏幕空间扩展股票图表显示内容,以增强应用的沉浸感和信息显示,例如股票详情、股票K线图。

平板、三折叠展开态(lg):相较于直板机,平板设备在横向模式下能够显示更多内容。默认为双栏显示,左侧栏主要用于显示股票列表,右侧栏则展示股票详情信息,例如股票K线图。当点击全屏按钮时,页面切换至单栏显示,页面主要展示股票详情信息,此时股票K线图会占据更大比例,显示内容更加丰富。

三、工程咋管理

在创建"一多"工程时,开发者会面临工程结构目录的划分问题。考虑到复用性和可维护性,本文以股票类应用为例,提供推荐的参考方案。

HarmonyOS的分层架构包括产品定制层、基础特性层和公共能力层,为开发者提供清晰、高效、可扩展的设计架构。

股票类应用根据一多推荐的commons、features、products的"三层工程架构"划分目录。其中四个页面功能不同,互不依赖,根据页面划分为两个features(基础特性层):首页-home、股票详情页-stockdetail。公共常量、媒体播放工具以及窗口管理工具等需要被不同页面依赖引用的内容,划分为一个commons(公共能力层):基础能力-base。products(产品定制层)定制了程序标准启动流程和多场景协同场景的入口能力。

工程结构如下:

├──commons                   
│  └──base/src/main/ets
│     ├──constants                        // 公共常量
│     └──utils                            // 公共工具
├──features
│  ├──home/src/main                   
│  │  ├──ets
│  │  │  ├──models                        // 股票类数据
│  │  │  ├──pages                         // 应用首页内容
│  │  │  └──views                         // 首页视图组件
│  │  └──resources                        // 应用静态资源目录
│  └──stockdetail/src/main               
│     ├──ets
│     │  ├──chartmodels                   // 图标组件
│     │  ├──models                        // 股票类数据
│     │  ├──pages                         // 股票详情页
│     │  └──views                         // 股票视图组件
│     └──resources                        // 应用静态资源目录
└──products
   ├──phone/src/main/ets
   │  ├──entryability                     // 程序入口
   │  ├──entrybackupability  
   │  ├──pages                            // 首页
   │  ├──splitScreenAbility               // 分屏入口
   │  └──splitScreenBackupAbility
   └──phone/src/main/resources            // 应用静态资源目录

四、窗口咋适配

窗口模式

多设备股票类界面示例,根据适配的设备,涉及全屏模式、分屏模式、悬浮窗模式、自由窗口模式。其中分屏模式与悬浮窗通常无特殊设计,可通过系统方式进入。应用监听窗口尺寸变化,通过断点刷新UI,将自动适配全屏、分屏、悬浮窗、自由窗口模式下的布局。

使用系统UI组件进入全景多窗,实现一个应用多个窗口并行运行的体验,可参考功能开发:应用多实例-多股比价。

窗口方向

通过设置window.setPreferredOrientation()使应用跟随传感器自动旋转,可旋转至竖屏、横屏、反向竖屏、反向横屏四个方向。本示例使用跟随桌面的旋转模式。

窗口沉浸式

根据UX设计,实现不同窗口模式(全屏、分屏、悬浮窗)下窗口的沉浸式。全屏、分屏和悬浮窗的沉浸式均可通过setWindowLayoutFullscreen()实现,并进行动态安全区避让。

五、自选股页面咋开发

页面布局

将自选股页划分为四个部分,效果图如下:

对各个区域使用的多种能力进行分析,实现方案如下:

区域1:底部/侧边页签

借助响应式组件Tabs实现。

区域2:指数

最后一个组件固定,其他组件使用List组件实现延伸能力,随着设备宽度变大,页签间距变大,页面能够展示更多页签内容。

区域3:股票列表-工具栏

文字和功能按钮中间增加Blank组件,实现拉伸能力。

区域4:股票列表

通过使用List组件设置固定宽度和Scroll组件,可实现股票列表数据的上下或左右滑动。同时,支持对不同列设置不同的justifyContent,以便实现各列的不同对齐方式。

整个页面使用的是分栏布局,在股票列表区域,点击某一股票时,平板上会分栏显示该股票的详细信息。

交互开发

页签切换、自选股查看和跳转等交互均为简单的点击事件,开发过程可参考多设备交互。

六、股票详情页咋开发

页面布局

将个股详情页划分为六个部分,效果图如下:

对各区域使用的能力进行分析,实现方案如下:

区域1:交易操作行

通过为"去交易"按钮设置layoutWeight布局权重,并使用Blank组件结合断点,实现该按钮的自适应拉伸。

区域2:标题

居中显示,其他操作两端对齐,空白空间使用Blank组件实现自适应布局拉伸能力。

区域3:行情列表数据

通过栅格布局并结合断点,控制在不同断点下显示不同的列数,列表自适应两列变多列。

区域4:中间Tab

通过List组件的space属性并结合断点,控制在不同断点下ListItem之间的间距。

区域5:曲线图和柱状图

使用layoutWeight属性实现拉伸能力。

区域6:讨论的Tab

通过List组件的space属性并结合断点,控制在不同断点下ListItem之间的间距。

交互开发

图表切换、股票交易等交互均为简单的点击事件,开发过程可参考多设备交互。

七、功能开发:应用多实例-多股比价

应用通过系统提供的MultiWindowEntryInAPP组件,配置需拉起的bundleName与UIAbility(仅限本应用,无法拉起其他应用),单击组件页面进入分屏(双股对比),在分屏状态下,在点击组件进入全景多窗(三股对比)。

下表以Mate X5设备为例,展示应用在分屏及全景多窗模式下的效果。

约束条件

MultiWindowEntryInAPP组件依赖全景多窗特性,只有当前设备及屏幕状态支持全景多窗,才支持设置此功能。目前支持全景多窗的设备型态有:

双折叠:展开态。

三折叠:双屏态,三屏态的横屏态。

平板:横屏态。

对于不支持的设备型态,该组件不可交互,不响应点击事件。

建议开发者在分屏副窗口左上角设置关闭按钮以直接关闭副窗口,本案例使用返回按钮,是股票比价场景需返回上级页面的特定需求。

开发步骤

应用使用MultiWindowEntryInAPP组件主动分屏或进入全景多窗。具体开发步骤如下:

导入模块:

import { MultiWindowEntryInAPP, MultiWindowEntryInAPPAttribute} from '@kit.UIDesignKit';
import { TextModifier } from '@kit.ArkUI';
import { Want } from '@kit.AbilityKit';

使用MultiWindowEntryInAPP组件,并且设置组件参数:

@Component
export struct MultiWindowEntryComponent {
  @Link textModifier: TextModifier;
  @Link want: Want;
  @State isShowMultiWindowEntry: boolean = false;
  // ...

  build() {
    Row() {
      MultiWindowEntryInAPP({
        want: this.want,
        isShowSubtitle: false,
        multiWindowEntryInAPPStyle: {
          iconOptions: {
            iconSize: 24,
            iconColor: $r('sys.color.font_primary'),
            iconWeight: FontWeight.Normal,
            backgroundColor: $r('sys.color.comp_background_tertiary')
          },
          subtitleOptions: {
            modifier: this.textModifier.fontColor(Color.Black)
          }
        }
      })
        .id("MultiWindowEntryInAPP")
    }
    .visibility(this.isShowMultiWindowEntry ? Visibility.Visible : Visibility.None)
  }
}

导入封装好的MultiWindowEntryComponent组件,并且设置组件参数:

import { MultiWindowEntryComponent } from './MultiWindowEntryComponent';
@Component
export struct TopTitleBar {
  // ...
  @State textModifier: TextModifier = new TextModifier();
  @State splitScreenWant: Want = {
    // Modify the bundleName, moduleName and abilityName of the current application, and launch the UIAbility within the application.
    bundleName: 'com.example.multiticketclass',
    moduleName: 'phone',
    abilityName: 'SplitScreenAbility',
  };
  // ...
  build() {
    Row() {
      // ...
      // The area displayed by the icon on the right side
      Row({ space: 16 }) {
        // split screen
        Row() {
          MultiWindowEntryComponent({
            textModifier: this.textModifier,
            want: this.splitScreenWant
          })
        }
        // ...
      }
    }
    // ...
  }
}

应用内分屏高阶组件窗口路由方案

建议开发者采用应用级多实例来实现分屏页面的路由管理。以下是页面级多实例与应用级多实例的主要区别,多股比价场景的分屏路由管理采用应用级多实例:

页面级多实例:

  • 每个UI Ability创建后,基于当前节点改造路由栈
  • 需要路由改造
  • 以当前路由节点生成路由表,开发者手动定义路由方案

应用级多实例(推荐):

  • 每个UI Ability创建独立的相同路由栈
  • 不需要路由改造
  • 每个窗口启动时创建独立路由栈(路由表相同)

应用内分屏高阶组件窗口路由退栈方案

在多股比价场景中,当在应用内进行分屏操作时,新增窗口应保留当前浏览的股票信息,而主窗口则应回到股票列表。为实现这一功能,建议在新窗口的启动生命周期中触发事件,原窗口通过监听该事件并执行退栈操作。

在分屏程序的入口SplitScreenAbility.ets中的onCreate()和onNewWant()生命周期中进行事件触发:

let eventData: emitter.EventData = {
  data: {
    'isStart': 1,
    'id': 1
  }
};
let innerEvent: emitter.InnerEvent = {
  eventId: 1,
  priority: emitter.EventPriority.HIGH
};

export default class SplitScreenAbility extends UIAbility {
  // ...

  onCreate(): void {
    // ...
    emitter.emit(innerEvent, eventData);
  }

  onNewWant(): void {
    // ...
    emitter.emit(innerEvent, eventData);
  }
  // ...
}

在原窗口进行事件监听并做退栈处理:

@Component
export struct TopTitleBar {
  // ...
  private innerEvent: emitter.InnerEvent = { eventId: 1 };
  private callBack: Callback<emitter.EventData> = (eventData: emitter.EventData) => {
    Logger.info(`eventData:${eventData}`);
    if (this.pageInfos?.pop) {
      this.pageInfos.pop();
    }
  };
  aboutToAppear(): void {
    if (this.context.abilityInfo.name === 'EntryAbility') {
      emitter.on(this.innerEvent, this.callBack);
    }
  }
  dispose(): void {
    emitter.off(this.innerEvent.eventId, this.callBack);
  }
  // ...
  }
}

应用内分屏高阶组件按钮显隐策略

在应用内分屏高阶组件时,对不支持全景多窗的设备隐藏分屏按钮。方案的主要逻辑为:

监听窗口尺寸变化:

public onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => {
  this.mainWindowInfo.windowSize = windowSize;
  this.mainWindowInfo.widthBp = this.uiContext!.getWindowWidthBreakpoint();
  this.mainWindowInfo.heightBp = this.uiContext!.getWindowHeightBreakpoint();
};
// ...
updateWindowInfo(): void {
  try {
    // ...
    // Register for window size change monitoring, update window size and width/height breakpoint.
    this.mainWindow.on('windowSizeChange', this.onWindowSizeChange);
    // ...
    AppStorage.setOrCreate('mainWindowInfo', this.mainWindowInfo);
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(0x0000, `TestLog`, `Failed to update window info. Code: ${err.code}, message: ${err.message}`);
  }
}

尺寸变化时获取按钮节点,查询其enabled属性:

@StorageLink('mainWindowInfo') @Watch('watchWindow') mainWindowInfo: WindowInfo = new WindowInfo();
aboutToAppear(): void {
  this.watchWindow();
}

private watchWindow(): void {
  setTimeout(()=> {
    let nodeStr = JSON.stringify(this.getUIContext()?.getFrameNodeById("MultiWindowEntryInAPP")?.getInspectorInfo());
    if (nodeStr?.search('"enabled":true') && nodeStr?.search('"enabled":true') !== -1) {
      this.isShowMultiWindowEntry = true;
    } else {
      this.isShowMultiWindowEntry = false;
    }
  })
}

根据enabled属性通过visibility控制组件的显隐:

Row() {
  MultiWindowEntryInAPP({
    want: this.want,
    isShowSubtitle: false,
    multiWindowEntryInAPPStyle: {
      // ...
    }
  })
    .id("MultiWindowEntryInAPP")
}
.visibility(this.isShowMultiWindowEntry ? Visibility.Visible : Visibility.None)

八、避坑指南

坑1:分屏按钮显隐

对不支持全景多窗的设备,分屏按钮应该隐藏,不能点击。

咋解决?监听窗口尺寸变化,获取按钮节点的enabled属性,通过visibility控制组件的显隐。

坑2:分屏窗口路由退栈

在应用内进行分屏操作时,新增窗口应保留当前浏览的股票信息,而主窗口则应回到股票列表。

咋解决?在新窗口的启动生命周期中触发事件,原窗口通过监听该事件并执行退栈操作。

坑3:应用级多实例 vs 页面级多实例

页面级多实例需要路由改造,开发者手动定义路由方案;应用级多实例不需要路由改造,每个窗口启动时创建独立路由栈。

咋解决?建议开发者采用应用级多实例来实现分屏页面的路由管理,减少开发工作量。

坑4:MultiWindowEntryInAPP组件约束

MultiWindowEntryInAPP组件依赖全景多窗特性,只有当前设备及屏幕状态支持全景多窗,才支持设置此功能。

咋解决?对于不支持的设备型态,该组件不可交互,不响应点击事件,需要通过enabled属性判断并控制显隐。

坑5:股票列表布局

股票列表需要支持对不同列设置不同的justifyContent,以便实现各列的不同对齐方式。

咋解决?通过使用List组件设置固定宽度和Scroll组件,可实现股票列表数据的上下或左右滑动,并支持自定义对齐方式。

坑6:行情列表数据自适应

行情列表数据需要根据不同断点自适应两列变多列。

咋解决?通过栅格布局并结合断点,控制在不同断点下显示不同的列数。

九、总结

股票类应用的多设备开发,从UX设计、工程管理、窗口适配、界面开发、功能开发等方面,都有值得学习的最佳实践。

UX设计方面,股票类应用的特点是丰富的信息聚合、图表数据高效展示、便捷高效的交互方式。在不同设备上,要保障用户正常使用,还要尽可能提升大屏幕的交互效率。

工程管理方面,三层架构划分清晰,commons、features、products各司其职,便于维护和复用。特别是分屏入口(splitScreenAbility)的设计,为多股比价场景提供了基础。

窗口适配方面,涉及全屏模式、分屏模式、悬浮窗模式、自由窗口模式。应用监听窗口尺寸变化,通过断点刷新UI,自动适配不同模式下的布局。

界面开发方面,自选股页面使用分栏布局,股票详情页使用栅格布局、List组件、layoutWeight等多种能力。关键是根据不同断点设置不同的属性值,实现自适应布局。

功能开发方面,应用多实例-多股比价是一个亮点。通过MultiWindowEntryInAPP组件,实现分屏(双股对比)和全景多窗(三股对比),提升大屏设备的交互效率。

最后,股票类应用的核心是金融业务的便捷性和图表数据的展示效率。开发时要多关注这些细节,才能做出好的产品。

Logo

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

更多推荐