1 简介
生活中的零碎信息太容易忘记,「记事APP」作为记录噜苏的小帮助,时刻记录用户关心的内容,分类整理,高效编辑,快捷分享。Less is More,借助HarmonyOS NEXT丰富的原生能力,一步操纵完成各种所想,抓住每一刻灵感。
本篇将介绍如何利用HarmonyOS NEXT原生能力开发灵感速记APP。效果为:
- 化繁为简,便捷高效
- 聊天式一键添加备忘、关联日程、强提示消息、私密消息等轻量条记。
- 可分类、搜索、分享、删除等管理记录的每条内容。
- 统一生态,有趣好用:
- 可共享消息到生态互联设备(灵动看板)。
- 支持多端结构。
2 环境搭建
我们起首需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
软件要求
- DevEco Studio版本:DevEco Studio NEXT Developer Preview2及以上。
- HarmonyOS SDK版本:HarmonyOS NEXT Developer Preview2 SDK及以上。
硬件要求
- 设备范例:华为手机。
- HarmonyOS体系:HarmonyOS NEXT Developer Preview2及以上。
环境搭建
- 安装DevEco Studio,详情请参考[下载]和[安装软件]。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常利用,详情请参考[设置开发环境]。
- 开发者可以参考以下链接,完成设备调试的相关设置:
3 代码结构解读
本篇文档只对核心代码进行讲解,对于完备代码,开源后提供下载链接。
- .
- |-- common
- | |-- BreakPointSystem.ets //断点-多端布局
- | |-- CommonConstant.ets //常量
- | |-- DateUtil.ets //时间日期工具
- | |-- InspiDataModule.ets //灵感数据模型
- | |-- Logger.ets //日志工具
- | |-- calendar.ets //日历日程工具
- | |-- database //RDB数据库,实现持久化存储
- | | |-- InspirationTable.ets
- | | |-- RDB.ets
- | | `-- inspirationData.ets
- | |-- messageManager //后台提醒、日程消息
- | | |-- BackgroundReminder.ets
- | | `-- CalenderMeaasge.ets
- | |-- microphone //语音输入
- | | `-- Recorder.ets
- | `-- shareTool.ets //分享工具
- |-- entryability
- | `-- EntryAbility.ts
- |-- entryformability
- | `-- EntryFormAbility.ets
- |-- pages
- | |-- Index.ets //入口页面
- | `-- SettingPage.ets //设置页面
- |-- subview
- | |-- AddPanel.ets //添加笔记组件
- | |-- InspiComponent.ets //笔记列表组件
- | |-- InspirationItemView.ets //单条笔记组件
- | |-- InspirationTotalView.ets//笔记分类视图
- `-- widget22
- `-- pages
- `-- Widget22Card.ets //2*2桌面卡片
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.
复制代码 4 构建应用主界面
灵感速记应用步伐遵循多端、极简页面、功能一触即达的计划理念。以手机端为例,如下图所示,首页结构了95%的功能,自下而上依次是添加和共享条记、条记查看、管理条记的三大功能区。
在主页面中利用[断点])来分配结构,比方在手机端首页只展示消息列表视图,在折叠屏或平板上屏幕右侧新增了分类视图。整体的结构框架如下:
- build() {
- Stack() {
- Row() {
- Column() {
- // menu
- Row(){ ... }
- // inspiration内容
- // 配合List组件,循环渲染,可滚动内容
- List({ space: 6, scroller: this.listScroller }) {
- ForEach(this.Inspirations : this.searchInspirations,
- (item: InspirationData, index: number) => {
- ListItem() {
- InspirationItemView({
- InspiItem: item,
- ...
- })
- }
-
- if (!this.isSearchPageShow && !this.isInsert && !this.isEditMode) {
- // 添加新内容+按钮
- Image($r('app.media.add_filled'))
- // 共享到灵动看板桌面 弹窗
- Image($r('app.media.panel_icon'))
- .onClick(() => {
- this.isShowDeskPanel = true
- })
- }
- .width(this.MycurrentBreakpoint != 'sm' ? '50%' : '100%')
- // 断点来实现多端布局
- if (this.MycurrentBreakpoint != 'sm') {
- Column() {
- // 内容按颜色整理的详情页
- InspirationTotalView({
- Inspirations: this.Inspirations
- })
- }
- .width('50%')
- .height('100%')
- }
- }.width('100%').height('100%')
- }
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.
复制代码 5 条记数据管理
数据界说
应用开发与数据结构痛痒相关,在进行其他功能开发前,规划如下灵感速记的数据,主要包罗条记文本内容、记录时间、类别、完成环境等。
- export default class InspirationData {
- id: number = -1;
- inspirationName: string = '';
- updateTime: string = '';
- progressValue: number = 0;
- inspirationType:number = InspirationType.CREATE_IDEA;
- inspirationColor:string = CommonConstants.CREATE_COLOR;
- inspirationDone: boolean = false;
- inspirationAlarm: boolean = false;
- inspirationPic: string = '';
- inspirationPicIndex: number = 0;
- inspirationIcon: string ='app.media.create_idea_filled';
- constructor() {
- ...
- }
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
复制代码 数据恒久化
用户记录的条记盼望能恒久化保存,便于用户随时查找翻阅条记。
HarmonyOS NEXT中的ArkData ([方舟数据管理])为开发者提供数据存储、数据管理和数据同步能力。此中,数据存储提供通用数据恒久化能力,根据数据特点,分为用户首选项、键值型数据库和关系型数据库。灵感速记的数据较为复杂,选择[关系型数据库](RDB)进行存储。
- 起首获取一个RdbStore,此中包罗建库、建表、升降级等操纵。
- import { relationalStore } from '@kit.ArkData';
- export default class Rdb {
- private rdbStore: relationalStore.RdbStore | null = null;
- private tableName: string;
- private sqlCreateTable: string;
- private columns: Array<string>;
- constructor(tableName: string, sqlCreateTable: string, columns: Array<string>) {
- this.tableName = tableName;
- this.sqlCreateTable = sqlCreateTable;
- this.columns = columns;
- }
- getRdbStore(callback: Function = () => {
- }) {
- let context: Context = getContext(this) as Context;
- relationalStore.getRdbStore( AppStorage.get('context'), CommonConstants.STORE_CONFIG, (err, rdb) => {
- ...
- this.rdbStore = rdb;
- this.rdbStore.executeSql(this.sqlCreateTable);
- });
- }
- insertData(data: relationalStore.ValuesBucket, callback: Function = () => {
- }) {
-
- let resFlag: boolean = false;
- const valueBucket: relationalStore.ValuesBucket = data;
- if (this.rdbStore) {
- this.rdbStore.insert(this.tableName, valueBucket, (err, ret) => {
- ...
- });
- }
- }
- deleteData(predicates: relationalStore.RdbPredicates, callback: Function = () => {
- }) {
- let resFlag: boolean = false;
- if (this.rdbStore) {
- this.rdbStore.delete(predicates, (err, ret) => {
- ...
- });
- }
- }
- updateData(predicates: relationalStore.RdbPredicates, data: relationalStore.ValuesBucket, callback: Function = () => {
- }) {
-
- let resFlag: boolean = false;
- const valueBucket: relationalStore.ValuesBucket = data;
- if (this.rdbStore) {
- this.rdbStore.update(valueBucket, predicates, (err, ret) => {
- ...
- });
- }
- }
- query(predicates: relationalStore.RdbPredicates, callback: Function = () => {
- }) {
-
- if (this.rdbStore) {
- this.rdbStore.query(predicates, this.columns, (err, resultSet) => {
- ..
- });
- }
- }
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.
复制代码
- 其次创建灵感速记条记的RDB数据表,赋予数据的增编削查能力。
- import { relationalStore } from '@kit.ArkData';
- import Rdb from './RDB';
- export class InspirationTable {
- private accountTable = new Rdb(CommonConstants.INSPIRATION_TABLE.tableName, CommonConstants.INSPIRATION_TABLE.sqlCreate,
- CommonConstants.INSPIRATION_TABLE.columns);
- constructor(callback: Function = () => {
- }) {
- this.accountTable.getRdbStore(callback);
- }
- getRdbStore(callback: Function = () => {
- }) {
- this.accountTable.getRdbStore(callback);
- }
- insertData(inspiration: InspirationData, callback: Function) {
- ...
- this.accountTable.insertData(valueBucket, callback);
- }
- deleteData(inspiration: InspirationData, callback: Function) {
- ...
- this.accountTable.deleteData(predicates, callback);
- }
- updateData(inspiration: InspirationData, callback: Function) {
- ...
- this.accountTable.updateData(predicates, valueBucket, callback);
- }
- InspirationTable()
- export default new InspirationTable() ;
- function generateBucket(inspiration: InspirationData): relationalStore.ValuesBucket {
- let obj: relationalStore.ValuesBucket = {};
- obj.inspirationName = inspiration.inspirationName;
- obj.updateTime = inspiration.updateTime;
- ...
- return obj;
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.
复制代码 6 条记功能开发
展示的每条条记都是一个List元素,默认是一条高度固定的消息条。在用户单击后Item后,通过条件渲染,启用如下编辑选项:选择类别、可编辑内容、复制与华为分享、插入图片。
- 选择类别
每种条记类别都有专属的图标和颜色,根据用户的选择实时渲染整条List Item,如下所示为用户选择默认紫色灵感类别时,暂时渲染的内容做相应的渲染,当用户选择√后,再做恒久化保存。
- // idea type button
- if (this.isExpanded) {
- Row({ space: 10 }) {
- Button({ type: ButtonType.Circle, stateEffect: true }) {
- Image($r(CommonConstants.CREATE_IMG))
- .fancyImage(20)
- }.fancyButton(CommonConstants.CREATE_COLOR)
- .onClick(() => {
- this.InspiItem.inspirationColor = CommonConstants.CREATE_COLOR
- this.InspiItem.inspirationType = InspirationType.CREATE_IDEA
- this.InspiItem.inspirationIcon = CommonConstants.CREATE_IMG
- })
- ...
- }.justifyContent(FlexAlign.Center).width(this.edite_width)
- .position({ x: 0, y: -15 })
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
复制代码 如果用户选择了强提示类别,会触发关照时间设置,到点自动发送关照和震动响铃。

- if (this.InspiItem.inspirationType === InspirationType.ATTENTION_IDEA) {
- DatePickerDialog.show({
- start: this.startDate,
- end: new Date("2100-12-31"),
- selected: this.selectedDate,
- showTime: true,
- useMilitaryTime: true,
- disappearTextStyle: {
- color: CommonConstants.ATTENTION_COLOR,
- font: { size: '14fp', weight: FontWeight.Bold }
- },
- textStyle: {
- color: CommonConstants.ATTENTION_COLOR,
- font: { size: '18fp', weight: FontWeight.Normal }
- },
- selectedTextStyle: { color: '#ff182431', font: { size: '22fp', weight: FontWeight.Regular } },
- onDateAccept: (value: Date) => {
- // 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
- this.selectedDate = value
- BackgroundReminder.publishReminder(this.InspiItem, value);
- if (this.updateItem !== undefined) {
- console.info('inspitest-点击修改内容')
- this.updateItem(this.isInsert, this.InspiItem)
- this.isInsert = false
- }
- // this.isExpanded = false;
- this.animationClick(false);
- }
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.
复制代码 如果用户选择了日程类别,会触发日程时间选择,自动关联到体系日历。

- else if (this.InspiItem.inspirationType === InspirationType.TODO_IDEA) {
- CalendarPickerDialog.show({
- selected: this.selectedDate,
- onAccept: (value) => {
- CalenderMessage.publishCalenderEvent(this.InspiItem, value);
- if (this.updateItem !== undefined) {
- console.info('inspitest-点击修改内容')
- this.updateItem(this.isInsert, this.InspiItem)
- this.isInsert = false
- }
- // this.isExpanded = false;
- this.animationClick(false);
- console.info("calendar onAccept:" + JSON.stringify(value))
- }
- })
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
复制代码
- 可编辑内容
条记内容包罗最近编辑时间(Text)、可编辑条记文本(InpuText)、选择的图片(Image)。这部分功能利用根本组件即可实现。
- 复制、华为分享
- // copy
- Column() {
- Image($r('app.media.ic_public_copy')).fancyImage(25)
- }
- .width('20%')
- .onClick(() => {
- if (this.TempInspiName === '') {
- promptAction.showToast({
- message: '内容为空',
- duration: 1500
- })
- return;
- }
- let text: string = this.TempInspiName;
- let pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
- let systemPasteBoard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
- systemPasteBoard.setData(pasteData).catch((err: BusinessError) => {
- console.error(`Failed to set pastedata. Code: ${err.code}, message: ${err.message}`);
- });
- let subText = this.TempInspiName.substring(0, 4);
- if (subText.length < this.TempInspiName.length) { //只提示部分内容
- subText = subText + '...'
- }
- })
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.
复制代码
- // share
- Column() {
- Image($r('app.media.ic_public_share')).fancyImage(25)
- }.width('20%')
- .onClick(() => {
- let ShareDta: systemShare.SharedData = new systemShare.SharedData({
- utd: utd.UniformDataType.PLAIN_TEXT,
- content: this.TempInspiName
- });
- // 构建ShareController
- let ShareController: systemShare.ShareController = new systemShare.ShareController(ShareDta);
- // 获取UIAbility上下文对象
- let ShareContext: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
- // 注册分享面板关闭监听
- ShareController.on('dismiss', () => {
- console.log('Share panel closed');
- // 分享结束,可处理其他业务。
- });
- // 进行分享面板显示
- ShareController.show(ShareContext, {
- previewMode: systemShare.SharePreviewMode.DETAIL,
- selectionMode: systemShare.SelectionMode.SINGLE
- });
- })
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.
复制代码
- 插入图片
通过picker.PhotoViewPicker访问用户相机和相册,保护用户隐私,利用也快捷方便。
- //add camera or photoPicker
- Column() {
- Image($r('app.media.ic_public_camera'))
- .fancyImage(25)
- }
- .width('16%')
- .onClick(() => {
- if (this.TempInspiPicIndex < 5) {
- const photoSelectOptions = new picker.PhotoSelectOptions();
- photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
- photoSelectOptions.maxSelectNumber = 5 - this.TempInspiPicIndex;
- const photoViewPicker = new picker.PhotoViewPicker();
- //调用select()接口拉起图库界面进行文件选择,文件选择成功后,返回PhotoSelectResult结果集
- photoViewPicker.select(photoSelectOptions)
- .then(async (photoSelectResult: picker.PhotoSelectResult) => {
- //用一个全局变量存储返回的uri
- this.selectedImage = photoSelectResult.photoUris
- for (let i = 0; i < photoSelectResult.photoUris.length; i += 1) {
- this.TempInspiPic += photoSelectResult.photoUris[i] + '|';
- this.TempInspiPicIndex += 1;
- }
- console.info('photoViewPicker.select to file succeed and uris are:' + this.TempInspiPic);
- })
- .catch((err: BusinessError) => {
- console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
- })
- } else {
- promptAction.showToast({
- message: '图片超出限制',
- duration: 1500
- })
- }
- })
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.
复制代码 通过开发上述编辑选项功能,实现的内容编辑效果如下图所示:

7 动效开发
APP的开发离不开动画,HarmonyOS NEXT ArkUI中提供多种动画接口(属性动画、转场动画等),灵感速记中利用了转场动画、弹簧效果等动效,给用户舒服的视觉体验。
通过根本的组件转场接口transition与TransitionEffect的组合利用,界说出一镜到底的操纵效果。如条记内容在睁开或收起时,到场下动画修饰:
- .transition(TransitionEffect.asymmetric(
- TransitionEffect.opacity(0)
- .animation({ curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), duration: 200, delay: 150 }),
- TransitionEffect.opacity(0)
- .animation({ curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), duration: 200 }),
- ))
- 1.2.3.4.5.6.
复制代码 条记睁开、收起动画效果立竿见影:
8 共享消息与灵动看板
灵感速记的共享消息功能,是将APP记录的内容分享到生态硬件(灵动看板),在灵动开板上点击完成,APP也有相应的记录。共享消息功能将条记“用起来”,发掘更多应用场景。共享消息功能主要利用了IoT技术实现,技术思绪如下图:
在APP内置了本地html网页,用于实现设置mqtt参数以及收发数据,该过程涉及应用侧与前端页面的交互,HarmonyOS NEXT的[ArkWeb(方舟Web)]提供了能力运行前端JavaScript函数、数据双向传输等能力。
这里讲解如何实现html与ArkTS之间数据交互。
起首,在启动app时,要在Index.ets的aboutToAppear()中创建一个和H5页面通信的消息通道,实现如下:
- TryWebPort(): void {
- try {
- // 1、创建两个消息端口。
- this.ports = this.webviewController.createWebMessagePorts();
- // 2、在应用侧的消息端口(如端口1)上注册回调事件。
- this.ports[1].onMessageEvent((result: web_webview.WebMessage) => {
- let msg = 'Got msg from HTML:';
- ...
- })
- // 3、将另一个消息端口(如端口0)发送到HTML侧,由HTML侧保存并使用。
- this.webviewController.postMessage('__init_port__', [this.ports[0]], '*');
- } catch (error) {
- promptAction.showToast({duration:2000,message:'发送失败'})
- let e: business_error.BusinessError = error as business_error.BusinessError;
- console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
- }
- }
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.
复制代码 其次,需要在本地src/main/resources/rawfile/index.html 中创建一个用于接收的监听端口,具体实现如下:
- // 页面
- var h5Port;
- var output = document.querySelector('.output');
- window.addEventListener('message', function (event) {
- if (event.data === '__init_port__') {
- if (event.ports[0] !== null) {
- h5Port = event.ports[0]; // 1. 保存从ets侧发送过来的端口
- h5Port.onmessage = function (event) {
- // 2. 接收ets侧发送过来的消息.
- var msg = 'Got message from ets:';
- var result = event.data;
- if (typeof(result) === 'string') {
- console.info(`received string message from html5, string is: ${result}`);
- msg = result;
- } else if (typeof(result) === 'object') {
- if (result instanceof ArrayBuffer) {
- console.info(`received arraybuffer from html5, length is: ${result.byteLength}`);
- msg = msg + 'lenght is ' + result.byteLength;
- } else {
- console.info('not support');
- }
- } else {
- console.info('not support');
- }
- // this.PositionName = msg.toString();
-
- }
- }
- }
- })
- 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.
复制代码 本地的H5可以通过与ets建立的消息通道,直接发送数据到用户页面,这个通道也可以用来接收H5发送回来的数据.
- // 使用h5Port往ets侧发送消息.
- function PostMsgToEts(data) {
- console.info('H5 to Ets data:'+data);
- if (h5Port) {
- h5Port.postMessage(data);
- } else {
- console.error('h5Port is null, Please initialize first');
- }
- }
- // 调用接口发送数据到ets用户页面,便于存储和展示
- this.PostMsgToEts(jsonObjTaskNumber.toString()+jsonObjTaskChecked.toString());
- 1.2.3.4.5.6.7.8.9.10.11.
复制代码
在最后
如果你觉得这篇内容对你还蛮有资助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注小编,同时可以等待后续文章ing |