期末项目实战:基于鸿蒙ArkTs语言的健康检测App食物列表页

5.食物列表页的开发

该页面为食物列表页,当用户点击某个食物的时候会弹出具体的信息

上图为食物列表页 单机食物后的详细信息页

食物列表页可以通过如下布局实现

        因为食物列表是一个单独的新页面,所以我们在Page文件下新建页面,并且命名为ItemPage

新建完成之后我们开始编辑,首先我们发现,食物列表页面整体是按分类来的{肉、蛋、奶、水果、蔬菜等},所以我们又可以用到先前学的TabBar的知识来对同一类的食物进行归类。TabBar的细节可以参考如下笔记
        TabBar与TabContent

定义后的Tabs布局如下:

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

@Component struct ItemList {

build() {

Tabs()

{

TabContent(){

} .tabBar('全部')

}

.width(CommonConstants.THOUSANDTH_940) .height('100%') } }

        通过页面的预览信息我们可以知道每一行的基本属性包括{图片、名称和卡路里、BLANK空格、+号按钮},因此,我们可以根据如下图片在TabContent进行编辑,我们发现ItemList中的食物的信息与RecordList中的食物的信息非常相似,所以我们可以复制粘贴到ItemList中进行微调。

IndexList中更改后的代码(原先为RecordList中的食物列表)

List({space:CommonConstants.SPACE_10}){ ForEach([1,2,3,4,5,6],(item)=>{ ListItem(){ Row({space:CommonConstants.SPACE_6}){ //其中的内容为图片+双行文字+千卡 Image($r('app.media.toast')) .width(50) //因为图片后的文字为上下布局,所以还要单独嵌套一个Column列布局 Column({space:CommonConstants.SPACE_4}){ Text('全买土司') .fontWeight(CommonConstants.FONT_WEIGHT_500) Text('one piece') .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) } /* * .swipeAction 看起来是一个方法调用,它可能是用于设置滑动操作的回调。 * 这种模式在许多现代UI框架中用于响应用户的滑动手势。end:this.deleteButton.bind(this) * 这部分代码意味着当滑动动作结束时,将调用 deleteButton 方法,并且 this 上下文被绑定到 * 当前对象。 * * * .bind 方法通常用于改变函数的 this 上下文,即函数内部 this 关键字所引用的对象。 * .bind 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时, * 它会使用 .bind 方法传入的第一个参数作为 this 的值,而其余参数会作为原函数的 * 参数传递。*/ }) } .width('100%') .height('100%')

        因为TabContent中每个食物信息都一样,所以为了避免代码的冗余,我们可以将TabContent中的内容封装到Builder中。

        但是会出现如下图所示的报错,因为不存在Tabs的父组件,单独对TabContent的拷贝会出现报错,所以我们另辟蹊径,既然TabContent不能拷贝,那么我们可以将TabContent其下的List进行构造拷贝。

更改后的Builder

因为不只是有一个Tabs,所以可以在Content中多新建几个Tabs,并且给每一个部分赋予不一样的名字。

6.单击按钮弹出底部Panel开发

当用户点击食物列表某一行的信息时,弹出如下的食物详情信息以及选择窗口

左右两张图片进行对应我们可以得出左侧图片的底层逻辑为:

1.第一行“早餐”文字之后的倒三角点击之后会弹出类似首页的日期选择器一样的弹窗

2.第二行是一个居中显示的图片

3.第三行是食物的详细信息(上面是热量单位,下面是具体的值)但是两行的布局要重复几份

4.食物的数量是左右布局的左面是Text下方跟着Divder右侧是一个文本

5.下方的键盘是自己生成的一个巨大的Row容器,布局使用的是Grid,使用Grid布局方便生成表格

6.最下方两个按钮分别是取消和提交按钮

在底部我们首先导入一个Panel面板,因为Panel的类型是Boolean 所以要赋初值为,我们默认给他一个初值为true

panel组件是一种可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中切换。

true为显示面板,false为隐藏面板

panel的属性有如下8种

mode也是一种枚举类型:Mini表示最小、Half表示只展示一半(默认)

dragbar:设置面板是否存在可以拖动的条(默认为true)

halfHeight:指定窗口高度

backgroundMask:设置蒙版颜色,默认为白色

PanelType的枚举:

1.Minibar:默认是最小化,用户可以手动将它设置大小

2.Foldable:内容永久展示区,可以有小中大三种展示方法(默认)

3.Temporary:将内容临时进行展示,只能进行大、中之间进行切换

那么如何判断我们点击的是哪个食物呢?我们首先先设置一个没有返回值的函数,数据先不传,后期再赋值

然后下方的点击事件我们就可以调用这个函数了,但是我们在此只是定义了一个showPanel的方法,具体showPanel的功能是什么呢?这需要我么去编写showPanle的方法

我们接下来回到ItemIndex文件中,在下方输入列表展示的代码,当用户点击到不同的物品的时候可以跳转过去。注意:我们并不是调用onPanelShow方法,而是使用它的bind方法来指向。功能就是在ItemList中点击的时候就会触发ShowPanel函数,而在函数内部我们又将showPanel方法设置为真,可以打开弹窗,控制弹窗面板展示

将onPanelShow的函数的结果设置为true

6.1设计面板

6.1.1顶部日期开发

如下为顶部日期的基础部分

import { CommonConstants } from '../../common/constants/CommonConstants' @Component struct ItemPanelHeader { build() { Row(){ Text('2024年6月20号 早餐') .fontSize(18) .fontWeight(CommonConstants.FONT_WEIGHT_600) Image($r('app.media.ic_public_spinner')) .width(20) } .width(CommonConstants.THOUSANDTH_940)//卡片不能占据全部的页面 } }

当我们完成之后默认的弹窗的界面如上所示

6.1.2记录条目的详细信息和数量信息卡片

首先我们可以将详细信息的卡片开发成一个组件,然后导出这个组件,在ItemIndex中调用该组件,基本语法如下

import { CommonConstants } from '../../common/constants/CommonConstants' @Component export default struct ItemCard { build() { Column() { Image($r('app.media.toast')) Row() { Text('全麦吐司') .fontWeight(CommonConstants.FONT_WEIGHT_600) } .backgroundColor($r('app.color.light_primary_color')) .padding({ top: 5, bottom: 5, left: 12, right: 12 }) Divider() .width(CommonConstants.THOUSANDTH_940) .opacity(0.6) // .width() } } }

在页面的中间,我们存在多个一样的样式的文字,我们可以使用Builder来统一定义一个样式,方便我们后期的使用,具体语法如下

@Builder NutrientInfo(label:string,value:number){ //上下纵式布局 Column({space:CommonConstants.SPACE_8}){ Text(label) .fontSize(14) .fontColor($r('app.color.gray')) Text(value.toFixed(1)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_500) //toFix(1)将value的数值类型转化为一位小数 } }

创建成功之后我们在分割线之后导入营养素的卡片(营养素的卡片放在一行内即可)

Row({space:CommonConstants.SPACE_8}){ this.NutrientInfo('热量(千卡)',91.0) this.NutrientInfo('碳水(克)',15.5) this.NutrientInfo('蛋白质(克)',4.4) this.NutrientInfo('脂肪(克)',1.3) }

接下来我们开发食物的数量值,因为人每天会摄入食物,所以食物的数量值每天都会发生变化,我们最好使用Prop状态变量。状态变量不适用State的原因:该组件负责渲染,所以必须要接收到来自键盘的输入。整体的布局又分为上下两部分的列式布局,具体代码如下:

Column({space:CommonConstants.SPACE_4}){ Text(this.amount.toFixed(1)) .fontSize(50) .fontColor($r('app.color.primary_color')) .fontWeight(CommonConstants.FONT_WEIGHT_600) Divider() .color($r('app.color.primary_color')) } Text('片') .fontWeight(CommonConstants.FONT_WEIGHT_600) .fontColor($r('app.color.light_gray'))

6.1.3数字键盘部分设计

使用Grid开发数字键盘,我们根之前类似将键盘封装到一个组件当中

Grid简介:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。内部需要使用GridItem组件

使用方法:

Grid(scroller?: Scroller)

需要在Grid内部传入一个Scroller滚动条控制器,但是数字键盘内一个GridItem中只有一个数字,不需要进行滚动,因此我们可以空出来不传值(空参)。

Grid、

Grid的属性(主要都是来控制行列的布局)

columnsTemplate:列模板,设置表格的列数,默认为1,如果传入参数为1fr 1fr 2fr说明存在三列,三列的布局比例为1:1:2,除外同理。

rowsTemplate:行模板(行列模板传入的都是字符串)行原理同列,如果三行三列但是存在十个子组件,那么超出的子组件被隐藏

完成Grid数字键盘的基本设置如上图所示,但是键盘需要在里面使用GridItem,我们可以不需要定义10个GridItem,我们可以先定义好每个GridItem之中的内容然后使用for循环来完成数字键盘内容的设计

numbers:string[] = ['1','2','3','4','5','6','7','8','9','0','.']

首先使用数组将键盘的内容定义好,然后我们发现键盘数字的内容与键盘文本的内容的样式是差不多的,我们可以将样式抽取出来@style

ForEach(this.numbers,num =>{ GridItem(){ Text(num)//循环的时候每循环一次就使用一次Text将顺序数字打印出来并渲染 .fontSize(20) .fontWeight(CommonConstants.FONT_WEIGHT_900) } .backgroundColor(Color.White)//设置GridItem的背景颜色 .height(60) .borderRadius(8) }) GridItem(){ Text('删除')//循环的时候每循环一次就使用一次Text将顺序数字打印出来并渲染 .fontSize(20) .fontWeight(CommonConstants.FONT_WEIGHT_900) } .backgroundColor(Color.White)//设置GridItem的背景颜色 .height(60) .borderRadius(8)

键盘定义完成后的图片如下,但是我们发现键盘无法与上述绿色的数字进行联动,我们点击了键盘数字仍没有反应,这时我们就可以用到amount值的思想,当我们点击键盘上的值的时候amount的值也跟随改变,现在我们只有amount改变绿色字体,缺乏数字键盘改变amount的值,父组件使用的时候我们可以传入amount的数值,点击数字键盘的时候再改变amount的值。使用Link双向绑定来完成联动

在ItemIndex中首先定义好变量value

定义完成后后面再调用方法的时候也要将value的值传入

营养素面板也不要忘记定义value的值

定义完成之后,还差最后一步,定义按钮单元的功能

按钮单元的开发又可以分为四个部分

1.拼接用户输入的内容

let val =this.value +num

2.校验(防止出现..2、.2、0.33片、99999片等异常情况发生)

let fistIndex = val.indexOf('.')//从前往后查找字符串是否有. let lastIndex = val.indexOf('.')//从后往前查找字符串是否有. if(fistIndex!=+lastIndex || (lastIndex !==-1 && lastIndex <val.length -2))//lastIndex !==-1 存在小数点 &&并且 lastIndex <val.length -2)判断小数点后面位数不能超过两位 { //非法输入 return //直接拒绝什么都不做 }

3.转换,为了减少代码的冗余,增加可复用性我们最好使用一个方法

//3.将字符串转为数值 //输入2.变成 2 let amount = this.parseFloat(val)//调用该方法将字符串的值转换成浮点数传给amount //4.保存 if(amount>=999.9){ this.amount = 999//防止操作999 this.value = '999' } else { this.amount=amount this.value = val }

parseFloat的编写

parseFloat(str:string){ //校验字符串是否以.结尾 if (str.endsWith('.')) {//判断字符串结尾最后一位是否为.的确认的情况 str = str.substring(0,str.length-1) /*在JavaScript中,表达式 str = str.substring(0, str.length - 1) 的作用 是从字符串 str 的起始位置(索引0)开始截取,直到字符串长度减去1的位置。 这意味着它将复制 str 字符串,但不包括最后一个字符。*/ } return parseFloat(str)//字符串转换成浮点数 }

4.保存

//4.保存,小于999.9才能正常保存 if(amount>=999.9){ this.amount = 999//防止操作999 this.value = '999' } else { this.amount=amount this.value = val }

6.1.4按钮设计

键盘中共存在两个按钮,两个按钮都在一行,所以我们可以使用Row容器将两个按钮放入一行,然后我们开始定义点击按钮的功能

首先定义好按钮的基本的样式模板
 

7.响应式布局(根据屏幕的尺寸选择宽屏显示还是竖屏)

如何实现上述的响应式布局呢?在ArkTs中为我们提供了相关媒体查询的API使用该API我们可以非常方便的去获取当前设备的信息,使用媒体查询的步骤如下:

7.1导入媒体查询模块并获取查询条件以及对应的Listener

我们在使用Listener的时候调用了媒体查询的matchMediaSync方法,在此方法内我们定义了判断界面到底是竖屏显示还是横屏显示的媒体特征(要求媒体特征宽度在320到600vp之间)。该条件给到matchMediaSync方法,那么这个方法返回一个监听器listener,监听器会一直监听条件所给出的媒体特征的状态变化,一旦媒体特征的状态发生变化它就会重新触发监听器的回调函数,但是我们现在缺少一个回调函数,因此我们现在可以把监听器绑定一个回调函数。(如7.2所示)更多的细节我们可以去到鸿蒙的官方文档去查看相关的指令

鸿蒙开发者文档:媒体查询(mediaquery)

在其中我们可以查询到媒体查询的条件包含三大部分:
媒体类型(media-type)媒体逻辑操作(media-logic-operations)媒体特征(media-feature)

7.1.1媒体类型

媒体类型只有一个screen参数,说明媒体的类型仅受到屏幕的相关参数影响,如果我们省略不写默认也是这个参数,并不会有影响。

7.1.2 媒体操作逻辑(与逻辑运算符相关)

7.1.3媒体的特征(你要查页面的哪一个属性)

举例:orientation表示屏幕的方向,页面在横屏和竖屏的展示效果是不同的,因此我们可以监听该媒体特征以此来判断页面是否是竖屏还是横屏显示

7.2设置回调函数

调用listener的on方法,传入两个参数第一个参数代表提示,说明这个监听器是来监听变化的,监听记录当前设备的状态,发生变化之后都会触发回调函数,例如width更改一定会触发回调函数,但是width的值是否还满足320到600的范围监视器并不确定,因此我们接下来做进一步判断,使用result来记录状态,如果这个结果是true,证明不仅仅状态发生了变更但是width仍符合范围,反之不符合(后续页面渲染的时候可以基于这个状态进行渲染)

那我们如何记录result的状态呢?我们可以给屏幕的宽度范围再起一个别名当作它的状态,提前为屏幕的宽度进行分段,类似衣服的号码(S、M、L)对应的具体的条件是320——600、600——840、840以上。媒体特征符合具体哪个条件直接就记录变量即可。后续的页面开发直接去除标记进行判断即可。

上述三个变量我们保存到哪里呢?要求是每个页面都能用到的长期保存、且共享的地方。由此,与AppStore全局存储的理念不谋而合,我们可以利用AppStore的SetOrCreate方法,对S、M、G去做一个键值存储。

以后再使用页面响应式布局的时候都可以直接调用StorageProp,利用这个装饰器去获取currentBreakpoint的值,读到这个值的时候我们再页面渲染的时候就能做出判断。

7.3编写媒体查询的工具类

7.3.1在constants文件夹下创建BreakpointConstant断点文件,创建并导出类BreakpointConstant

声明一个只读的变量,并定义一个字符串常量BREAKPOINT_SM,其值被设置为'sm'。这通常用于响应式设计中,表示小屏幕设备的断点(breakpoint)。同理定义中型屏幕和大型屏幕

定义三种屏幕类型的宽度的范围,同样也为只读的形式,方便计算机进行区分。最后定义静态只读属性static readonly BAR_POSITION,用来判断不同断点下的BarPosition配置

接下来我们新建断点系统文件,用于响应媒体查询(Media Queries)的变化,媒体查询是CSS中用来根据不同的屏幕尺寸、分辨率等条件应用不同样式的一种功能。

它们都是mediaQuery.MediaQueryListener类型的实例

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)

上述代码定义了三个私有属性的监视器,利用mediaQuery的MediaQueryListener方法,根据不同的屏幕尺寸、分辨率条件应用不同样式的一种功能。

7.4调试

接下来我们可以利用index页面举例,打开Multi-profile preview 多设备调试,以多种设备预览手机

我们发现使用了响应式布局之后,手机、折叠屏、平板都可以运行该APP,但是我们发现宽屏状态下的内容被拉的很大,所以最好将宽屏状态下的Bar改成左侧列布局

默认我们设置的BarPosition在底部,我们可以把它调整到左侧来规划布局。

但是我们也要区分更改布局的情况,当宽度超过600的时候判定为折叠屏就可以更改布局了(currentBreakpoint为大屏幕的时候)。我们可以设置一个choose函数,函数的功能就是判断当前的屏幕是大屏幕还是小屏幕。

第一种方法:使用if对屏幕的大小进行判断

//1.方法1:使用if else判断 // if(this.currentBreakpoint === 'sm'){ // return BarPosition.End // }else if (this.currentBreakpoint === 'md') { // return BarPosition.Start // } // if (this.currentBreakpoint === 'lg') { // return BarPosition.Start // } // }

第二种方法:使用键值对的方法可以简化窗口屏幕的判断。

let p = { sm:BarPosition.End, md:BarPosition.Start, lg:BarPosition.Start } return p[this.currentBreakpoint]

最后我们在给build属性的时候给它添加一个Vertical的纵向布局模式,与先前类似,仍需判断屏幕的尺寸,对不同尺寸的屏幕施行不一样的判断方法

.vertical({ sm:false, md:true, lg:true }[this.currentBreakpoint])//更改布局模式为vertical的纵向布局

因为Vertical内的结果只有false和true两种,所以我们对不同的屏幕直接赋予不同的true和false值。这样我们对于尺寸的判断就完成了。

但是界面仍存在不够美观的问题,我们可以在bean文件夹下创建BreakpointType.ets文件,用来创建断点的类型。

declare interface BreakpointTypeOptions<T>{/*定义一个泛型接口 declare关键字用于声明类型,而不是定义具体的实现。 BreakpointTypeOptions<T>: 接口名称后面跟着尖括号<T>, 表示这是一个泛型接口,T是泛型参数,可以是任何类型。*/ sm?:T, md?:T, lg?:T/* { sm?: T, md?: T, lg?: T }: 这个对象结构定义了三个可选属性 * :sm、md和lg。每个属性的类型都是泛型参数T, * 这意味着你可以为这些属性传递任何类型的值。*/ } export default class BreakpointType<T>{//该类要从对象中取值,所以最好有一个参数Options,它的类型应该和上面的泛型类型相同。继承一下上述类型中的泛型 options: BreakpointTypeOptions<T> constructor(options: BreakpointTypeOptions<T>) {//同时,在option中得到上述的泛型之后我需要再使用一个构造函数来记住options,也是接收泛型,构造完成之后赋值即可 this.options = options //这样BreakpointType类内部就获取到传入的对象了 } getValue(breakpoint: string): T{//定义一个getValue方法传入breakpoint的string类型同时被返回一个T类型 return this.options[breakpoint] } }

我们发现在宽屏状态下界面的卡片不方便滑动,最好是平铺状态,我们对此进行设计,首先我们先定位到卡片的编辑文件StatusCard中,在营养素统计的属性下设置discount为(2),这样会一次性显示两个元素,就不会发生滑动的情况,但是,我们没有去区分宽窄平导致窄屏的营养素卡片都堆到了一起不美观,所以我们还要对宽窄屏幕进行判断

.displayCount(new BreakpointType({ sm:1, md:1, lg:2 }).getValue(this.currentBreakpoint))//默认展示元素的数量,如果我们改为两个,则就没有滑动了直接平铺,但是仍要区分宽窄屏 //为此我们可以对何时平铺进行一个判断

直接把sm、md、ld的discount显示的元素的个数赋值并导入currentBrealpoint的包,这样计算机就能区分什么时候平铺什么时候滚动了。

同理,根据常识,界面平铺的时候最好让滑动按钮消失掉,根据上述原理,我们可以在判断的时候先分出宽窄屏然后indicator方法来显示窄屏的滑动按钮,隐藏宽屏的滑动按钮。

这样我们的平板显示的时候就没有滑动按钮了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值