三、首页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: