最近收到很多小伙伴反馈,想基于扩展的TS语言(eTS)进行HarmonyOS应用开发,但是不知道代码该从何处写起,从0到1的过程让新手们抓狂。

本期我们将带来“分布式计算器”的开发,帮助大家了解声明式开发范式的UI描述、组件化机制、UI状态管理、渲染控制语法等核心机制和功能。下面我们直接进入正题。

一、整体介绍

分布式计算器可以进行简单的数值计算,并支持远程拉起另一个计算器FA,实现两个FA进行协同计算。

如图1所示,分布式计算器界面主要由“键盘”、“显示”及“标题栏”三个模块组成。其中,“键盘”与“显示”模块负责响应用户点击并控制运算表达式及运算结果的显示,实现了基础的计算功能。“菜单栏”模块为计算器顶部的菜单栏,是分布式计算功能的入口。

那么,如何实现分布式计算器各模块的功能?下面我们将从组件化、声明式描述和状态管理三个维度来解析分布式计算器的实现。

图1  计算器界面

1. 组件化

ArkUI开发框架定义了一些具有特殊含义的组件管理装饰器,如图2所示:

图2 组件管理装饰器

根据声明式UI的组件化思想,我们可以将通过组件管理装饰器将计算器界面上的各个模块组件化为一个个独立的UI单元。

2. 声明式描述

通过ArkUI开发框架提供的一系列基础组件,如Column、Text、Divider、Button等,以声明方式进行组合和扩展来对各个模块进行描述,包括参数构造配置、属性配置、事件配置以及子组件配置等,并通过基础的数据绑定和事件处理机制实现各个模块的逻辑交互。

3. 状态管理

ArkUI开发框架定义了一些具有特殊含义的状态管理装饰器,如图3所示:

图3 状态管理装饰器

通过状态管理装饰器装饰组件拥有的状态属性,当装饰的变量更改时,组件会重新渲染更新UI界面。

以上就是实现分布式计算器的核心原理,下面我们将为大家带来分布式计算器的基础计算功能与分布式功能的具体实现。

二、基础计算功能的实现

上文中提到,分布式计算器的基础计算功能由键盘模块及显示模块实现。

1. 键盘模块

键盘模块响应了用户的点击,并实现了计算器的基本功能。下面我们将介绍键盘布局以及键盘功能的实现。

(1) 键盘布局

计算器界面上的键盘,其实是一张张图片按照 4*5格式排列,如图4所示:

图4 键盘模块

首先,我们需要将所有图片保存至项目的media文件夹下,并初始化为ImageList,代码如下:

export function obtainImgVertical(): Array<Array<ImageList>> {
let list =
[
[
{ img: $r('app.media.ic_cal_seven'), value: '7' },
{ img: $r('app.media.ic_cal_eight'), value: '8' },
{ img: $r('app.media.ic_cal_nine'), value: '9' }
],
[
{ img: $r('app.media.ic_cal_four'), value: '4' },
{ img: $r('app.media.ic_cal_five'), value: '5' },
{ img: $r('app.media.ic_cal_six'), value: '6' }
],
]
return list
}
export function obtainImgV(): Array<ImageList> {
let list =
[
{ img: $r('app.media.ic_cal_delete'), value: '' },
{ img: $r('app.media.ic_cal_minus'), value: '-' },
{ img: $r('app.media.ic_cal_plus'), value: '+' },
{ img: $r('app.media.ic_cal_equal'), value: '=' }
]
return list
}

然后,我们需要对键盘模块进行组件化操作。这里我们通过@Component装饰器让键盘模块成为一个独立的组件。

最后,使用ArkUI开发框架提供的内置组件及属性方法进行声明性描述。这里我们使用了Grid组件进行布局,并通过ForEach组件来迭代图片数组实现循环渲染,同时还为每张图片添加了ClickButton事件方法。代码如下:

@Component
export struct ButtonComponent {
private isLand: boolean
private onInputValue: (result) => void
build() {
Row() {
Grid() {
ForEach(obtainImgV(), (item, index) => {
GridItem() {
Image(item.Img)
.margin({ top: 5 })
.onClick(() => {
this.onInputValue(item.value)
})
}
.rowStart(index)
.rowEnd(index === 3 ? index + 1 : index)
.columnStart(3)
.columnEnd(3)
})
ForEach(obtainImgVertical(), (item) => {
ForEach(item, (item) => {
GridItem() {
Image(item.Img)
.margin({ top: 5 })
.onClick(() => {
this.onInputValue(item.value)
})
}
})
})
}
}
}
}

(2) 功能实现

按键功能包含了“归零”、“清除”、“计算”三个功能。

① 当用户点击“C”按钮后,运算表达式与运算结果“归零”,代码如下:

onInputValue = (value) => {
if (value === 'C') { // 当用户点击C按钮,表达式和运算结果归0
this.expression = ''
this.result = ''
return
}
// 输入数字,表达式直接拼接,计算运算结果
this.expression += value
this.result = JSON.stringify(MATH.evaluate(this.expression))
}

② 当用户点击“X”按钮后,删除运算表达式的最后一个字符。代码如下:

onInputValue = (value) => {
if (value === '') { // 当用户点击删除按钮,表达式删除上一次的输入,重新运算表达式
this.expression = this.expression.substr(0, this.expression.length - 1)
this.result = JSON.stringify(MATH.evaluate(this.expression))
return
}
if (this.isOperator(value)) { // 当用户输入的是运算符
// 判断表达式最后一个字符是运算符则覆盖上一个运算符,否则表达式直接拼接
if (this.isOperator(this.expression.substr(this.expression.length - 1, this.expression.length))) {
this.expression = this.expression.substr(0, this.expression.length - 1)
this.expression += value
} else {
this.expression += value
}
return
}
// 输入数字,表达式直接拼接,计算运算结果
this.expression += value
this.result = JSON.stringify(MATH.evaluate(this.expression))
}

③ 当用户点击“=”按钮后,将调用JavaScript的math.js库对表达式进行计算。代码如下:

import { create, all } from 'mathjs'
onInputValue = (value) => {
if (value === '=') { // 当用户点击=按钮
this.result = ''
// 判断表达式最后一个字符是运算符,运算结果需要去掉最后一个运算符运算,否则直接运算
if (this.isOperator(this.expression.substr(this.expression.length - 1, this.expression.length))) {
this.expression = JSON.stringify(MATH.evaluate(this.expression.substr(0, this.expression.length - 1)))
} else {
this.expression = JSON.stringify(MATH.evaluate(this.expression))
}
return
}
// 输入数字,表达式直接拼接,计算运算结果
this.expression += value
this.result = JSON.stringify(MATH.evaluate(this.expression))
}

注:计算功能的实现依赖于JavaScript的math.js库,使用前需通过npm install mathjs--save命令下载并安装math.js库。

2. 显示模块

显示模块实现了“键入的运算表达式”与“运算结果”的显示,本质上是Text文本,如图5所示:

图5 显示模块

首先我们通过@Component装饰器使该模块具有组件化能力,然后在build方法里描述UI结构,最后使用@Link状态装饰器管理组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。代码如下:

@Component
export struct InPutComponent {
private isLand: boolean
@Link result: string
@Link expression: string
build() {
Stack({ alignContent: this.isLand ? Alignment.BottomStart : Alignment.TopEnd }) {
Column() {
//运算表达式文本框
Scroll() {
Text(this.expression)
.maxLines(1)
.opacity(0.9)
.fontWeight(400)
.textAlign(TextAlign.Start)
.fontSize(this.isLand ? 50 : 35)
}
.width('90%')
.scrollable(ScrollDirection.Horizontal)
.align(this.isLand ? Alignment.Start : Alignment.End)
//运算结果文本框
Scroll() {
Text(this.result)
.maxLines(1)
.opacity(0.38)
.textAlign(TextAlign.Start)
.fontSize(this.isLand ? 45 : 30)
.margin(this.isLand ? { bottom: 64 } : {})
}
}
}
}
}

至此,一个初具计算功能的计算器就实现了。下面我们将实现计算器的分布式功能。

三、分布式功能的实现

计算器的分布式功能以菜单栏模块为入口,并基于分布式设备管理与分布式数据管理技术实现。

1. 菜单栏模块

“菜单栏”模块为计算器顶部菜单栏,是计算器分布式功能的入口。

图6 菜单栏模块

如图6所示,当用户点击图标 时,执行terminate()方法,退出计算器应用。当用户点击 按钮时,执行showDialog()方法,界面上弹出的分布式设备列表弹窗,选择设备后将获取分布式数据管理的权限,最后实现远端设备的拉起。代码如下:

@Component
export struct TitleBar {
build() {
Row() {
Image($r("app.media.ic_back"))
.height('60%')
.margin({ left: 32 })
.width(this.isLand ? '5%' : '8%')
.objectFit(ImageFit.Contain)
//执行terminate()方法,退出计算器应用
.onClick(() => {
app.terminate()
})
Blank().layoutWeight(1)
Image($r("app.media.ic_hop"))
.height('60%')
.margin({ right: 32 })
.width(this.isLand ? '5%' : '8%')
.objectFit(ImageFit.Contain)
//执行showDialog()方法,界面上弹出的分布式设备列表弹窗
.onClick(() => {
this.showDiainfo()
})
}
.width('100%')
.height(this.isLand ? '10%' : '8%')
.constraintSize({ minHeight: 50 })
.alignItems(VerticalAlign.Center)
}
}

2. 分布式设备管理

在分布式计算器应用中,分布式设备管理包含了分布式设备搜索、分布式设备列表弹窗、远端设备拉起三部分。首先在分布式组网内搜索设备,然后把设备展示到分布式设备列表弹窗中,最后根据用户的选择拉起远端设备。

(1) 分布式设备搜索

通过SUBSCRIBE_ID搜索分布式组网内的远端设备,代码如下:

startDeviceDiscovery() {
SUBSCRIBE_ID = Math.floor(65536 * Math.random())
let info = {
subscribeId: SUBSCRIBE_ID,
mode: 0xAA,
medium: 2,
freq: 2,
isSameAccount: false,
isWakeRemote: true,
capability: 0
}
Logger.info(TAG, `startDeviceDiscovery ${SUBSCRIBE_ID}`)
this.deviceManager.startDeviceDiscovery(info)
}

(2) 分布式设备列表弹窗

分布式设备列表弹窗实现了远端设备的选择,如图7所示,用户可以根据设备名称选择相应的设备进行协同计算。

图7 分布式设备列表弹窗

这里我们使用@CustomDialog装饰器来装饰分布式设备列表弹窗,代码如下:

@CustomDialog
export struct DeviceDialog {
build() {
Column() {
List() {
ForEach(this.deviceList, (item, index) => {
ListItem() {
Row() {
Text(item.deviceName)
.fontSize(21)
.width('90%')
.fontColor(Color.Black)
Image(index === this.selectedIndex ? $r('app.media.checked') : $r('app.media.uncheck'))
.width('8%')
.objectFit(ImageFit.Contain)
}
.height(55)
.margin({ top: 17 })
.onClick(() => {
if (index === this.selectedIndex) {
return
}
this.selectedIndex = index
this.onSelectedIndexChange(this.selectedIndex)
})
}
}, item => item.deviceName)
}
}
}
}

(3) 远端设备拉起

通过startAbility(deviceId)方法拉起远端设备的FA,代码如下:

startAbility(deviceId) {
featureAbility.startAbility({
want: {
bundleName: 'ohos.samples.DistributeCalc',
abilityName: 'ohos.samples.DistributeCalc.MainAbility',
deviceId: deviceId,
parameters: {
isFA: 'FA'
}
}
}).then((data) => {
this.startAbilityCallBack(DATA_CHANGE)
})
}

3. 分布式数据管理

分布式数据管理用于实现协同计算时数据在多端设备之间的相互同步。我们需要创建一个分布式数据库来保存协同计算时数据,并通过分布式数据通信进行同步。

(1) 管理分布式数据库

创建一个KVManager对象实例,用于管理分布式数据库对象。代码如下:

async createKvStore(callback) {
//创建一个KVManager对象实例
this.kvManager = await distributedData.createKVManager(config)
let options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: 1,
securityLevel: 1,
}
// 通过指定Options和storeId,创建并获取KVStore数据库,并通过Promise方式返回,此方法为异步方法。
this.kvStore = await this.kvManager.getKVStore(STORE_ID, options)
callback()
}

(2) 订阅分布式数据变化

通过订阅分布式数据库所有(本地及远端)数据变化实现数据协同,代码如下:

kvStoreModel.setOnMessageReceivedListener(DATA_CHANGE, (value) => {
if (this.isDistributed) {
if (value.search(EXIT) != -1) {
Logger.info(TAG, `EXIT ${EXIT}`)
featureAbility.terminateSelf((error) => {
Logger.error(TAG, `terminateSelf finished, error= ${error}`)
});
} else {
this.expression = value
if (this.isOperator(this.expression.substr(this.expression.length - 1, this.expression.length))) {
this.result = JSON.stringify(MATH.evaluate(this.expression.substr(0, this.expression.length - 1)))
} else {
this.result = JSON.stringify(MATH.evaluate(this.expression))
}
}
}
})

至此,具有分布式能力的计算器就实现了。期待广大开发者能基于TS扩展的声明式开发范式开发出更多有趣的应用。

点击链接,可获取分布式计算器完整代码:https://gitee.com/openharmony/app_samples/tree/master/Preset/DistributeCalc

搜索

复制

满满干货!手把手教你实现基于eTS的分布式计算器的更多相关文章

  1. 周一干货~手把手教你安装 Visual Studio 安卓模拟器

    干货~手把手教你安装 Visual Studio 安卓模拟器 转 http://mini.eastday.com/mobile/171107134734194.html# 今天软妹为大家带来一篇来自M ...

  2. 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接

    本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...

  3. 手把手教你写基于C++ Winsock的图片下载的网络爬虫

    手把手教你写基于C++ Winsock的图片下载的网络爬虫 先来说一下主要的技术点: 1. 输入起始网址,使用ssacnf函数解析出主机号和路径(仅处理http协议网址) 2. 使用socket套接字 ...

  4. 庐山真面目之十一微服务架构手把手教你搭建基于Jenkins的企业级CI/CD环境

    庐山真面目之十一微服务架构手把手教你搭建基于Jenkins的企业级CI/CD环境 一.介绍 说起微服务架构来,有一个环节是少不了的,那就是CI/CD持续集成的环境.当然,搭建CI/CD环境的工具很多, ...

  5. 干货 | 手把手教你搭建一套OpenStack云平台

    1 前言 今天我们为一位朋友搭建一套OpenStack云平台. 我们使用Kolla部署stein版本的OpenStack云平台. kolla是用于自动化部署OpenStack的一个项目,它基于dock ...

  6. 手把手教你学会 基于JWT的单点登录

      最近我们组要给负责的一个管理系统 A 集成另外一个系统 B,为了让用户使用更加便捷,避免多个系统重复登录,希望能够达到这样的效果--用户只需登录一次就能够在这两个系统中进行操作.很明显这就是单点登 ...

  7. Delphi - 手把手教你基于D7+Access常用管理系统架构的设计与实现 (更新中)

    前言 从事软件开发工作好多年了,学的越深入越觉得自己无知,所以还是要对知识保持敬畏之心,活到老,学到老! 健身和代码一样都不能少,身体是革命的本钱,特别是我们这种高危工种,所以小伙伴们运动起来!有没有 ...

  8. 【转】手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

    1.引言 特别说明:本文内容仅用于即时通讯技术研究和学习之用,请勿用于非法用途.如本文内容有不妥之处,请联系JackJiang进行处理!   我司有关部门为了获取黑产群的动态,有同事潜伏在大量的黑产群 ...

  9. 手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

    1.引言 特别说明:本文内容仅用于即时通讯技术研究和学习之用,请勿用于非法用途.如本文内容有不妥之处,请联系JackJiang进行处理!   我司有关部门为了获取黑产群的动态,有同事潜伏在大量的黑产群 ...

随机推荐

  1. Ueditor上传本地音频MP3

    遇到一个项目,客户要求能在编辑框中上传录音文件.用的是Ueditor编辑器,但是却不支持本地MP3上传并使用audio标签播放,只能搜索在线MP3,实在有点不方便.这里说一下怎么修改,主要还是利用原来 ...

  2. Django ElasticSearch Ionic 打造 GIS 移动应用 —— 架构设计

    搜索引擎是个好东西,GIS也是个好东西.当前还有Django和Ionic.最后效果图 构架设计 对我们的需求进行简要的思考后,设计出了下面的一些简单的架构. GIS架构说明 -- 服务端 简单说明: ...

  3. H5本地存储:sessionStorage和localStorage

    作者:心叶时间:2018-05-01 18:30 H5提供了二种非常好用的本地存储方法:sessionStorage和localStorage,下面分别介绍一下: 1.sessionStorage:保 ...

  4. 自定义View的onDraw 函数不执行

    解决办法:    在自定义的View 的构造方法中添加一句话:   this.setWillNotDraw(false);解释:那么加这条语句的作用是什么?先看API:        If this ...

  5. react在移动端的自适应布局

    react+flexible适配布局 (1)npm i lib-flexible --save (2)npm i postcss-px2rem --save (3)在 node_modules/rea ...

  6. oracle查询出现科学计数法问题

  7. Struts bean:define标签用法

    bean:define:有三个用途 一是定义新字符串常量: <bean:define id="foo" value="This is a new String&qu ...

  8. 库存管理系统实现 C语言课设

    1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 //定义一个商品结构体 6 ...

  9. Linux---必备命令(1)

    文件和目录 # 更改目录位置 cd /tmp # 进入文件夹 cd dirr # 新建文件夹 mkdir dirr # 创建文本 touch text.txt # 显示当前目录下的所有文件,包含已'. ...

  10. Windows和ubuntu下更改pip国内镜像

    windows下更改pip国内镜像 # 在C:\Users\admin路径下创建pip文件夹,然后创建pip.ini文件, 并在文件下写入 [global] index-url = http://py ...