第四天
前言
综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新性的移动应用软件。
项目要求
- 技术栈:HarmonyOS 4.0,DevEco Studio。
- 实用性与创意:软件需解决具体问题,具备用户友好界面,创新性需体现在功能或用户体验上。
- 代码质量:遵守HarmonyOS编码规范,代码需模块化,易于维护。
多端布局
代码:
import mediaQuery from '@ohos.mediaquery'
import BreakpointConstants from '../constants/BreakpointConstants'
export default class BreakpointSystem{
private smListener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync(BreakpointConstants.RANGE_SM)
private mdListener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync(BreakpointConstants.RANGE_MD)
private lgListener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync(BreakpointConstants.RANGE_LG)
smListenerCallback(result: mediaQuery.MediaQueryResult){
if(result.matches){
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_SM)
}
}
mdListenerCallback(result: mediaQuery.MediaQueryResult){
if(result.matches){
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_MD)
}
}
lgListenerCallback(result: mediaQuery.MediaQueryResult){
if(result.matches){
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_LG)
}
}
updateCurrentBreakpoint(breakpoint: string){
AppStorage.SetOrCreate(BreakpointConstants.CURRENT_BREAKPOINT, breakpoint)
}
register(){
this.smListener.on('change', this.smListenerCallback.bind(this))
this.mdListener.on('change', this.mdListenerCallback.bind(this))
this.lgListener.on('change', this.lgListenerCallback.bind(this))
}
unregister(){
this.smListener.off('change', this.smListenerCallback.bind(this))
this.mdListener.off('change', this.mdListenerCallback.bind(this))
this.lgListener.off('change', this.lgListenerCallback.bind(this))
}
}
declare interface BreakpointTypeOptions<T>{
sm?:T,
md?:T,
lg?:T
}
export default class BreakpointType<T>{
options: BreakpointTypeOptions<T>
constructor(options: BreakpointTypeOptions<T>) {
this.options = options
}
getValue(breakpoint: string): T{
return this.options[breakpoint]
}
}
import BreakpointType from '../common/bean/BreanpointType'
import BreakpointConstants from '../common/constants/BreakpointConstants'
import { CommonConstants } from '../common/constants/CommonConstants'
import BreakpointSystem from '../common/utils/BreakpointSystem'
import RecordIndex from '../view/record/RecordIndex'
@Entry
@Component
struct Index {
@State currentIndex :number = 0
private breakpointSystem:BreakpointSystem = new BreakpointSystem()
@StorageProp('currentBreakpoint') currentBreakpoint:string = BreakpointConstants.BREAKPOINT_SM
@Builder TabBarBuilder(title:ResourceStr,image:ResourceStr,index:number){
Column({space:CommonConstants.SPACE_8}){
Image(image)
.width(22)
.fillColor(this.selectColor(index))
Text(title)
.fontSize(14)
.fontColor(this.selectColor(index))
}
}
aboutToAppear(){
this.breakpointSystem.register()
}
aboutToDisappear(){
this.breakpointSystem.unregister()
}
selectColor(index){
return this.currentIndex === index ? $r('app.color.primary_color'):$r('app.color.gray')
}
build() {
Tabs({barPosition:BreakpointConstants.BAR_POSITION.getValue(this.currentBreakpoint)/*位置底部*/}){
TabContent(){
RecordIndex({ isPageShow: this.isPageShow })
}
.tabBar(this.TabBarBuilder($r('app.string.tab_record'),$r('app.media.ic_calendar'),0))
TabContent(){
Text('发现页面')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_discover'),$r('app.media.discover'),1))
TabContent(){
Text('我的主页')
}
.tabBar(this.TabBarBuilder($r('app.string.tab_user'),$r('app.media.ic_user_portrait'),2))
}
.vertical(false)
.width('100%')
.height('100%')
.onChange(index => this.currentIndex = index)
.vertical(new BreakpointType({
sm:false,
md:true,
lg:true
}).getValue(this.currentBreakpoint))
//vertical为false时,tabbar宽度会默认撑满屏幕的宽度,vertical为true时,tabbar的高度会默认实际内容高度
}
}
import BreakpointType from '../../common/bean/BreanpointType'
import RecordPO from '../../common/bean/RecordPO'
import BreakpointConstants from '../../common/constants/BreakpointConstants'
import { CommonConstants } from '../../common/constants/CommonConstants'
import DateUtil from '../../common/utils/DateUtil'
import RecordService from '../../service/RecordService'
import RecordVO from '../../viewmodel/RecordVO'
import StatsInfo from '../../viewmodel/StatsInfo'
import CalorieStats from './CalorieStats'
import DatePickDialog from './DatePickDialog'
import NutrientStats from './NutrientStats'
@Component
export default struct StatsCard {
@StorageProp('selectedDate') selectedDate:number = DateUtil.beginTimeOfDay(new Date())//单向传输
@StorageProp('currentBreakpoint') currentBreakpoint:string = BreakpointConstants.BREAKPOINT_SM
@Consume @Watch('onRecordsChange') record:RecordVO[]
@State info:StatsInfo = new StatsInfo()
onRecordsChange(){
this.info = RecordService.calculateStatsInfo(this.record)
}
controller:CustomDialogController = new CustomDialogController({
builder:DatePickDialog({selectedDate: new Date(this.selectedDate)})
})
build() {
Column(){
//1、日期
Row(){
Text(DateUtil.formatDate(this.selectedDate))
.fontColor($r('app.color.secondary_color'))
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor($r('app.color.secondary_color'))//填充
}
.padding(CommonConstants.SPACE_10)
.onClick(()=>this.controller.open())
//2、统计
Swiper(){
//热量统计
CalorieStats({ intake:this.info.intake,expend:this.info.expend })
//营养素
NutrientStats({carbon:this.info.carbon,protein:this.info.protein,fat:this.info.fat})
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(CommonConstants.DEFAULT_18)
.indicatorStyle({selectedColor:$r('app.color.primary_color')})//切换点的样式
.displayCount(new BreakpointType({
sm:1,
md:1,
lg:2
}).getValue(this.currentBreakpoint))//平铺
}
.width(CommonConstants.THOUSANDTH_940)
.backgroundColor($r('app.color.stats_title_bgc'))
.borderRadius(CommonConstants.DEFAULT_18)
}
}
该代码实现了,不同设备的不同布局
记录项
代码:
class ItemModel{
getById(id: number, isFood: boolean = true){
return isFood ? foods[id] : workouts[id - 10000]
}
list(isFood: boolean = true): RecordItem[]{
return isFood ? foods : workouts
}
listItemGroupByCategory(isFood: boolean = true){
// 1.判断要处理的是食物还是运动
let categories = isFood ? FoodCategories : WorkoutCategories
let items = isFood ? foods: workouts
// 2.创建空的分组
let groups = categories.map(itemCategory => new GroupInfo(itemCategory, []))//item转成group
// 3.遍历记录项列表,将食物添加到对应的分组
items.forEach(item => groups[item.categoryId].items.push(item))
// 4.返回结果
return groups
}
}
import { CommonConstants } from '../../common/constants/CommonConstants'
import ItemModel from '../../model/ItemModel'
import GroupInfo from '../../viewmodel/GroupInfo'
import ItemCategory from '../../viewmodel/ItemCategory'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct ItemList {
showPanel:(item:RecordItem)=>void
@Prop isFood:boolean
build() {
Tabs(){
TabContent(){
this.TabsList(ItemModel.list(this.isFood))
}
.tabBar('全部')
ForEach(ItemModel.listItemGroupByCategory(this.isFood),
(group:GroupInfo<ItemCategory,RecordItem>) =>
{
TabContent(){
this.TabsList(group.items)
}
.tabBar(group.type.name)
})
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
.barMode(BarMode.Scrollable)//超出可以滚动
}
@Builder TabsList(items:RecordItem[]){
List({space:CommonConstants.SPACE_10}){
ForEach(items,
(item:RecordItem)=>{
ListItem(){
Row(){
Image(item.image)
.width(50)
Column(){
Text(item.name)
.fontWeight(CommonConstants.FONT_WEIGHT_500)
Text(`${item.calorie}千卡/${item.unit}`)
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
Blank()
Image($r('app.media.ic_public_add_norm_filled'))
.width(18)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
.padding(CommonConstants.SPACE_6)
}
.onClick(()=>this.showPanel(item))
}
)
}
.width('100%')
.height('100%')
}
}
该代码实现了记录项的分组查询,例如选择主食,只显示主食,选择果蔬,只显示果蔬。
在ItemCard,Panel滑动面板,判断id是否小于10000,小于的话,显示的为碳水蛋白质等食物信息,如果大于10000则显示运动相关的信息。
总结
我学会了多设备布局,查询不同分组数据,开始通过aboutToAppear,来进行布局大小的注册,取消注册。通过@StorageProp('currentBreakpoint')来监听currentBreakpoint为SM,MD或LG。再进行不同布局。Tabs中通过BreakpointConstants.BAR_POSITION.getValue(this.currentBreakpoint),判断currentBreakpoint是属于什么来放置导航栏的位置。在vertical函数中,根据currentBreakpoint来进行动态改变。
isFood判断查询信息是食物还是运动,创建groups通过map映射把itemCategory转换成GroupInfo类型,再循环将groups数据信息添加到对应的分组中。在食物查询列表页中,循环导入并引用GroupInfo数据