首页 > 其他分享 >鸿蒙期末大作业——甜点店铺APP(四)精选页面的详细完善

鸿蒙期末大作业——甜点店铺APP(四)精选页面的详细完善

时间:2024-06-18 22:32:29浏览次数:16  
标签:index 鸿蒙 APP number 甜点 索引 数组 列表 监听

一、精选页面的具体分析

        精选页面我们采用一级二级列表联动的效果来展示我们商店的甜品。

        <1> 首先我们需要构造懒加载数据源类型MyDataSource。在types.ets中定义相关的数据类型——用于保存甜品信息的数据类型CustomDataType接口,里面包含四个参数—图片资源、描述、类别以及价格等;ListIndexPosition接口里面包含两个参数—可视区域起点索引和可视区域终点索引。并且定义两个类,BasicDataSource基础数据源类 和 MyDataSource我的数据源类。BasicDataSource基础数据源类实现IDataSource接口,MyDataSource我的数据源类继承BasicDataSource,重写父类的方法。这两个类用于处理数据源相关的逻辑——数据的增删改查以及与数据监听者的交互。

  1. 在BasicDataSource基础数据源类里面实现的具体逻辑有:获得原始数据数组长度、获取指定索引,向数据源处添加listener监听、移除监听,通知监听者数据重新加载、在Index对应索引处添加子组件(即数据)、删除子组件,通知监听者指定索引的数据发生了变化、通知监听者两个索引处的数据发生了交换。
  2. 在MyDataSource我的数据源类需要重写父类的方法,里面实现的具体逻辑有:获得自定义数据数组的长度、获取指定索引数据,在指定索引插入数据并通知监听者、向数组末尾添加数据并通知监听者。

        <2> 在ChoicePage.ets页面,创建两个Scroller对象,一二级列表分别绑定不同的Scroller对象,一级列表(tagList)绑定 classifyScroller对象,二级列表绑定scroller对象;创建MyDataSource对象,用于存储所有商品的数据。并且定义相关的变量——整型的currentTagIndex用于跟踪当前选中的标签索引、布尔类型的isClickTagList用于表示是否点击一级列表、tagList数组用于保存一级列表的标题、records空数组稍后用于记录每个分类下商品的起始位置以及ListIndexPosition接口类型的tagIndexPosition用于存储当前可见的分类列表的起始和结束位置。最后还定义了两个常量——TAG_LIST_LENGTH代表标签列表的长度,CONTENT_PER_TAG代表每个标签下包含的商品数量。

        <3> 在ChoicePage.ets页面定义完相关变量后,我们在aboutToAppear方法中加载甜品的一系列信息。用for循环添加数据并且更新records数组以记录每个分类的商品起始位置。定义findItemIndex方法,其作用是根据给定的索引返回records数组中对应的商品起始位置,以方便在下面的代码中调用;定义findClassIndex方法,其作用是根据给定的商品索引,查找其所属的分类索引。

        <4> 然后在build方法中通过list列表forEach循环将一级列表加载渲染出来,并设置样式——如果当前标签索引等于循环索引,则这个文本组件可见、字体颜色为#ffdbad23、背景颜色为白色,否则不可见、字体颜色为#333333(深灰色)、背景颜色为淡灰色。并且为该列表标签设置触发事件,如果触摸类型是按下则表示点击了一级列表;设置点击事件,将循环索引赋值给当前标签索引,调用findItemIndex方法,传入当前循环索引,获取要滚动到的项目索引,然后使用scroller对象滚动到指定的项目索引。最后调用onScrollIndex监听滚动事件记录当前滚动范围内的起始和结束索引。

        <5> 最后通过list列表LazyforEach循环将二级列表数据加载渲染出来,并分别设置图片和文字的样式。添加触发事件,如果触摸类型是按下,则将isClickTagList置为false。最后调用onScrollIndex监听滚动事件,当滚动到新的索引时,计算当前分类的索引,并检查是否需要更新currentTagIndex,如果需要,会滚动分类列表到相应的索引位置,实现分类和商品列表的同步滚动。

二、效果展示

  我这里将三组甜点信息分为一组。

 

 

三、代码详情

ets/view/ChoicePage.ets

import { CustomDataType, ListIndexPosition, MyDataSource } from '../model/types';

const TAG_LIST_LENGTH=5   //标签列表的长度,即有多少个分类
const CONTENT_PER_TAG=3   //每个标签下包含的商品数量

@Component
export default struct ChoicePage {

  @State message:string = '精选'

  @State currentTagIndex :number = 0;     //用于跟踪当前选中的标签索引
  @State isClickTagList :boolean = false; //是否点击一级列表
  @State contentData : MyDataSource= new MyDataSource();  //用于存储所有商品的数据
  private classifyScroller :Scroller = new Scroller() //分别用于滚动分类标签列表和商品内容列表
  private scroller :Scroller = new Scroller()
  private tagList:Array<string> = ['品牌甄选','现烤面包','鲜果蛋糕','裸蛋糕','慕斯蛋糕']
  private records:Array<number> = [] //一个空的数字数组,稍后用于记录每个分类下商品的起始位置
  private tagIndexPosition:ListIndexPosition = {start:0,end:0} //用于存储当前可见的分类列表的起始和结束位置

  aboutToAppear():void{
    let tempData:Array<CustomDataType> = new Array()
    //品牌甄选
    tempData.push({
      img:$r('app.media.nakedCake1'),
      desc:'芒果裸蛋糕',
      tag:'六寸/动物奶油',
      price:'$77.9'
    });
    tempData.push({
      img:$r('app.media.fruitCake2'),
      desc:'水果蛋糕',
      tag:'四寸/动物奶油',
      price:'$53.6'
    });
    tempData.push({
      img:$r('app.media.mousseCake4'),
      desc:'巧克力慕斯蛋糕',
      tag:'四寸/动物奶油',
      price:'$25.6'
    });
    //现烤面包
    tempData.push({
      img:$r('app.media.bread1'),
      desc:'和风红豆包',
      tag:'红豆夹心',
      price:'$5.6'
    });
    tempData.push({
      img:$r('app.media.bread2'),
      desc:'蛋挞',
      tag:'四寸/动物奶油',
      price:'$2.6'
    });
    tempData.push({
      img:$r('app.media.bread3'),
      desc:'酸奶乳酪包',
      tag:'酸奶夹心',
      price:'$4.9'
    });
    //鲜果蛋糕
    tempData.push({
      img:$r('app.media.fruitCake1'),
      desc:'水果蛋糕',
      tag:'四寸/乳脂奶油',
      price:'$43.6'
    });
    tempData.push({
      img:$r('app.media.fruitCake2'),
      desc:'水果蛋糕',
      tag:'四寸/动物奶油',
      price:'$53.6'
    });
    tempData.push({
      img:$r('app.media.fruitCake3'),
      desc:'水果蛋糕',
      tag:'四寸/动物奶油',
      price:'$57.6'
    });
    //裸蛋糕
    tempData.push({
      img:$r('app.media.nakedCake1'),
      desc:'芒果裸蛋糕',
      tag:'六寸/动物奶油',
      price:'$77.9'
    });
    tempData.push({
      img:$r('app.media.nakedCake2'),
      desc:'无花果裸蛋糕',
      tag:'四寸/动物奶油',
      price:'$65.6'
    });
    tempData.push({
      img:$r('app.media.nakedCake3'),
      desc:'草莓裸蛋糕',
      tag:'四寸/动物奶油',
      price:'$54.7'
    });
    //慕斯蛋糕
    tempData.push({
      img:$r('app.media.mousseCake1'),
      desc:'芒果慕斯蛋糕',
      tag:'两寸/动物奶油',
      price:'$17.9'
    });
    tempData.push({
      img:$r('app.media.mousseCake3'),
      desc:'抹茶慕斯蛋糕',
      tag:'两寸/动物奶油',
      price:'$15.6'
    });
    tempData.push({
      img:$r('app.media.mousseCake4'),
      desc:'巧克力慕斯蛋糕',
      tag:'四寸/动物奶油',
      price:'$25.6'
    });

    //并更新records数组以记录每个分类的商品起始位置
    for(let i = 0;i<TAG_LIST_LENGTH;i++){
      console.log('testTag','for'+(i * CONTENT_PER_TAG))
      this.records.push(i * CONTENT_PER_TAG)
      this.contentData.pushData(tempData)
      console.log('testTag','after'+(i * CONTENT_PER_TAG))
    }
    this.records.push(CONTENT_PER_TAG * TAG_LIST_LENGTH)

  }

  // onIndexChange(){
  //
  // }

  //根据给定的索引返回records数组中对应的商品起始位置
  findItemIndex(index:number){
    return this.records[index]
  }

  //根据给定的商品索引,查找其所属的分类索引。
  //通过遍历records数组,找到第一个满足条件的分类索引并返回。如果找不到符合条件的分类,则返回0。
  findClassIndex(index:number):number{
    let ans = 0;
    for(let i = 0; i < this.records.length;i++){
      if(index>=this.records[i] && index<this.records[i+1]){
        ans = i
        break
      }
    }
    return ans
  }

  build() {
    Column() {

      Text(this.message)
        .margin(10)
        .fontSize(25)
        .width('95%')
        .fontWeight(FontWeight.Bold)

      Row(){
        List({scroller:this.classifyScroller,initialIndex:0}){
          ForEach(this.tagList,(item:string,index:number)=>{
            ListItem(){
              Column(){
                Row(){
                  Text().width(7).height(30).backgroundColor('#ffdbad23')
                    //如果当前标签索引等于循环索引,则这个文本组件可见,否则不可见。
                    .visibility(this.currentTagIndex===index?Visibility.Visible:Visibility.None)
                  Text(item).width('100%').height(70)

                    .fontWeight(FontWeight.Regular)
                    //如果当前标签索引等于循环索引,则字体颜色为#ffdbad23,否则为#333333(深灰色)
                    .fontColor(this.currentTagIndex===index?'#ffdbad23':'#333333')
                    .textAlign(TextAlign.Center)

                }
                //如果当前标签索引等于循环索引,则背景颜色为白色,否则为淡灰色
                .backgroundColor(this.currentTagIndex===index?Color.White:'#f8f8f8')
              }
              .onTouch((event:TouchEvent)=>{
                if(event.type===TouchType.Down){ //如果触摸类型是按下
                  this.isClickTagList = true   //表示点击了一级列表
                }
              })
              .onClick(()=>{
                this.currentTagIndex = index
                let itemIndex :number = this.findItemIndex(index) //调用findItemIndex方法,传入当前循环索引,获取要滚动到的项目索引
                this.scroller.scrollToIndex(itemIndex)  //使用scroller对象滚动到指定的项目索引
              })
            }
          })
        }

        .onScrollIndex((start:number,end:number)=>{
          this.tagIndexPosition = {start,end} //记录当前滚动范围内的起始和结束索引
        })
        .listDirection(Axis.Vertical)
        .scrollBar(BarState.Off)
        .height('100%')
        .width('27%')

        List({scroller:this.scroller,space:1}){
          LazyForEach(this.contentData,(item:CustomDataType)=>{ //数据懒加载
            ListItem(){
              Row({space:10}){
                Image(item.img).aspectRatio(1)  //设置宽高比
                  .height(100).width(100)
                  .backgroundColor(Color.White)
                Column({space:10}){
                  Text(item.desc).fontSize(20)
                  Text(item.tag).fontColor('#909399')
                  Row(){
                    Text(item.price).fontSize(18).fontColor(Color.Red)
                  }
                }.width('100%').alignItems(HorizontalAlign.Start)
                .justifyContent(FlexAlign.SpaceEvenly)
                .height('100%')

              }.height(130).backgroundColor(Color.White)
            }
          })
        }
        .scrollBar(BarState.Off)
        .listDirection(Axis.Vertical)
        .edgeEffect(EdgeEffect.None)
        .flexShrink(1)
        .onTouch((event:TouchEvent) => {
          if(event.type == TouchType.Down){
            this.isClickTagList = false  //?????
          }
        })
        //监听滚动事件,当滚动到新的索引时,计算当前分类的索引,并检查是否需要更新currentTagIndex,
        //如果需要,会滚动分类列表到相应的索引位置,实现分类和商品列表的同步滚动。
        .onScrollIndex((start:number)=>{
          let currentClassIndex = this.findClassIndex(start)
          if(currentClassIndex !== this.currentTagIndex && this.isClickTagList!==true){
            this.currentTagIndex = currentClassIndex
            this.classifyScroller.scrollToIndex(currentClassIndex)
          }
        })
      }.width('100%').layoutWeight(1)
    }
    .padding(15).backgroundColor('#F8F8F8')
  }

}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 ets/model/types.ets

//存放各种数据类型

export interface ItemType{
  id?:number        //可选属性
  title?:string
  img?:string | Resource   //联合类型
}

/*
 *代表自定义类型数据的接口
 * @interface
 *
 * @property {string} desc-描述
 * @property {string} tag-类别
 */
export interface CustomDataType{
  img:Resource
  desc:string    //描述
  tag:string     //类别
  price:string
}

/**
 * 一级列表可视区域的起始索引和终点索引
 * @property {number} start-可视区域起点索引
 * @property {number} end-可视区域终点索引
 */
export interface ListIndexPosition{
  start:number
  end:number
}


/**
 * Basic implementation of IDataSource to handle data listener
 *
 * @class
 * @implements {IDataSource}
 */
class BasicDataSource implements IDataSource{  // 基础数据源类,实现IDataSource接口
  private listeners:DataChangeListener[] = []   // 数据变更监听者数组
  private originDataArray:CustomDataType[] = []  // 原始数据数组

  /**
   * 获取数组长度
   * @returns {number} 返回数组长度
   */
  public totalCount():number{
    // return 0;  //????为什么返回0
    return this.originDataArray.length
  }

  /**
   * 获取指定索引
   * @param {number} index-索引值
   * @returns {CustomDataType} 返回指定索引数据
   */
  public getData(index:number):CustomDataType{
    return this.originDataArray[index]
  }

  /**
   * 为LazyForEach组件向其数据源处添加listener监听
   * @param {DataChangeListener} listener - 监听对象
   */
  registerDataChangeListener(listener:DataChangeListener):void{
    if(this.listeners.indexOf(listener)<0){
      console.info('add listener')   // 日志:添加监听者
      this.listeners.push(listener)  // 将监听者添加到数组中
    }
  }

  /**
   * 为对应的LazyForEach组件在数据源处去除listener监听
   * @param {DataChangeListener} listener - 监听对象
   */
  unregisterDataChangeListener(listener:DataChangeListener):void{
    const pos = this.listeners.indexOf(listener)  // 查找监听者在数组中的位置
    if(pos >= 0){
      console.info('remove listener')
      this.listeners.splice(pos,1)  // 从数组中移除监听者
    }
  }

  //新加的
  // 通知所有监听者数据重新加载
  notifyDataReload():void{
    this.listeners.forEach(listener=>{
      listener.onDataReloaded()  // 调用每个监听者的onDataReloaded方法
    })
  }

  /**
   * 通知LazyForEach组件需要在index对应索引处添加子组件(通知所有监听者在指定索引添加了新数据)
   * @param {number} index - 索引值
   */
  notifyDataAdd(index:number):void{
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  /**
   * 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
   * @param {number} index - 索引值
   */
  notifyDataChange(index:number):void{
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  /**
   * 通知LazyForEach组件需要在index对应索引处删除子组件
   * @param {number} index - 索引值
   */
  notifyDataDelete(index:number):void{
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  /**
   * 通知LazyForEach组件将from索引和to索引处的子组件(数据)进行交换
   * @param {number} from - 起始值
   * @param {number} to - 终点值
   */
  notifyDataMove(from:number,to:number):void{
    this.listeners.forEach(listener => {
      listener.onDataMove(from,to)
    })
  }

}

/**
 * 继承自BasicDataSource的子集,重写了方法
 * 用于提供更具体的数据源操作
 * @class
 * @extents {BasicDataSource}
 */
export class MyDataSource extends BasicDataSource{

  private dataArray: CustomDataType[] = [] //自定义数据数组

  /**
   * 获取数组长度
   * 重写父类的totalCount方法,返回自定义数据数组的长度
   * @returns {number} 返回数组长度
   */
  public totalCount(): number {
    return this.dataArray.length;
  }

  /**
   * 获取指定索引数据
   * 重写父类的getData方法,返回自定义数据数组中指定索引的数据
   * @param {number} index-索引值
   * @returns {CustomDataType} 返回指定索引数据
   */
  public getData(index: number): CustomDataType {
    return this.dataArray[index]
  }

  /**
   * 改变单个数据,在指定索引插入数据并通知监听者
   * @param {number} index - 索引值
   * @param {CustomDataType} data-修改后的值
   */
  public addData(index:number,data:CustomDataType):void{
    this.dataArray.splice(index,0,data)  // 在指定索引插入数据
    this.notifyDataAdd(index);  //通知监听者在该索引添加了数据
  }

  /**
   * 添加数据,向数组末尾添加数据并通知监听者
   * @param {CustomDataType} data-需要添加的数据
   */
  public pushData(data:CustomDataType | CustomDataType[]):void{
    if(Array.isArray(data)){
      this.dataArray.push(...data);//如果数据是数组,将数组中的所有元素添加到数组末尾
    }else{
      this.dataArray.push(data); //如果数据不是数组,直接添加到数组末尾
    }
    this.notifyDataAdd(this.dataArray.length-1)  // 通知监听者在数组末尾添加了数据

  }

}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

四、分享的亮点

        数据懒加载

        性能提升:
        懒加载允许开发者仅在需要时加载数据和渲染视图,而不是一开始就加载全部数据。这意味着应用程序在启动时消耗的资源更少,页面加载速度更快,用户体验更佳。

        效能优化:

        数据懒加载由于只加载可视区域的数据,应用程序的内存占用大幅减少,尤其是处理大量数据时,避免了因一次性加载所有数据而导致的内存溢出问题。

        一级二级列表联动

        导航直观与数据关联:

        二级联动列表提供了清晰的层级结构,用户可以快速定位到他们感兴趣的内容,增强了导航的直观性。并且能够反映出数据间的关联性。

        节省空间:

        通过联动,一级列表不需要显示所有的二级选项,节省了屏幕空间,使界面更加简洁。

        提高效率:

        用户在选择某个一级分类后,二级列表自动更新相关选项,减少了用户操作步骤,提高了选择效率。

标签:index,鸿蒙,APP,number,甜点,索引,数组,列表,监听
From: https://blog.csdn.net/qq_74141710/article/details/139784272

相关文章

  • HarmonyOS开发从入门到跨平台系列:深入了解鸿蒙项目的核心结构
    前言深圳已经发了2024年关于鸿蒙软件生态的规划,如果目标达到,过几年很有可能出现iOSAndroid鸿蒙三足鼎立的情况,因此我们客户端程序员有必要储备一下鸿蒙知识。接下来我将分几篇文章介绍鸿蒙开发的入门、实战和跨平台相关知识,今天这篇文章作为开篇,主要介绍一下鸿蒙开......
  • 【鸿蒙开发教程】详解HarmonyOS Next UI开发技巧
    前言根据研究机构CounterpointResearch发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式的......
  • 【鸿蒙实战开发案例】如何构建一个HarmonyOS应用工程
    前言随着春节假期结束各行各业复产复工,一年一度的春招也持续火热起来。最近,有招聘平台发布了《2024年春招市场行情周报(第一期)》。总体来说今年的就业市场还是人才饱和的状态,竞争会比较激烈。但是,通过报告我们也能看到让人眼前一亮的信息,比如华为鸿蒙系统对应的人才市场就......
  • 【鸿蒙开发教程】HarmonyOS NEXT对于游戏类App的基础支持
    前言根据研究机构CounterpointResearch发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式的......
  • 【鸿蒙教程】华为HarmonyOS NEXT 应用开发 实现日常提醒应用
    前言根据研究机构CounterpointResearch发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统。随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式......
  • 基于SpringBoot+Vue+uniapp的高校实验室信息化综合管理平台建设的详细设计和实现(源码
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue+uniapp的电商购物网站的详细设计和实现(源码+lw+部署文档+讲解等)
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue+uniapp的酒店预订管理系统的详细设计和实现(源码+lw+部署文档+讲
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • Mybatis的Mapper中方法入参什么时候加@Param
    参数情况:一个基本类型--不需要多个基本类型--需要一个对象 --不需要多个对象  --不需要一个集合  --不需要 单个基本类型不用加@ParamMapper接口方法:voiddeleteUserById(LonguserId);XML中的SQL语句:<deleteid="deleteUserById"parameterType=......
  • 学习记录APP
    1、用户注册:用户注册信息包括用户ID(学号)、用户名(姓名),手机号码,用户单位(班级),用户班级四项基本信息,用户第一次注册后,用户姓名不用每次输入。2、设定每周学习目标:每周一设置学习目标。例如:本周完成数据库连接等等具体任务目标。3、每日学习记录:内容包括:开始时间、结束时间,每日学习......