黑马健康App

三、首页UI框架设计

1.分析设计

布局如下:


所需知识有:

tabBar水平布局:

tabBar垂直布局:

注意:

(1)可以通过Tabs()的属性{barPosition:BarPosition.End}来调整标题布局:End表示在底部,默认为顶部。

(2)样式属性.vertical()判断bar是否为垂直布局,true垂直布局,false水平布局(默认为水平)。

(3)需要用自定义样式(注意用@Builder)定义想要的tabBar的样式,但此时变量的控制也需要自己操作(比如点到某个图标时会显示高亮)。

(4)Tabs()本身就是一个容器。Image()的样式属性.fillColor()的图片必须是SVG格式的。

2.代码实现

/*Index*/
@Entry
@Component
struct Index {
  @State currentIndex: number = 0

  @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))
    }
  }

//通用样式
selectColor(index: number) {
    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))

    }
    .width('100%')
    .height('100%')
    .onChange(index => this.currentIndex = index)
  }
}

previewer:

四、饮食记录UI设计

1.布局分析

注意:点击日期会出现弹窗以便用户选择具体记录日期,需要用到DatePicker组件。卡片部分可以切换具体内容,用到Swiper组件。

2.代码实现

整体分为三部分:(1)头部搜索栏(2)统计卡片(3)纪录列表
目录划分:

头部搜索栏

(1)搜索栏用到Search()组件,其属性placeholder指栏中灰色的提示信息。
(2)显示邮箱的提示图标需要用到Badge()组件,它本身就是一个容器,需要传入参数count、position、style。

/*SearchHeader*/
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct SearchHeader {
  build() {
    Row({space: CommonConstants.SPACE_6}){
      Search({placeholder: '搜索饮食或运动信息'})
        .textFont({size: 18})
        .layoutWeight(1)
      Badge({count: 1, position: BadgePosition.RightTop, style: {fontSize: 12}}){
        Image($r('app.media.ic_public_email'))
          .width(24)
      }
    }
    .width(CommonConstants.THOUSANDTH_940)//表示占总宽度的94%
  }
}

/*RecordIndex*/
@Component
export default struct RecordIndex {
build() {
    Column(){
      // 1.头部搜索栏
      SearchHeader()
      // 2.统计卡片
     
      // 3.记录列表
      
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.index_page_background'))
  }
}

previewer:

统计卡片

包括:a.日期信息(a.1文本 a.2弹窗) b.统计信息( b.1.热量统计 b.2.营养素统计)

日期信息

点击日期会出现弹窗以便用户选择具体记录日期,需要用到DatePicker组件。

文本:文本内边距太拥挤,需要用.padding()调节。

/*StatsCard*/
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct StatsCard {
build() {
    Column(){
      // 1.日期信息
      Row(){
        Text('2024/01/25')
          .fontColor($r('app.color.secondary_color'))
        Image($r('app.media.ic_public_spinner'))
          .width(20)
          .fillColor($r('app.color.secondary_color'))
      }
      .padding(CommonConstants.SPACE_8)
    }
    .width(CommonConstants.THOUSANDTH_940)//占总宽度的94%
    .backgroundColor($r('app.color.stats_title_bgc'))
    .borderRadius(CommonConstants.DEFAULT_18)//使卡片有圆弧
  }
}

previewer:

弹窗:

(1)日期选择器DataPicker: 


(2)AppStorage整个应用的内部存储(全局存储)中的SetOrCreate()创建后保存函数,作用是该值若不存在就创建,若存在就直接覆盖。注意这个地方不要把日期date对象存进去,因为它在将来做状态变量监控 的时候会有问题,所以我们存对应的毫秒值(日期和其对应毫秒值是可以互相转化的),因为这种number类型可以做状态监控。
(3)不能这样初始化日期:@StorageProp('selectedDate') selectedDate: number = new Date(),因为new Date()获得的数据是当天的年月日加时分秒,而我们只需要年月日。
(4)注意把获取的日期值传回,使下次打开时依旧保存(需将number类型转回日期类型)。

/*DatePickDialog*/
import { CommonConstants } from '../../common/constants/CommonConstants'
@CustomDialog
export default struct DatePickDialog {
  controller: CustomDialogController
  selectedDate: Date = new Date()
  build() {
    Column({space: CommonConstants.SPACE_12}){
      // 1.日期选择器
      DatePicker({
        start: new Date('2020-01-01'),
        end: new Date(),
        selected: this.selectedDate
      })
        .onChange((value: DatePickerResult) => {
          this.selectedDate.setFullYear(value.year, value.month, value.day)
        })
      // 2.按钮
      Row({space:CommonConstants.SPACE_12}){
        Button('取消')
          .width(120)
          .backgroundColor($r('app.color.light_gray'))
          .onClick(() => this.controller.close())
        Button('确定')
          .width(120)
          .backgroundColor($r('app.color.primary_color'))
          .onClick(() => {
            // 1.保存日期到全局存储
            AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime())
            // 2.关闭窗口
            this.controller.close()
          })
      }
    }
    .padding(CommonConstants.SPACE_12)
  }
}

/*StatsCard*/
import DatePickDialog from './DatePickDialog'

@Component
export default struct StatsCard {

  @StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date())
controller: CustomDialogController = new CustomDialogController({
    builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})//把获取的日期值传回,使下次打开时依旧保存(注意将number类型转回日期类型)
  })
  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_8)
      .onClick(() => this.controller.open())

      // 2.统计信息
    }
    .width(CommonConstants.THOUSANDTH_940)//占总宽度的94%
    .backgroundColor($r('app.color.stats_title_bgc'))
    .borderRadius(CommonConstants.DEFAULT_18)//使卡片有圆弧
  }
}

previewer:

统计信息

(1)统计卡片部分可以穿梭切换不同内容,用到Swiper组件。
          


(2)穿梭框样式属性.indicatorStyle()

热量统计:

注意均匀分布空间和内边距
做抽取
加进度条需要用到层叠关系组件Stack()容器,进度条用Progress()

import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct CalorieStats {
  intake: number=192//摄入量
  expend: number=150//消耗量
  recommend: number = CommonConstants.RECOMMEND_CALORIE//推荐值写死

  //计算剩余摄入量函数
  remainCalorie(){
    return this.recommend - this.intake + this.expend
  }

  build() {
    Row({space: CommonConstants.SPACE_6}){
      // 1.饮食摄入
      this.StatsBuilder('饮食摄入',  this.intake)
     // 2.还可以吃
      Stack(){
        // 2.1.进度条
        Progress({
          value: this.intake,
          total: this.recommend,
          type: ProgressType.Ring
        })
          .width(120)
          .style({strokeWidth: CommonConstants.DEFAULT_10})//调节进度条环的粗细
          .color($r('app.color.primary_color'))
        // 2.2.统计数据
        this.StatsBuilder({label: '还可以吃', value: this.remainCalorie(),tips: `推荐${this.recommend}`})
      }
      // 3.运动消耗
      this.StatsBuilder( '运动消耗',this.expend)
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceEvenly)
    .padding({top: 30, bottom: 35})
  }

  //label文字说明,value摄入量,tips补充说明
  @Builder StatsBuilder($$:{label: string, value: number, tips?: string}){
    Column({space: CommonConstants.SPACE_6}){
      Text($$.label)
        .fontColor($r('app.color.gray'))
        .fontWeight(CommonConstants.FONT_WEIGHT_600)
      Text($$.value.toFixed(0))
        .fontSize(20)
        .fontWeight(CommonConstants.FONT_WEIGHT_700)
      if($$.tips){
        Text($$.tips)
          .fontSize(12)
          .fontColor($r('app.color.light_gray'))
      }
    }
  }
}

previewer:

营养素统计:
用toFixed()将number类型转变成String类型。

import { CommonConstants } from '../../common/constants/CommonConstants'

@Component
export default struct NutrientStats {
   carbon: number=23
   protein: number=9
   fat: number=7

  recommendCarbon: number = CommonConstants.RECOMMEND_CARBON
  recommendProtein: number = CommonConstants.RECOMMEND_PROTEIN
  recommendFat: number = CommonConstants.RECOMMEND_FAT

build() {
    Row({space: CommonConstants.SPACE_6}){
      this.StatsBuilder(
       '碳水化合物',
        this.carbon,
        this.recommendCarbon,
        $r('app.color.carbon_color')
      )
      this.StatsBuilder(
        '蛋白质',
        this.protein,
        this.recommendProtein,
        $r('app.color.protein_color')
      )
      this.StatsBuilder(
        '脂肪',
        this.fat,
        this.recommendFat,
        $r('app.color.fat_color')
      )
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceEvenly)
    .padding({top: 30, bottom: 35})
  }

  @Builder StatsBuilder($$:{label: string, value: number, recommend: number, color: ResourceStr}){
    Column({space: CommonConstants.SPACE_6}){
      Stack(){
        Progress({
          value: value,
          total: recommend,
          type: ProgressType.Ring
        })
          .width(95)
          .style({strokeWidth: CommonConstants.DEFAULT_6})
          .color($$.color)
        Column({space: CommonConstants.SPACE_6}){
          Text('摄入推荐')
            .fontSize(12)
            .fontColor($r('app.color.gray'))
          Text(`${value.toFixed(0)}/${recommend.toFixed(0)}`)
            .fontSize(18)
            .fontWeight(CommonConstants.FONT_WEIGHT_600)
        }
      }
      Text(`${$$.label}(克)`)
        .fontSize(12)
        .fontColor($r('app.color.light_gray'))
    }
  }
}

previewer:

纪录列表

在List()内部利用foreach循环
加侧滑事件swipeAction加入删除按钮

import router from '@ohos.router'
import { CommonConstants } from '../../common/constants/CommonConstants'

//text特有样式(用@Extend)
@Extend(Text) function grayText(){
  .fontSize(14)
  .fontColor($r('app.color.light_gray'))
}

@Component
export default struct RecordList {
  build() {
    List({space: CommonConstants.SPACE_10}){
      ForEach([1,2,3,4,5], (item) => {
        ListItem(){
          Column({space: CommonConstants.SPACE_8}){
            // 1.分组的标题
            Row({space: CommonConstants.SPACE_4}){
              Image($r('app.media.ic_breakfast')).width(24)
              Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
              Text('建议423~592千卡').grayText()
              Blank()
              Text('190').fontSize(14).fontColor($r('app.color.primary_color'))
              Text('千卡').grayText()
              Image($r('app.media.ic_public_add_norm_filled'))
                .width(20)
                .fillColor($r('app.color.primary_color'))
            }
            .width('100%')
   
            // 2.组内记录列表
            List(){
              ForEach([1,2], (item) => {
                ListItem(){
                  Row({space: CommonConstants.SPACE_6}){
                    Image($r('app.media.toast')).width(50)
                    Column({space: CommonConstants.SPACE_4}){
                      Text('全麦吐司').fontWeight(CommonConstants.FONT_WEIGHT_500)
                      Text('一片').grayText()
                    }
                    Blank()
                    Text('91千卡').grayText()
                  }
                  .width('100%')
                  .padding(CommonConstants.SPACE_6)
                }.swipeAction({end: this.deleteButton.bind(this)})
              })
            }
            .width('100%')
}
    .width(CommonConstants.THOUSANDTH_940)
    .height('100%')
    .margin({top: 10})
  }

    @Builder deleteButton(){
    Image($r('app.media.ic_public_delete_filled'))
      .width(20)
      .fillColor(Color.Red)
      .margin(5)
  }
}

previewer:

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值