界面实例:
代码
食物列表页:
ItemIndex.ets
import router from '@ohos.router'
import { CommonConstants } from '../common/constants/CommonConstants'
import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel'
import RecordService from '../RecordService/RecordService'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NumberKeyboard from '../view/item/NumberKeyboard'
import RecordItem from '../viewmodel/RecordItem'
import RecordType from '../viewmodel/RecordType'
@Extend(Button) function panelButtonStyle(){
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
}
@Entry
@Component
struct ItemIndex {
@State amount: number = 1
@State value: string = ''
@State showPanel: boolean = false
@State item: RecordItem = null
@State type: RecordType = RecordTypes[0]
@State isFood: boolean = true
onPanelShow(item: RecordItem) {
this.amount = 1
this.value = ''
this.item = item
this.showPanel = true
}
onPageShow(){
// 1.获取跳转时的参数
let params: any = router.getParams()
// 2.获取点击的饮食记录类型
this.type = params.type
this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
}
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({ showPanel: this.onPanelShow.bind(this), isFood: this.isFood })
.layoutWeight(1)
// 3.底部面板
Panel(this.showPanel) {
// 3.1.顶部日期
ItemPanelHeader()
// 3.2.记录项卡片
if(this.item){
ItemCard({amount: this.amount, item: $item})
}
// 3.3.数字键盘
NumberKeyboard({amount: $amount, value: $value})
// 3.4.按钮
this.PanelButton()
}
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask($r('app.color.light_gray'))
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
@Builder PanelButton(){
Row({space: CommonConstants.SPACE_6}){
Button('取消')
.panelButtonStyle()
.backgroundColor($r('app.color.light_gray'))
.onClick(() => this.showPanel = false)
Button('提交')
.panelButtonStyle()
.backgroundColor($r('app.color.primary_color'))
.onClick(() => {
// 1.持久化保存
RecordService.insert(this.type.id, this.item.id, this.amount)
.then(() => {
// 2.关闭弹窗
this.showPanel = false
})
})
}
.margin({top: 10})
}
@Builder Header() {
Row() {
Image($r('app.media.ic_public_back'))
.width(24)
.onClick(() => router.back())
Blank()
Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
食物列表页包括头部导航,列表,底部面板三部分,底部面板又包括顶部日期,记录项卡片,数字键盘,按钮四个部分,底部面板的出现是一个弹窗,点击规定的地方才会出现应该出现的弹窗。
列表项:
ItemList.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordItem from '../../viewmodel/RecordItem'
import ItemModel from '../../model/ItemModel'
import ItemCategory from '../../viewmodel/ItemCategory'
import GroupInfo from '../../viewmodel/GroupInfo'
@Component
export default struct ItemList {
showPanel:(item:RecordItem)=>void
@Prop isFood:boolean
build() {
Tabs()//是个容器
{
TabContent() {
this.TabContentBuilder(ItemModel.list(this.isFood))//从里面查
}
.tabBar('全部')
ForEach(
ItemModel.listItemGroupByCategory(this.isFood),
(group: GroupInfo<ItemCategory, RecordItem>) => {
TabContent() {
this.TabContentBuilder(group.items)//group里面已经有数据了不用再查了
}
.tabBar(group.type.name)
})
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
.barMode(BarMode.Scrollable)//Scrollable Tabbar的枚举布局 占多少位置就是多少位置 超出位置可以进行滚动
}
//封装
@Builder TabContentBuilder(items:RecordItem[]){
List({space:CommonConstants.SPACE_10})
{
ForEach(items,(item:RecordItem)=>{
ListItem(){
Row({space:CommonConstants.SPACE_6}){
Image(item.image).width(50)
Column({space:CommonConstants.SPACE_4})
{
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))//itemCard的父组件 item先传给它 再去传给ItemCard
})
}
.width('100%')
.height('100%')
}
}
panel面板:
ItemPanelHeader.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemPanelHeader {
build() {
Row() {
Text('1月17日 早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor(Color.Black)
}
}
}
记录项卡片:
ItemCard.ets
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)
}
}
}
数字键盘:
NumberKeyboard.ets
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')//3列 1:1:1 行的话自己根据空间进行排列了
.columnsGap(8)
.rowsGap(8)
.padding(8)
.margin({top:16})
}
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)
{
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)
}
}
食物页面总结
1.Panel:可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中
2.在列表项中使用Divider
添加分隔线,增强视觉分隔效果。在NumberKeyboard
中,使用不同的方法处理数字输入和删除操作,提供用户输入反馈。
3.在ItemList
组件中,根据分类对项目进行分组,并使用Tabs
组件展示不同分类的标签页。
4.使用List
和Grid
组件来渲染列表和网格布局。使用ForEach
方法遍历数据集合,为每项数据创建对应的UI元素。
5.使用@Component
和@Prop
注解定义组件和属性,实现组件化开发。
多段部署
要实现的效果:
代码:
BreakpointConstants.ets:
import BreakpointType from '../bean/BreanpointType';
export default class BreakpointConstants {
/**
* 小屏幕设备的 Breakpoints 标记.
*/
static readonly BREAKPOINT_SM: string = 'sm';
/**
* 中等屏幕设备的 Breakpoints 标记.
*/
static readonly BREAKPOINT_MD: string = 'md';
/**
* 大屏幕设备的 Breakpoints 标记.
*/
static readonly BREAKPOINT_LG: string = 'lg';
/**
* 当前设备的 breakpoints 存储key
*/
static readonly CURRENT_BREAKPOINT: string = 'currentBreakpoint';
/**
* 小屏幕设备宽度范围.
*/
static readonly RANGE_SM: string = '(320vp<=width<600vp)';
/**
* 中屏幕设备宽度范围.
*/
static readonly RANGE_MD: string = '(600vp<=width<840vp)';
/**
* 大屏幕设备宽度范围.
*/
static readonly RANGE_LG: string = '(840vp<=width)';
//用的比较多,定义为常量
static readonly BAR_POSITION: BreakpointType<BarPosition> = new BreakpointType({
sm: BarPosition.End,
md: BarPosition.Start,
lg: BarPosition.Start,
})
}
BreakpointSystem.ets:
import mediaQuery from '@ohos.mediaquery'
import BreakpointConstants from '../constants/BreakpointConstants'
export default class BreakpointSystem{
//定义监听器
//ets才能导入
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))
}
}
Index:
private breakpointSystem:BreakpointSystem=new BreakpointSystem()
//storageProp必须初始化
@StorageProp('currentBreakpoint') currentBreakpoint: string=BreakpointConstants.BREAKPOINT_SM
aboutToAppear(){
//多段部署
this.breakpointSystem.register()//完成回调函数注册
}
aboutToDisappear(){
this.breakpointSystem.unregister()
}
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]
}
}
Tabs:
.vertical(new BreakpointType({
sm:false,
md:true,
lg:true
}).getValue(this.currentBreakpoint))//布局模式
swiper:
.displayCount(new BreakpointType({
sm: 1,
md: 1,
lg: 2
}).getValue(this.currentBreakpoint))
运行截图:
多段部署总结:
实现了用户多种设备的选择,实现了响应式布局,我们可以将这个程序应用到折叠手机,平板上。