作为一个文艺青年,偶尔会有想要表达一下自己心情的需求,这时候鸿蒙系统的桌面卡片就非常合适。
简单设想一下,这是一个常驻桌面、能显示个性文案的元服务。如果用户对文案内容不满意的话可以手动切换文案。由于每次只能单条刷新很不方便,因此还需要一个应用界面,里面能够分类显示很多不同的文案,便于快速选择,只要手动点击某一条文案,就能直接显示到桌面卡片上。
使用端云一体化开发可以很容易的实现这个效果
效果展示:
每日走心文案阶段性功能演示
简单介绍一下端云一体化开发:
作为一个独立开发者,由于时间和开发能力的限制,想要独立开发一个应用程序其实还是挺难的,微博、B站那种功能复杂又高大上的应用自然不必说,很多时候连简单的应用也会受限于前后端开发的差异,学前端的不会写后端、不会配置服务器,学后端的搞不懂前端三件套,让人搞得一头雾水,项目未半而中道崩卒。
DevEco的端云一体化开发工具能让应用开发变得非常简单,前后端语法统一,前端用基于JS衍生的ArkTS,后端能直接用Node.js,语法高度相似。
severless不需要手动安装各种服务器软件,也不需要进行大量复杂难懂的系统配置。直接新建项目就能让客户端自动连接上severless,并且服务器常用的登录、后端环境、数据库、文件存储系统、日志系统他都有。突出一个简单快捷,傻瓜式操作。
文案可以统一放在云数据库,使用云数据库的接口来查询。
桌面卡片的刷新事件主要靠postCardAction接口的message事件来实现,
点击跳转应用靠postCardAction接口的router事件来实现
FormAbility和应用页面点击后刷新卡片的功能依靠formProvider.updateForm接口来实现。
为了能管理多张应用卡片,避免出现明明没有卡片但应用提示刷新成功,已经明明有应用卡片但应用找不到对应的FormID导致卡片不刷新的情况,又需要一个能记录所有FormID的函数。
关于postCardAction接口和formProvider.updateForm接口可以参考以下文章:
鸿蒙元服务开发教程:从底层原理开始讲透桌面卡片的call事件刷新机制
鸿蒙元服务开发教程02:从底层原理开始讲透桌面卡片的message事件刷新机制
鸿蒙元服务开发教程03:从底层原理开始讲透桌面卡片的router事件刷新机制
第一步:在AGC中新建项目,新建应用。
如果不会创建端云一体化工程的话可以参考一下这篇文章:【纯新手向】手把手带你使用模板创建第一个端云一体化元服务
第二步:按照项目需求开通对应的服务。
配置完成后新建本地工程。(强烈建议新手先完成severless的各种配置再新建本地项目,因为建立本地项目时会自动下载与severless配置情况对应的agconnect-services.json与schema.json文件。改动severless配置后需要手动更新这两个文件,否则可能会无法运行。)
开发桌面卡片的代码
let storage=new LocalStorage()//页面级存储,靠它才能卡片内容的跨文件刷新
@Entry(storage)
@Component
struct WidgetCard {
@LocalStorageProp('formId') @Watch('sentFormID') formId: string = '0';//保存FormID的变量
@LocalStorageProp('slogan') slogan:string='我希望有个如你一般的人。如山间清爽的风,如古城温暖的光,由清晨到傍晚,由山野到书房,只要最后是你,就好。'//卡片的默认文案
sentFormID(){
console.log('发送formID')
postCardAction(this,{//call事件,可以后台启动UIAbility并执行预设的事件。
'action':'call',
"abilityName": 'EntryAbility',
'params': {
'method': 'createFormId',
'formId': this.formId,
'detail': ''
}
})
}
build() {
Stack() {
Image($r("app.media.background1"))
.width('100%')
.height('100%')
Column() {
Row(){
Image($r("app.media.logo_white"))
.objectFit(ImageFit.Contain)
.width('30vp')
Text('每日走心文案')
.fontColor(Color.White)
}
.height('20%')
.width('100%')
.justifyContent(FlexAlign.Start)
Row(){
Text(this.slogan)
.fontColor(Color.White)
.width('80%')
Column(){
Image($r('app.media.freshButton'))
.height('35%')
.objectFit(ImageFit.Contain)
.padding(5)
}
.onClick(()=>{
postCardAction(this,{//message事件,用于启动FormAbility并执行预设的事件。
'action':'message',
'params': {
'msgTest': 'messageEvent'
}
})
})
.padding(5)
.margin(5)
.backgroundColor(Color.Gray)
.borderRadius(50)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.height('80%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.padding($r('app.float.column_padding'))
}
.width('100%')
.height('100%')
.onClick(() => {
postCardAction(this, {//router事件,可以跳转到UIAbiliity,同时也可以执行UIAbiliity中预设的事件
"action": "router",
"abilityName": 'EntryAbility',
"params": {
"message": ''
}
});
})
}
}
import formInfo from '@ohos.app.form.formInfo';
import formBindingData from '@ohos.app.form.formBindingData';
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
import formProvider from '@ohos.app.form.formProvider';
import agconnect from '@hw-agconnect/api-ohos';
import { AGConnectCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/database-ohos';
import {dailySlogan} from'../models/dailySlogan'
export default class EntryFormAbility extends FormExtensionAbility {
objectTypeInfo=null
cloudZone:CloudDBZone=null
cloud:AGConnectCloudDB=null
onAddForm(want) {//want中包含了FormId等信息,在卡片创建时读取并更新,以便卡片执行call时使用
let formId = want.parameters["ohos.extra.param.key.form_identity"];
let dataObj1 = {
"formId": formId
};
let obj1 = formBindingData.createFormBindingData(dataObj1);
console.log('formId向卡片发送'+formId)
return obj1;
}
async onFormEvent(formId, message) {//当message事件触发时,查询数据库并刷新卡片
// Called when a specified message event defined by the form provider is triggered.
try {
agconnect.instance().init(this.context)//初始化agc
}
catch (error){}
try {
await AGConnectCloudDB.initialize(this.context)//初始化AGConnectCloudDB
}
catch (error){}
if (this.cloud == null) {
this.cloud = await AGConnectCloudDB.getInstance()//获取实例
}
if (this.objectTypeInfo == undefined) {//查询并构建云数据库的数据类型
const value = await this.context.resourceManager.getRawFile('schema.json');
let json = "";
for (var i = 0; i < value.length; i++) {
json += String.fromCharCode(value[i]);
}
this.objectTypeInfo = JSON.parse(json);
this.cloud.createObjectType(this.objectTypeInfo);
}
if (this.cloudZone == null) {
this.cloudZone = await this.cloud.openCloudDBZone(new CloudDBZoneConfig("sloganZone"))//打开存储区
}
const sql = CloudDBZoneQuery.where(dailySlogan)
.limit(1, Math.floor(Math.random() * 9)) //用于查询的sql语句
this.cloudZone.executeQuery(sql).then((sloganInfo) => {
let info:dailySlogan[]=sloganInfo.getSnapshotObjects()//处理查询结果
let slogan=info[0].Slogan
let formData = {
'slogan': slogan, // 此处需要和卡片页面的变量相对应
};
let formInfo = formBindingData.createFormBindingData(formData)
formProvider.updateForm(formId, formInfo).then((data) => {//刷新卡片
console.info('FormAbility updateForm success.' + JSON.stringify(data));
}).catch((error) => {
console.error('FormAbility updateForm failed: ' + JSON.stringify(error));
})
}).catch((error) => {
console.error('数据查询失败: ' + JSON.stringify(error));
})
}
onRemoveForm(formId) {
// Called to notify the form provider that a specified form has been destroyed.
}
onAcquireFormState(want) {
// Called to return a {@link FormState} object.
return formInfo.FormState.READY;
}
};
import agconnect from '@hw-agconnect/api-ohos';
import { AGConnectCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/database-ohos';
import {dailySlogan} from'../models/dailySlogan'
import formBindingData from '@ohos.app.form.formBindingData';
import formProvider from '@ohos.app.form.formProvider';
import formInfo from '@ohos.app.form.formInfo';
@Entry
@Component
struct homepage{
@State sloganList:Array<string>=[]//保存文案列表的变量
objectTypeInfo=null
cloudZone:CloudDBZone=null
cloud:AGConnectCloudDB=null
aboutToAppear(){
this.fresh()
}
async fresh(){//用于查询新文案的代码,与FormAbility中的基本一致
try {
try {
agconnect.instance().init(getContext(this))
}
catch (error){}
try {
await AGConnectCloudDB.initialize(getContext(this))
}
catch (error){}
if (this.cloud == null) {
this.cloud = await AGConnectCloudDB.getInstance()
}
if (this.objectTypeInfo == undefined) {
const value = await getContext(this).resourceManager.getRawFile('schema.json');
let json = "";
for (var i = 0; i < value.length; i++) {
json += String.fromCharCode(value[i]);
}
this.objectTypeInfo = JSON.parse(json);
this.cloud.createObjectType(this.objectTypeInfo);
}
if (this.cloudZone == null) {
this.cloudZone = await this.cloud.openCloudDBZone(new CloudDBZoneConfig("sloganZone"))
}
const sql = CloudDBZoneQuery.where(dailySlogan)
.limit(4, Math.floor(Math.random() * 9))
this.cloudZone.executeQuery(sql).then((sloganInfo) => {
let info: dailySlogan[] = sloganInfo.getSnapshotObjects()
this.sloganList = []
for (var i = 0;i < info.length; i++) {
this.sloganList.push(info[i].Slogan)
}
}).catch((error) => {
console.error('数据查询失败: ' + JSON.stringify(error));
})
}
catch (error){
console.error('刷新异常: ' + JSON.stringify(error));
}
}
build(){
List(){
ListItem(){
Button('刷新文案')
.onClick(()=>{
this.fresh()
})
}
ForEach(this.sloganList,(listInfo)=>{
ListItem(){
Column(){
Text(listInfo)
}
.width('90%')
.height('20%')
.backgroundImage($r('app.media.background1'))
.margin({top:5,bottom:5})
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(async()=>{//判断是否存在卡片并根据用户点击的文案来刷新这些卡片
if(AppStorage.Has('formId')){
let formIdList:Array<string>=AppStorage.Get('formId')
let formData = {
"slogan": listInfo
};
let formMsg = formBindingData.createFormBindingData(formData)
console.log('开始刷新')
for(let i=0;i<formIdList.length;i++){
try {
await formProvider.updateForm(formIdList[i], formMsg)
}
catch (error){
if(error.code==16501001){//formId都不存在的错误码
console.log('formID是'+formIdList[i])
formIdList[i]=' '
}
}
}
let newFormIdList=[]
for(var i=0;i<formIdList.length;i++){
if(formIdList[i]!=' '){
console.log('检测成功的formID是'+formIdList[i])
newFormIdList.push(formIdList[i])
}else{
console.log(i+'id值已清空'+formIdList[i])
}
}
if(newFormIdList.length==0){
console.log('卡片已被清空')
AppStorage.Delete('formId')
AlertDialog.show({
message:'卡片不存在,请重新添加卡片'
})
}
else{
AppStorage.SetOrCreate('formId',newFormIdList)
AlertDialog.show({
message:'卡片刷新已完成'
})
}
}else {
AlertDialog.show({
message:'请先添加卡片'
})
}
})
}
},)
}
.width('100%')
.height('100%')
.alignListItem(ListItemAlign.Center)
}
}
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import formInfo from '@ohos.app.form.formInfo';
import formBindingData from '@ohos.app.form.formBindingData';
import formProvider from '@ohos.app.form.formProvider';
function saveFormId(data){//保存新增的FormId的函数
console.log('监听事件已激活')
let FormIdList=[]
if(AppStorage.Has('formId')){
FormIdList=AppStorage.Get('formId')
}
let params = JSON.parse(data.readString())
if (params.formId !== undefined) {
let FormId = params.formId;
console.log('读取到formId')
FormIdList.push(FormId)
AppStorage.SetOrCreate('formId', FormIdList)
for(var i=0;i<FormIdList.length;i++){
console.log('formID保存成功')
console.log(FormIdList[i])
}
}
else {
console.log('formId读取失败')
}
return null
}
export default class EntryAbility extends UIAbility {
onCreate() {
this.callee.on('createFormId',saveFormId);//应用启动时监听call事件
let AtManager = abilityAccessCtrl.createAtManager();
AtManager.requestPermissionsFromUser(this.context, ['ohos.permission.READ_MEDIA', 'ohos.permission.MEDIA_LOCATION']).then((data) => {
hilog.info(0x0000, 'testTag', '%{public}s', 'request permissions from user success' + data);
}).catch((err) => {
hilog.error(0x0000, 'testTag', 'Failed to request permissions from user. Cause: %{public}s', JSON.stringify(err) ?? '');
});
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
this.callee.off('createFormId');//应用销毁时解除监听call事件
}
}