元服务《Days Matter》构建实例 原创
1.元服务《Days Matter》功能介绍
Days Matter是一款展示某些重大日期下的事件的软件。主要功能包括:
(1)根据日期或者其他关键词搜索
(2)日历插件的使用
(3)文章收藏以及收藏列表展示
(4)跳转动画的使用
2.三方库使用
axios网络请求框架的使用
工程目录下oh-package.json5,配置三方库
"devDependencies": {
"@ohos/hypium": "1.0.19",
"@ohos/hamock": "1.0.0",
"@ohos/axios": "^2.1.1"
}
3.封装网络请求框架
为了更便捷快速的进行网络请求,我们将post/get请求进行封装
import axios, { AxiosError, AxiosHeaders, AxiosResponse } from '@ohos/axios';
/**
* 封装网络请求框架
*/
export class HttpUtil {
static get<T, D>(url: string, parms?: T, resp?: (res: D) => void, error?: (err: Error) => void,) {
httpRequest(url, "get", parms, resp, error)
}
static post<T, D>(url: string, parms?: T, resp?: (res: D) => void, error?: (err: Error) => void,) {
httpRequest(url, "post", parms, resp, error)
}
}
function httpRequest<T, D>(url: string, method: string, params?: T, resp?: (res: D) => void,
error?: (err: Error) => void) {
if ("get" === method) {
axios.get<T, AxiosResponse<D>, null>(url, {
params: params,
headers: getHeader()
})
.then((res: AxiosResponse<D>) => {
resp!(res.data)
})
.catch((error: AxiosError) => {
})
} else if ("post" === method) {
axios.post<T, AxiosResponse<D>>(url, params, {
headers: getHeader()
}).then((res: AxiosResponse<D>) => {
console.log("===============res:" + JSON.stringify(res.data))
resp!(res.data)
}).catch((err: AxiosError) => {
console.log("===============err:" + JSON.stringify(err))
})
}
}
//配置header
function getHeader(ignoreToken?: boolean): AxiosHeaders {
if (ignoreToken) {
return new AxiosHeaders({
'Content-Type': ContentType.JSON
})
} else {
return new AxiosHeaders({
'Content-Type': ContentType.JSON
})
}
}
export const enum ContentType {
JSON = 'application/json',
FORM = 'multipart/form-data'
}
4.页面设计
如图我们的底部导航栏为“首页”与“我的”
首页包含日期的筛选,关键词的搜索,与结果列表的展示。
我们使用DatePickerDialog来作为日期选择器,Search组件作为搜索组件,结果使用List组件进行展示。Navigation路由导航的根视图容器。
5.代码实例。
首先我们在@Entry修饰的主页面加入Navigation路由导航容器。底部导航栏由Tabs实现
Index.ets
import { authentication } from '@kit.AccountKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { DetailView } from '../DetailView';
import { MainView } from '../MainView';
import { MeView } from '../MeView';
@Entry
@Component
struct Index {
@State currentIndex: number = 0
//路由栈
@Provide('pageStack') pageStack: NavPathStack = new NavPathStack();
@Builder
PageMap(name: string, param: ESObject) {
//根据参数判断跳转哪个目标页面
NavDestination() {
if ("detail" === name) {
DetailView(param)
}
}
.mode(NavDestinationMode.STANDARD)
}
build() {
//Navigation创建时传入路由栈,用于管理每个页面的出栈、入栈
Navigation(this.pageStack) {
Column() {
Tabs() {
TabContent() {
//首页
MainView()
}
.tabBar(this.tabBuilder("首页", 0, $r("app.media.tab_main2"), $r("app.media.tab_main")))
TabContent() {
//我的
MeView()
}
.tabBar(this.tabBuilder("我的", 1, $r("app.media.tab_me2"), $r("app.media.tab_me")))
}.barPosition(BarPosition.End)
.onChange((index: number) => {
this.currentIndex = index
})
.onTabBarClick((index) => {
this.currentIndex = index
})
}
}
.navDestination(this.PageMap)
.mode(NavigationMode.Stack)
.hideTitleBar(true)
.height('100%')
.width('100%')
}
/**
* 自定义导航栏的样式
* @param title
* @param targetIndex 当前item的索引
* @param selectedImg 选中时的图片
* @param normalImg 未选中时的图片
*/
@Builder
tabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1396DB' : '#8B8B8B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
}
}
MainView.ets
import { HistoryInfo } from "./HistoryInfo"
import { HttpUtil } from "./HttpUtil"
import { promptAction, router } from "@kit.ArkUI"
/**
* 首页内容
*/
@Component
export struct MainView {
@State list: Array<HistoryInfo> = []
private selectedDate: Date = new Date('2024-04-23')
@Consume('pageStack') pageStack: NavPathStack //路由栈
//网络搜索
search(keyword: string) {
class P {
page: number = 1
pageSize: number = 10
keyword:string = ""
}
let p = new P()
p.keyword = keyword
HttpUtil.get("http://xxxx",p, (list: Array<HistoryInfo>) => {
this.list = list
})
aboutToAppear(): void {
this.search("")
}
build() {
Column() {
this.searchBuilder()
this.ListViewBuilder()
}.layoutWeight(1)
.height("100%")
.padding({ left: 10, right: 10, top: 60 })
.justifyContent(FlexAlign.Start)
}
//搜索
@Builder
searchBuilder() {
Row() {
Button("日期", { type: ButtonType.Normal })
.fontColor(Color.White)
.borderRadius(8)
.backgroundColor(Color.Orange)
.onClick(() => {
DatePickerDialog.show({
start: new Date("2000-1-1"),
end: new Date("2100-12-31"),
selected: this.selectedDate,
onDateAccept: (value: Date) => {
promptAction.showToast({message:value.getFullYear()+""})
}
})
})
Search({ placeholder: "请输入关键词" })
.layoutWeight(1)
.searchButton("查找")
.margin({ left: 10 })
.onSubmit((value: string) => {
})
}.width("100%")
}
@Builder
ListViewBuilder() {
List() {
ForEach(this.list, (e: HistoryInfo, index: number) => {
ListItem() {
this.itemBuilder(e)
}
})
}
}
@Builder
itemBuilder(historyInfo: HistoryInfo) {
Column() {
Text(historyInfo.date + "-" + historyInfo.title)
.padding(15)
Divider()
}.width("100%")
.alignItems(HorizontalAlign.Start)
.onClick(() => {
this.pageStack.pushPath({ name: "detail", param: historyInfo })
})
}
}
MeView.ets
import { HistoryInfo } from "./HistoryInfo"
import { HttpUtil } from "./HttpUtil"
@Component
export struct MeView {
@State list: Array<HistoryInfo> = []
@Consume('pageStack') pageStack: NavPathStack //路由栈
aboutToAppear(): void {
class P {
page: number = 1
pageSize: number = 10
}
HttpUtil.get("https:xxxxxxx", new P(), (histories: HistoryInfo[]) => {
this.list = histories
})
}
build() {
Column() {
Text("已收藏文章")
.width("100%")
.fontColor(Color.Orange)
.textAlign(TextAlign.Center)
.margin({ top: 30 })
.fontWeight(FontWeight.Bold)
List() {
ForEach(this.list, (e: HistoryInfo, index: number) => {
ListItem() {
this.itemBuilder(e)
}
})
}
}
.width("100%")
.height("100%")
}
@Builder
itemBuilder(historyInfo: HistoryInfo) {
Column() {
Text(historyInfo.date + "-" + historyInfo.title)
.padding(15)
Divider()
}.width("100%")
.alignItems(HorizontalAlign.Start)
.onClick(() => {
this.pageStack.pushPath({ name: "detail", param: historyInfo })
})
}
}
6.动画
在详情页展示时,以放缩动画和旋转动画的方式出现在页面。我们使用animateTo显式动画来实现这个效果。
我们在组件onAppear回调方法中调用动画。如果在aboutToAppear中调用动画,自定义组件内的build还未执行,内部组件还未创建,动画时机过早,动画属性没有初值无法对组件产生动画。
下面看实例代码:
/**
* 详情页
*/
@Component
export struct DetailView {
@State rotateAngle: number = 90
@State title: string = ""
@State content: string = ""
@State date: string = ""
@State x: number = 0
@State y: number = 0
build() {
Column() {
Text(this.title)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.fontSize(20)
Stack() {
Text(this.date)
.margin({ top: 10 })
.fontColor($r("app.color._8b8b8b"))
.textAlign(TextAlign.Center)
.width("100%")
Text("收藏")
.margin({ top: 10 })
.fontColor(Color.Orange)
.onClick(()=>{
//调用收藏接口
})
}.width("80%")
.alignContent(Alignment.End)
Text(this.content)
.fontSize(20)
.margin({ top: 10 })
.lineHeight(40)
}
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
.rotate({
angle: this.rotateAngle
})
.scale({
x: this.x,
y: this.y,
})
.onAppear(() => {
animateTo({
duration: 800,
curve: Curve.LinearOutSlowIn,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end')
}
}, () => {
this.rotateAngle = 0
this.x = 1
this.y = 1
})
})
}
}
7.服务卡片
至此我们实现的是一个元服务的功能,那么如果需要在桌面显示一张卡片来显示我们的数据要怎么实现呢?
以创建一个动态卡片为例,右键->new ->Service Widget->Dynamic Widget创建一个4*4尺寸的卡片。如图为创建的空的一张4*4的卡片。
卡片如何请求数据,并点击数据后跳转到指定页面?
WidgetCard.ets (卡片页面)
import { HistoryInfo } from '../../HistoryInfo';
let storageUpdateByMsg = new LocalStorage();
@Entry(storageUpdateByMsg)
@Component
struct WidgetCard {
//第三步、接收来自于FormExtensionAbility返回的数据
@LocalStorageProp('list') list: Array<HistoryInfo> = []
aboutToAppear(): void {
// 第一步、触发message事件 向FormExtensionAbility请求数据
postCardAction(this, {
action: "message"
})
}
build() {
Row() {
Column() {
List() {
ForEach(this.list, (e: HistoryInfo, index: number) => {
ListItem() {
this.itemBuilder(e)
}
})
}
}
.width("'100%")
}
.height("100%")
.onClick(() => {
})
}
@Builder
itemBuilder(historyInfo: HistoryInfo) {
Column() {
Text(historyInfo.date + "-" + historyInfo.title)
.padding(15)
Divider()
}.width("100%")
.alignItems(HorizontalAlign.Start)
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility', // 第四步、跳转到当前应用下的UIAbility
params: {
date: historyInfo.date,
title: historyInfo.title,
content: historyInfo.content
}
});
})
}
}
EntryFormAbility.ets
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { HistoryInfo } from '../HistoryInfo';
export default class EntryFormAbility extends FormExtensionAbility {
onFormEvent(formId: string, message: string) {
//第二步、接收到来自卡片的请求,并通过updateForm方法向卡片传递数据
let list:HistoryInfo[] = []
let history = new HistoryInfo()
history.date = "1925年"
history.title = "洛迦诺公约》正式在伦敦签字"
history.content = "在95年前的今天,1925年12月1日(农历1925年10月16日),《洛迦诺公约》正式在伦敦签字。1925年12月1日,《洛迦诺公约》正式在伦敦签字,1926年9月14日生效。公约包括:洛迦诺会议最后议定书,德、比、法、英、意相互保证条约(又称莱茵保安公约),德国同比、法、波、捷4国分别签订的仲裁条约,以及法国同波、捷两国分别签订的相互保证条约。洛迦诺会议暂时调整了西欧各国的关系恢复了德国在欧洲的大国地位,削弱了法国的领导地位。点评:法国作为一战战胜国,开始走下坡路,而德国摆脱了失败阴影,开始快速成长"
let history2 = new HistoryInfo()
history2.date = "2015年"
history2.title = "扎克伯格裸捐事件"
let history3 = new HistoryInfo()
history3.date = "2018年"
history3.title = "孟晚舟事件"
let history4 = new HistoryInfo()
history4.date = "2018年"
history4.title = "美国华裔物理学家张首晟教授去世"
list.push(history)
list.push(history2)
list.push(history3)
list.push(history4)
class FormDataClass {
list: Array<HistoryInfo> = list
}
let formData = new FormDataClass();
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData);
formProvider.updateForm(formId,formInfo)
}
};
EntryAbility.ets(元服务的入口)
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { JSON } from '@kit.ArkTS';
import { HistoryInfo } from '../HistoryInfo';
export default class EntryAbility extends UIAbility {
private selectPage: string = '';
private currentWindowStage: window.WindowStage | null = null;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 第五步第一种情况、获取router事件中传递的targetPage参数
if (want?.parameters?.params) {
let historyInfo = JSON.parse(JSON.stringify(want.parameters.params)) as HistoryInfo
if (historyInfo) {
this.selectPage = "pages/DetailPage"
AppStorage.setOrCreate('currentHistoryInfo', historyInfo);
}
}
}
// 第五步第二种情况 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.params) {
let historyInfo = JSON.parse(want.parameters.params.toString())
if (historyInfo) {
this.selectPage = "pages/DetailPage"
AppStorage.setOrCreate('currentHistoryInfo', historyInfo);
if (this.currentWindowStage !== null) {
this.onWindowStageCreate(this.currentWindowStage);
}
}
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
if (this.currentWindowStage === null) {
this.currentWindowStage = windowStage;
}
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
if (this.selectPage.length > 0) {
windowStage.loadContent('pages/DetailPage')
} else {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
}
}
}
DetailPage.ets(点击卡片跳转的页面)
import { HistoryInfo } from '../HistoryInfo'
@Entry
@Component
struct DetailPage {
@StorageLink("currentHistoryInfo") currentHistoryInfo: HistoryInfo = new HistoryInfo()
@State rotateAngle: number = 90
@State title: string = ""
@State content: string = ""
@State date: string = ""
@State x: number = 0
@State y: number = 0
build() {
Column() {
Text(this.currentHistoryInfo.title)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.fontSize(20)
Stack() {
Text(this.currentHistoryInfo.date)
.margin({ top: 10 })
.fontColor($r("app.color._8b8b8b"))
.textAlign(TextAlign.Center)
.width("100%")
Text("收藏")
.margin({ top: 10 })
.fontColor(Color.Orange)
.onClick(() => {
//调用收藏接口
})
}.width("80%")
.alignContent(Alignment.End)
Text(this.currentHistoryInfo.content)
.fontSize(20)
.margin({ top: 10 })
.lineHeight(40)
}
.alignItems(HorizontalAlign.Center)
.width("100%")
.height("100%")
.padding({ top: 80 })
.justifyContent(FlexAlign.Start)
.rotate({
angle: this.rotateAngle
})
.scale({
x: this.x,
y: this.y,
})
.onAppear(() => {
animateTo({
duration: 800,
curve: Curve.LinearOutSlowIn,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end')
}
}, () => {
this.rotateAngle = 0
this.x = 1
this.y = 1
})
})
}
}