OpenHarmony最近一段时间,简直火的一塌糊度,学习OpenHarmony相关的技术栈也有一段时间了,做个记账本小应用,将所学知识点融合记录一下。
1、记账本涉及知识点基础组件(Button、Select、Text、Span、Divider、Image)、容器(Row、Flex、List、Grid、Column)、定位(position)、路由(router)、Toast(promptAction)、Web组件;
自定义弹窗(@CustomDialog);
分布式键值数据库(@ohos.data.distributedKVStore);
Web组件;
OpenHarmony三方库;
2、效果预览
3、功能点实现简介 3.1 自定义弹窗 // 自定义弹窗定义 @CustomDialog struct CustomDialogSetting { // 双向绑定传值 @Link settingBudgetValue: string // 弹窗控制器,控制打开/关闭,必须传入,且名称必须为:controller controller: CustomDialogController // 弹窗中的按钮事件 cancel: () => void confirm: () => void // 弹窗中的内容描述 build() { Column() { Text($r('app.string.budget_setting')) .fontSize(18) .fontWeight(FontWeight.Bold) .margin(12) .textAlign(TextAlign.Center) .width("100%") TextInput({ placeholder: $r('app.string.estimated_amount_tips'), text: this.settingBudgetValue }) .type(InputType.Number) .height(60) .width('90%') .onChange((value: string) => { this.settingBudgetValue = value }) Flex({ justifyContent: FlexAlign.SpaceAround }) { Button($r('app.string.cancel')) .onClick(() => { this.settingBudgetValue = '' this.controller.close() this.cancel() }).backgroundColor(0xffffff).fontColor(Color.Black) Button($r('app.string.confirm')) .onClick(() => { this.controller.close() this.confirm() }).backgroundColor(0xffffff).fontColor(AccountBookConstant.FONT_COLOR_BLUE) }.margin(15) } } } // 使用自定义弹窗 dialogController: CustomDialogController = new CustomDialogController({ builder: CustomDialogSetting({ cancel: this.onCancel.bind(this), confirm: this.onAccept.bind(this), settingBudgetValue: $settingBudgetValue }), cancel: this.onCancel, autoCancel: true, alignment: DialogAlignment.Center, gridCount: 4, customStyle: false }) // 开启弹窗 this.dialogController.open() 3.2 悬浮按钮 // 设置按钮, 通过position进行绝对定位 Button({ stateEffect: true }){ Image($rawfile('setting.svg')).width(22).height(22) }.width(42).height(42) .borderRadius(90) .shadow({ radius: 10, color: Color.Gray, offsetX: 5, offsetY:5 }) .position({ x: '98%', y: '98%' }) .markAnchor({ x: '98%', y: '98%'}) .margin(10).backgroundColor('#67C23A') .onClick(() => { if (this.dialogController != undefined) { this.dialogController.open() } }) 3.3 数据存储 // 定义键值对存储类 import distributedKVStore from '@ohos.data.distributedKVStore'; const BUNDLE_NAME = "baseInfo" let context = getContext(this) // 数据库对象 let kvManager: distributedKVStore.KVManager | undefined = undefined; // KVStore数据库 let kvStore: distributedKVStore.SingleKVStore | undefined = undefined; class DistributedUtil { constructor() { this.createKeyValueDB(); } async getKvManager(bundleName?: string) { const kvStoreConfig: distributedKVStore.KVManagerConfig = { context: context, bundleName: bundleName || BUNDLE_NAME }; try { kvManager = distributedKVStore.createKVManager(kvStoreConfig); } catch (err) { console.error(`error:${err}`) } } // 创建并得到指定类型的KVStore数据库 async createKeyValueDB(op?: distributedKVStore.Options) { if (!kvManager) { await this.getKvManager(); } try { const options: distributedKVStore.Options = { // 当数据库文件不存在时是否创建数据库,默认为true createIfMissing: true, // 设置数据库文件是否加密,默认为false,即不加密 encrypt: false, // 设置数据库文件是否备份,默认为true,即备份 backup: false, // 设置数据库文件是否自动同步。默认为false,即手动同步 autoSync: true, // kvStoreType不填时,默认创建多设备协同数据库 kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION, // 多设备协同数据库:kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION, securityLevel: distributedKVStore.SecurityLevel.S1 }; kvManager.getKVStore<distributedKVStore.SingleKVStore>('storeId', op || options, (err, store: distributedKVStore.SingleKVStore) => { if (err) { console.error(`Failed to get KVStore: Code:${err.code},message:${err.message}`); return; } console.info('Succeeded in getting KVStore.'); kvStore = store; }); } catch (e) { console.error(`An unexpected error occurred. Code:${e.code},message:${e.message}`); } return kvStore; } // 删除指定键值的数据 async deleteStoreData(key: string) { if (!kvStore) { return; } try { kvStore.delete(key, (err) => { if (err !== undefined) { console.error(`Failed to delete data. Code:${err.code},message:${err.message}`); return; } console.info('Succeeded in deleting data.'); }); } catch (e) { console.error(`An unexpected error occurred. Code:${e.code},message:${e.message}`); } } // 向键值数据库中插入数据 async putStoreData(key: string, value: any) { if (!key || !value) { return } if(!kvStore) { kvStore = await this.createKeyValueDB(); } try { kvStore.put(key, value, (err) => { if (err !== undefined) { console.error(`Failed to put data. Code:${err.code},message:${err.message}`); return; } console.info('Succeeded in putting data.'); }); } catch (e) { console.error(`putStoreData===>An unexpected error occurred. Code:${e.code},message:${e.message}`); } } // 获取指定键的值 async getStoreData(key: string) { if (!key) { return } if(!kvStore) { kvStore = await this.createKeyValueDB(); } return new Promise((resolve, reject) => { try { kvStore.get(key, (err, data) => { if (err != undefined) { console.error(`Failed to get data. Code:${err.code},message:${err.message}`); reject(err) return; } resolve(data) }); } catch (err) { reject(err) console.error('TAG-getStoreData', `Failed to get value, Cause: ${err}`) } }); } } export default new DistributedUtil(); // 使用键值对存储 import distributedUtil from '../../common/distributedStrong' // 1、增加 distributedUtil.putStoreData('amountRecords', JSON.stringify(dataArray)) // 2、 获取 distributedUtil.getStoreData('amountRecords').then((res: string) => { if(res) { const result = JSON.parse(res) // 处理存储的图片资源,会自动转换为id的形式,无法直接获取展示 result.map(item => { item.icon = $rawfile(item.icon.params[0]) return item }) this.recordsArray = result } }) 3.4 统计图表 3.4.1 定义本地html文件
在resources下创建rawfile文件夹,增加chart.html文件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>柱状图示例</title> <script src="./js/echarts5.4.0.js"></script> </head> <body> <h1>本月支出</h1> <div id="chartBarContainer" style="width: 400px; height: 300px;"></div> <div id="chartPieContainer" style="width: 400px; height: 300px;"></div> </body> <script> function initBarChart(chartData) { const data = JSON.parse(chartData); var chartContainer = document.getElementById('chartBarContainer'); var chart = echarts.init(chartContainer); var option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, xAxis: { type: 'category', data: data.xAxisData }, yAxis: { type: 'value' }, series: [{ data: data.seriesData, type: 'bar', showBackground: true, stack: 'Total', label: { show: true, position: 'top' }, emphasis: { itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#2378f7' }, { offset: 0.7, color: '#2378f7' }, { offset: 1, color: '#83bff6' } ]) } }, itemStyle: { borderRadius: [25, 25, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#83bff6' }, { offset: 0.5, color: '#188df0' }, { offset: 1, color: '#188df0' } ]) } }] }; chart.setOption(option); } function initPieChart(chartData) { const data = JSON.parse(chartData); var chartContainer = document.getElementById('chartPieContainer'); var chart = echarts.init(chartContainer); var option = { tooltip: { trigger: 'item' }, legend: { show: false, top: '5%', left: 'center' }, series: [{ data: data.seriesData, type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, startAngle: 180, labelLine: { show: true, length: 20, // 标签线的长度 length2: 50 // 标签线的第二段长度 }, emphasis: { label: { show: true, fontSize: 40, fontWeight: 'bold' } }, labelLine: { show: false } }] }; chart.setOption(option); } </script> </html> 3.4.2 Web组件使用本地文件 import web_webview from '@ohos.web.webview' @Entry @Component struct Chart { controllerWeb: web_webview.WebviewController = new web_webview.WebviewController() build() { Web({ src: $rawfile('barChart.html'), controller: this.controllerWeb }) } } 3.4.3 Web组件向H5页面传值,调用H5中的方法 // 初始化柱状图 const codeJS = ` initBarChart('${JSON.stringify(this.chartBarData)}') ` this.controllerWeb.runJavaScript(codeJS) 3.4.4 完整调用代码 import web_webview from '@ohos.web.webview' import router from '@ohos.router'; interface ChartDataType { xAxisData?: Array<string | number>; seriesData?: Array<string | number | any>; } @Entry @Component struct BarChart { controllerWeb: web_webview.WebviewController = new web_webview.WebviewController() private chartBarData: ChartDataType = { xAxisData: ['餐饮', '购物', '教育', '生活', '宠物', '运动', '娱乐', '其他'], seriesData: [10, 20, 15, 30, 10, 20, 15, 30], } private chartPieData: ChartDataType = { seriesData: [ { value: 10, name: '餐饮' }, { value: 20, name: '购物' }, { value: 15, name: '教育' }, { value: 30, name: '生活' }, { value: 10, name: '宠物' }, { value: 20, name: '运动' }, { value: 15, name: '娱乐' }, { value: 30, name: '其他' }, ], } build() { Column() { Row() { Button() { Image($rawfile('icon_back.png')).width(18) } .backgroundColor(Color.Transparent) .padding(10) .onClick(() => router.back()) Text('图表分析').fontSize(20).fontWeight(FontWeight.Bold) } .padding(10) .justifyContent(FlexAlign.SpaceBetween) .width('100%') Web({ src: $rawfile('barChart.html'), controller: this.controllerWeb }) .verticalScrollBarAccess(true) .javaScriptAccess(true) .onPageEnd(() => { // 初始化柱状图 const codeJS = ` initBarChart('${JSON.stringify(this.chartBarData)}') ` this.controllerWeb.runJavaScript(codeJS) // 初始化饼图 const codeJSPie = ` initPieChart('${JSON.stringify(this.chartPieData)}') ` this.controllerWeb.runJavaScript(codeJSPie) }) }.width('100%').height('100%') } } 3.4.5 传值注意点总结传递数据需要通过 JSON.stringify() 转换为字符串;
传递的参数必须使用引号包裹,否则无法调用到H5中的方法;
H5中使用传过来的数据,同理,需要使用 JSON.parse() 进行转换;
3.5 自定义键盘使用Grid布局,通过rowStart、rowEnd、columnStart、columnEnd进行单元格合并。或者使用column和row布局,循环即可。
参考:https://gitee.com/harmonyos/codelabs/tree/master/SimpleCalculator
Grid() { GridItem() { this.GridItemButtonBuilder('7') }.gridItemStyle().onClick(() => { this.clickBtn(7) }) GridItem() { this.GridItemButtonBuilder('8') }.gridItemStyle().onClick(() => { this.clickBtn(8) }) GridItem() { this.GridItemButtonBuilder('9') }.gridItemStyle().onClick(() => { this.clickBtn(9) }) GridItem() { Text(this.time).backgroundColor(Color.White).width('100%').height('100%').textAlign(TextAlign.Center) }.gridItemStyle() GridItem() { this.GridItemButtonBuilder('4') }.gridItemStyle().onClick(() => { this.clickBtn(4) }) GridItem() { this.GridItemButtonBuilder('5') }.gridItemStyle().onClick(() => { this.clickBtn(5) }) GridItem() { this.GridItemButtonBuilder('6') }.gridItemStyle().onClick(() => { this.clickBtn(6) }) GridItem() { this.GridItemButtonBuilder('remove') }.gridItemStyle().onClick(() => { this.clickBtn('remove') }) GridItem() { this.GridItemButtonBuilder('1') }.gridItemStyle().onClick(() => { this.clickBtn('1') }) GridItem() { this.GridItemButtonBuilder('2') }.gridItemStyle().onClick(() => { this.clickBtn('2') }) GridItem() { this.GridItemButtonBuilder('3') }.gridItemStyle().onClick(() => { this.clickBtn('3') }) GridItem() { this.GridItemButtonBuilder('保存', '#409EFF') }.rowStart(2).rowEnd(3).columnStart(3).columnEnd(4).onClick(() => { this.clickBtn('save') }) GridItem() { this.GridItemButtonBuilder('清空') }.gridItemStyle().onClick(() => { this.clickBtn('clear') }) GridItem() { this.GridItemButtonBuilder('0') }.gridItemStyle().onClick(() => { this.clickBtn('0') }) GridItem() { this.GridItemButtonBuilder('.') }.gridItemStyle().onClick(() => { this.clickBtn('.') }) } .height(220) .columnsTemplate('1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr 1fr 1fr') .rowsGap(0) .margin({ top: 0 }) 4、扩展(如何使用基础组件自定义柱状图)使用Stack容器进行堆叠
通过Rect绘制柱子
通过Divider绘制分割线
一个简单的柱状图就完成了,具体可以参考健康生活,希望ArkUI可以早日集成Echarts类似的图表组件,JS版本的有Chart组件,ArkTs的还未集成,期待官方❤️❤️❤️
@Builder content(item: OneMealStatisticsInfo) { Column() { if (item.totalFat > 0) { Rect({ width: 14, height: item.totalFat / 200 + 14, radius: 7 }).fill('#FD9A42') .padding({ top: 14 }) .margin({ bottom: -28 }) } if (item.totalProtein > 0) { Rect({ width: 14, height: item.totalProtein / 200 + 14, radius: 7 }) .fill('#FBD44E') .padding({ top: 14 }) .margin({ bottom: -21 }) } if (item.totalCarbohydrates > 0) { Rect({ width: 14, height: item.totalCarbohydrates / 200 + 14, radius: 7 }).fill('#73CD57') .padding({ top: 7 }) .margin({ bottom: -7 }) } }.clip(true) } @Builder legendComponent(item: HistogramLegend) { Text(item.value).fontSize(12).fontColor('#18181A').fontFamily('HarmonyHeTi') } @Component struct Histogram { @Consume("dietData") dietData: Array<OneMealStatisticsInfo> @BuilderParam content?: (item: OneMealStatisticsInfo) => void @BuilderParam legendComponent?: (item: HistogramLegend) => void private title: string | Resource = '' private legend: HistogramLegend[] = [] build() { Column() { Text(this.title) .textAlign(TextAlign.Start) .fontSize(24) .fontColor('#000000') .fontFamily('HarmonyHeTi-Medium') .width('100%') .height(46) Stack({ alignContent: Alignment.Bottom }) { Column() { ForEach([0, 0, 0, 0, 0, 0], () => { Divider() .strokeWidth(1) .color('#D8D8D8') }) } .height('100%') .margin({ top: 20 }) .justifyContent(FlexAlign.SpaceBetween) Column() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceEvenly, alignItems: ItemAlign.Start }) { ForEach(this.dietData, (item: OneMealStatisticsInfo) => { if (item.mealFoods.length > 1) { Column() { if (this.content !== undefined) { this.content(item) } Text(item.mealTime.name).fontSize(14) .fontColor('#7E7E7E') .fontFamily('HarmonyHeTi') .margin({ top: 10 }) } .justifyContent(FlexAlign.End) .height('100%') } }) } } .height(236) } .height(190) Row() { ForEach(this.legend, (item: HistogramLegend) => { Row() { Rect({ width: 9, height: 9, radius: 9 }).fill(item.color).margin({ right: 18 }) if (this.legendComponent !== undefined) { this.legendComponent(item) } } }) } .justifyContent(FlexAlign.SpaceEvenly) .width('100%') .margin({ top: 70 }) } .height('100%') .padding({ left: 32, right: 32 }) .borderRadius(12) .backgroundColor('#FFFFFF') } }后面计划基于canvas基础组件实现一个柱状图,不断学习,期望鸿蒙不断强大完善。