一、精选页面的具体分析
精选页面我们采用一级二级列表联动的效果来展示我们商店的甜品。
<1> 首先我们需要构造懒加载数据源类型MyDataSource。在types.ets中定义相关的数据类型——用于保存甜品信息的数据类型CustomDataType接口,里面包含四个参数—图片资源、描述、类别以及价格等;ListIndexPosition接口里面包含两个参数—可视区域起点索引和可视区域终点索引。并且定义两个类,BasicDataSource基础数据源类 和 MyDataSource我的数据源类。BasicDataSource基础数据源类实现IDataSource接口,MyDataSource我的数据源类继承BasicDataSource,重写父类的方法。这两个类用于处理数据源相关的逻辑——数据的增删改查以及与数据监听者的交互。
- 在BasicDataSource基础数据源类里面实现的具体逻辑有:获得原始数据数组长度、获取指定索引,向数据源处添加listener监听、移除监听,通知监听者数据重新加载、在Index对应索引处添加子组件(即数据)、删除子组件,通知监听者指定索引的数据发生了变化、通知监听者两个索引处的数据发生了交换。
- 在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')
}
}
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) // 通知监听者在数组末尾添加了数据
}
}
四、分享的亮点
数据懒加载
性能提升:
懒加载允许开发者仅在需要时加载数据和渲染视图,而不是一开始就加载全部数据。这意味着应用程序在启动时消耗的资源更少,页面加载速度更快,用户体验更佳。
效能优化:
数据懒加载由于只加载可视区域的数据,应用程序的内存占用大幅减少,尤其是处理大量数据时,避免了因一次性加载所有数据而导致的内存溢出问题。
一级二级列表联动
导航直观与数据关联:
二级联动列表提供了清晰的层级结构,用户可以快速定位到他们感兴趣的内容,增强了导航的直观性。并且能够反映出数据间的关联性。
节省空间:
通过联动,一级列表不需要显示所有的二级选项,节省了屏幕空间,使界面更加简洁。
提高效率:
用户在选择某个一级分类后,二级列表自动更新相关选项,减少了用户操作步骤,提高了选择效率。
标签:index,鸿蒙,APP,number,甜点,索引,数组,列表,监听 From: https://blog.csdn.net/qq_74141710/article/details/139784272