概要
提示:这里可以添加技术概要
整体流程
1.预期效果
预期①底部页面
预期②键盘
预期③部署
2.结构框架
3.具体代码
1.预期①
ItemPanelHeader
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemPanelHeader {
build() {
Row() {
Text('2024年6月20日 早餐')
.fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor(Color.Black)
}
.width(CommonConstants.THOUSANDTH_940)
}
}
ItemCard
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct ItemCard {
@Prop amount: number//接收
@Link item: RecordItem
build() {
Column({space: CommonConstants.SPACE_8}){
// 1.食物或者运动的图片
Image(this.item.image).width(150)
// 2.上面图片的名称
Row(){
Text(this.item.name).fontWeight(CommonConstants.FONT_WEIGHT_700)
}
.backgroundColor($r('app.color.lightest_primary_color'))
.padding({top: 5, bottom: 5, left: 12, right: 12})//内边距
Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)//下划线
// 3.营养素
Row({space: CommonConstants.SPACE_8}){
this.NutrientInfo({label: '热量(千卡)', value: this.item.calorie})
if(this.item.id < 10000){
this.NutrientInfo({label: '碳水(千卡)', value: this.item.carbon})
this.NutrientInfo({label: '蛋白质(千卡)', value: this.item.protein})
this.NutrientInfo({label: '脂肪(千卡)', value: this.item.fat})
}
}
Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)
// 4.数量,吃了多少这个食物
Row(){
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'))
}
.width(150)
Text(this.item.unit)
.fontColor($r('app.color.light_gray'))
.fontWeight(CommonConstants.FONT_WEIGHT_600)
}
}
}
//营养素信息
@Builder NutrientInfo($$:{label: string, value: number}){
Column({space: CommonConstants.SPACE_8}){
Text($$.label).fontSize(14).fontColor($r('app.color.light_gray'))
//随着数量的变更而变更
Text(($$.value * this.amount).toFixed(1)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
}
}
}
2.预期②
NumberKeyboard
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NumberKeyboard {
numbers: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.']
@Link amount: number
@Link value: string
@Styles keyBoxStyle(){
.backgroundColor(Color.White)
.borderRadius(8)
.height(60)
}
build() {
Grid(){
ForEach(this.numbers, num => {
GridItem(){
Text(num).fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickNumber(num))
})
GridItem(){
Text('删除').fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickDelete())
}
.width('100%')
.height(280)
.backgroundColor($r('app.color.index_page_background'))
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding(8)
.margin({top: 10})
}
//点击数值时
clickNumber(num: string){
// 1.拼接用户输入的内容
let val = this.value + num
// 2.校验输入格式是否正确,判断输入的正误
let firstIndex = val.indexOf('.')//输入内容是否有脚标
let lastIndex = val.lastIndexOf('.')
//小数点后面的位数不能超过两位
if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length - 2)){
// 非法输入
return
}
// 3.将字符串转为数值
let amount = this.parseFloat(val)
// 4.保存
if(amount >= 999.9){//不能超过999,否则强制赋值
this.amount = 999.0
this.value = '999'
}else{
this.amount = amount
this.value = val
}
}
//点击删除
clickDelete(){
//不要删到尾
if(this.value.length <= 0){
this.value = ''
this.amount = 0
return
}
this.value = this.value.substring(0, this.value.length - 1)
this.amount = this.parseFloat(this.value)
}
//字符串的转换
parseFloat(str: string){
if(!str){
return 0
}
if(str.endsWith('.')){
str = str.substring(0, str.length - 1)
}//不是非法输入,转换
return parseFloat(str)
}
}
3.
BreakpointTypeOptions
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]
}
}
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))
}
.width('100%')
.height('100%')
.onChange(index=>this.currentIndex=index)
.vertical(new BreakpointType({
sm: false,
md: true,
lg: true
}).getValue(this.currentBreakpoint))
}
实现步骤
1.我们已经完成了列表项的主页面ItemIndex已经完成了第一部分,也就是具体项的列表的开发,现在食物列表已经展示出来了,当用户点击某一个具体项时会弹出底部面板,在这个面板里展示食物(具体项)的详细信息。
2.面板主要包括四部分,顶部日期ItemPanelHeader,记录项卡片ItemCard,数字键盘NumberKeyboard和按钮。在单独创建一个组件去编写,代码清晰整洁。
3.我们的整体布局已经完成,需要完成对多设备的响应式布局。
新的API
1.Panel滑动面板
可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中切换
接口
Panel(show: boolean)
// xxx.ets
@Entry
@Component
struct PanelExample {
@State show: boolean = false
build() {
Column() {
Text('2021-09-30 Today Calendar: 1.afternoon......Click for details')
.width('90%').height(50).borderRadius(10)
.backgroundColor(0xFFFFFF).padding({ left: 20 })
.onClick(() => {
this.show = !this.show
})
Panel(this.show) { // 展示日程
Column() {
Text('Today Calendar')
Divider()
Text('1. afternoon 4:00 The project meeting')
}
}
.type(PanelType.Foldable).mode(PanelMode.Half)
.dragBar(true) // 默认开启
.halfHeight(500) // 默认一半
.onChange((width: number, height: number, mode: PanelMode) => {
console.info(`width:${width},height:${height},mode:${mode}`)
})
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
}
}
.mode(PanelMode.Full)//面板的占据 .dragBar(false)//能不能调整面板的高度 .backgroundMask($r('app.color.light_gray'))//蒙版的颜色 .backgroundColor(Color.White)
2.Grid
网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
包含GridItem子组件
接口
Grid(scroller?: Scroller)//滚动条
// xxx.ets
@Entry
@Component
struct GridExample {
@State Number: String[] = ['0', '1', '2', '3', '4']
scroller: Scroller = new Scroller()
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (day: string) => {
ForEach(this.Number, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
}, day => day)
}, day => day)
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('90%')
.backgroundColor(0xFAEEE0)
.height(300)
}
}
3.parseFloat//字符串转换
//字符串的转换
parseFloat(str: string){
if(!str){
return 0
}
if(str.endsWith('.')){
str = str.substring(0, str.length - 1)
}//不是非法输入,转换
return parseFloat(str)
}
技术小结
1.函数
什么时候展示底部Panel,当点击按钮时弹出,所以我们要在具体项的列表ItemList页面给每一个item项设计点击事件,那我们如何控制父组件(列表项(食物和运动)主页面ItemIndex)中底部Panel的弹出呢???需要用函数控制。我们把函数声明出来,当父组件调用的时候去覆盖这个声明。
showPanel展示这个Panel,点击哪个食物就弹出哪个具体页,传入参数item。然后我们就能在列表项后面添加点击事件
showPanel:(item:RecordItem)=>void
回到列表项的主页面,声明一个函数onPanelShow//当底板展示的时候需要做一些事情//,然后我们就可以在使用它时传递,即在具体项的列表ItemList中传递(因为就是这些具体的项要弹出的Panel自然要在这里传递)。
2.Panel不弹出??
面板Panel是不占高度的,它是浮在表面的,但是它要求它所在的容器以及它所在容器里面的组件的高度都是固定的,其中具体项的列表ItemLis的高度是不固定的,导致Panel没有办法展示出来。所以要给列表固定高度(除了顶部全是列表,即权重布局),然后就能在固定的位置渲染面板了
3.抽取
在记录项卡片ItemCard页面中间部分有具体信息的描述,我们会发现他们的样式相同,可抽取
//营养素信息
@Builder NutrientInfo($$:{label: string, value: number}){
Column({space: CommonConstants.SPACE_8}){
Text($$.label).fontSize(14).fontColor($r('app.color.light_gray'))
//随着数量的变更而变更
Text(($$.value * this.amount).toFixed(1)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
}
}
4.状态
食物吃了多少,需要一个数量值amount,我们不能把它写死,所以定义一个状态变量(@Prop amount: number//接收),因为这个值是用户可以输入的,我们不能在此给他固定。在ItemIndex页面声明变量,在记录项列表中传入参数
为啥不用@State???因为这个记录项卡片ItemCard是负责渲染的组件,数量amount和键盘不在一个页面,当键盘修改时,用@State则此页面不能接受做出修改。
5.键盘操作
我们要在点击键盘时改变amount的值,需要在数字键盘NumberKeyboard组件传入一个变量(@Link amount: number//双向绑定)
当用户多次点击键盘时,需要将数字拼接在一起。amount代表最终展示的结果,要有一个单独的变量记录用户所点击的内容
还要判断输入格式是否正确以及小数点保证一位,
//点击数值时
clickNumber(num: string){
// 1.拼接用户输入的内容
let val = this.value + num
// 2.校验输入格式是否正确,判断输入的正误
let firstIndex = val.indexOf('.')//输入内容是否有脚标
let lastIndex = val.lastIndexOf('.')
//小数点后面的位数不能超过两位
if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length - 2)){
// 非法输入
return
}
// 3.将字符串转为数值
let amount = this.parseFloat(val)
// 4.保存
if(amount >= 999.9){//不能超过999,否则强制赋值
this.amount = 999.0
this.value = '999'
}else{
this.amount = amount
this.value = val
}
}
6.媒体查询
定义监听器 利用mediaQuery.matchMediaSync获取,需要传入查询条件。 给监听器绑定回调函数,需要进行判断是否匹配,是则存储到全局监听器。