(四)鸿蒙实战案例 - 黑马健康生活应用

第四天

前言

综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新性的移动应用软件。


项目要求

  1. 技术栈:HarmonyOS 4.0,DevEco Studio。
  2. 实用性与创意:软件需解决具体问题,具备用户友好界面,创新性需体现在功能或用户体验上。
  3. 代码质量:遵守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数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值