【鸿蒙实战开发】基于List和Scroller由简单到复杂列表布局开发实践

208 篇文章 0 订阅
208 篇文章 0 订阅

场景描述

在多列表页面开发中,数据展示往往有联动关系,

场景一:单列表布局多长列表页面,如门户首页、商城首页

场景二:双列表滚动联动,如城市选择

场景三:多列表滚动横向纵向联动,如汽车参数对比,股票信息列表

方案描述

场景一:

单列表布局多长列表页面,如门户首页、商城首页效果图

0030086000558681258.20240513140636.01679575068906274591823953568442.gif

方案

运用List组件作为整个首页长列表的容器,通过ListItem对不同模块进行定制。

  1. Refresh包裹List实现下拉刷新
  2. ListItem-0嵌套Swiper实现轮播图。
  3. ListItem-1嵌套Grid实现快捷入口。
  4. ListItem-2嵌套Column实现秒杀
  5. ListItemGroup实现商品分类列表
  6. 最底部ListItem实现触底自动加载

核心代码

build() {

  Column() {

    // 搜索框 置顶

    if (this.searchSticky) {

      this.searchBarBuilder()

    }

    // 下拉刷新组件

    Refresh({ refreshing: $$this.isRefreshing }) {

      // List组件作为长列表布局

      List({ space: 10 }) {

        // 搜索框跟随

        if (!this.searchSticky) {

          ListItem() {

            this.searchBarBuilder()

          }

        }

        // ListItem 自定义Swiper轮播图模块

        ListItem() {

          this.bannerBuilder()

        }

        // ListItem 自定义Grid快接入口模块

        ListItem() {

          this.quickBuilder()

        }

        // ListItem 自定义Column秒杀模块

        ListItem() {

          this.flashBuilder()

        }

        // ListItemGroup 商品分类列表

        this.productsBuilder()

        // 最后ListItem 自定义触底加载更多

        ListItem() {

          this.footerLoadingBuilder()

        }.height(50).width('100%').backgroundColor(0xeeeeee)

      }

      .sticky(StickyStyle.Header)

      .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })

      .height('100%')

      .layoutWeight(2)

      // List组件触底模拟网络请求

      .onReachEnd(() => {

        if (this.productsArray.length >= 20) {

          this.noMoreData = true

          return

        }

        setTimeout(() => {

          this.productsArray.push('商品' + (this.productsArray.length + 1))

        }, 2000)

      })

    }

    // 下拉刷新模拟网络请求

    .onRefreshing(() => {

      setTimeout(() => {

        this.productsArray = ['商品1', '商品2', '商品3', '商品4', '商品5']

        this.noMoreData = false

        this.isRefreshing = !this.isRefreshing

      }, 2000)

    })

    .layoutWeight(1)

    .width('95%')

  }

}

场景二:

双列表滚动同向联动,如城市选择

效果图

0030086000558681258.20240513140654.54387922019090088896212774675799.gif

方案

整体运用Stack组件(List组件+List组件)布局,左List作为城市列表,右List快捷导航列表,通过ListItem对对应数据进行渲染。

1.左List用ListItemGroup对城市数据进行分组

2.右List用ListItem对首字母进行渲染

3.通过右List首字母导航点击可以切换左List滚动到对应分组

核心代码

@State private selectGroupIndex: number = -1 //导航栏选中index

private cityScroller: ListScroller = new ListScroller() // 城市列表Scoller控制器

private navgationScroller: ListScroller = new ListScroller() // 导航列表Scoller控制器

private isClickScroll:boolean = false // 导航列表点击标记为true,城市列表触摸滚动为false

build() {

  Stack({alignContent : Alignment.End}) {

    this.cityList()

    this.navigationList()

  }

  .width('100%')

  .height('100%').backgroundColor(0xFFFFFF)

}

// 城市列表

@Builder

cityList() {

  List({ scroller: this.cityScroller }) {

    ListItemGroup({ header: this.itemHead('当前城市') }) {

      ListItem() {

        Text(this.currentCity)

        ......

      }

    }

    ListItemGroup({ header: this.itemHead('热门城市') }) {

      ForEach(this.hotCities, (hotCity: string) => {

        ListItem() {

          Text(hotCity)

          ......

        }

      })

    }

    // A~Z城市分组

    ForEach(this.groupNameList, (item: string) => {

      ListItemGroup({ header: this.itemHead(item) }) {

        ForEach(this.getCitiesWithGroupName(item), (cityItem: City) => {

          ListItem() {

            Text(cityItem.city)

            ......

          }

        }, (item: City) => item.city)

      }

    })

  }

  .width('100%')

  .height('100%')

  .scrollBar(BarState.Off)

  .sticky(StickyStyle.Header)

  .onTouch(()=>{

    // 城市列表触摸滚动,isClickScroll=false,防止滚动过程中与导航列表触发滚动冲突

    this.isClickScroll = false

  })

  .onScrollIndex((start: number, end: number, center: number)=>{

    // 通过selectGroupIndex状态变量与start联动控制导航列表选中状态

    if(!this.isClickScroll)

      this.selectGroupIndex = start - 2

  })

}

// 导航列表@Builder

navigationList() {

  List({scroller:this.cityScroller1}) {

    ForEach(this.groupNameList, (item: string, index: number) => {

      ListItem() {

        Text(item)

        ......

        .onClick(() => {

          // 导航列表选中isClickScroll=true,防止与城市列表滚动过程中带动导航列表状态变化

          this.isClickScroll = true

          this.selectGroupIndex = index

          // 通过导航选中selectGroupIndex与Scroller控制城市列表滚动到对应位置

          this.cityScroller.scrollToIndex(index + 2, true, ScrollAlign.START)

        })

      }

    }, (item: string) => item)

  }

  .listDirection(Axis.Vertical)

  .backgroundColor(Color.Transparent)

  .width('10%')

}

场景三:

多列表滚动横向纵向联动,如汽车参数对比,股票信息列表

效果图

0030086000558681258.20240513140707.52524758274206999927872748174354.gif

方案

1.Column组件(Row组件1 + Row组件2)整体布局上下两部分,Row1代表上部分,Row2代表下部分

2.上部分Row组件1(Column组件+ List组件0),Column组件用来布局固定信息,List组件0用来渲染底部内容区域表头,与下部分List组件3+进行联动滚动,如股票参数,车型列表。

3.下部分Row组件2(List组件1 + Scroll组件(List组件2)),List组件1渲染每条信息的头部,内部用ListItemGroup进行分组渲染,竖向滚动;Scroll组件用来包裹详细内容数据List组件2,与List组件1进行竖向滚动联动;List组件2用来渲染内容数据,与List组件0进行横向滚动联动。

4.List组件2作为内容数据容器,ListItem中嵌套List组件3+横向滚动,联动List组件0进行横向滚动。

核心代码

export class ShowData {

  sticky?:string

  sub?: string[];

  scrollerArray?: Scroller[] = [];

}

@State remainOffset: number = 0 // 内容行在横向滚动时回调的offset

private bottomRightScroller: Scroller = new Scroller() //下部分左侧标题List(行标题)

private bottomLeftScroller: Scroller = new Scroller() // 下部分右侧内容List(内容)

private topRightScroller: Scroller = new Scroller() // 上部分右侧类型List(列标题)

// 整体布局

build() {

  Column() {

    // 上部分

    this.topFixed()

    // 下部分

    Row() {

      this.leftList()

      this.rightList()

      Line().height('100%').width(0.5).backgroundColor('#EEEEEE').position({ x: LeftItemWidth })

    }

    .justifyContent(FlexAlign.Start)

    .alignItems(VerticalAlign.Top)

  }.height('100%')

  .justifyContent(FlexAlign.Start)

  .alignItems(HorizontalAlign.Start)

}

// 上部分整体Row(Column + List)

@Builder

topFixed() {

  Row() {

    // 上部分左侧固定信息

    Column() {

      .......

    }

    .......

    .padding(10)

    // 分割线

    Line().height(100).width(0.5).backgroundColor(0xeeeeee)

    // 上部分右侧车型横向滚动列表

    List({ scroller: this.topRightScroller/* 绑定Scroller控制器与其他控制器联动*/ }) {

      ForEach(this.topRightArr, (item: string, index: number) => {

        ListItem() {

          .......

        }

      }, (item: string) => item)

    }

    .......

    .onScrollFrameBegin((offset: number, state: ScrollState) => {

      // 关键联动,通过对象保存的Scroller控制器数组遍历保持offset同步

      this.dataSource.getAllData().forEach(showData => {

        showData.scrollerArray!.forEach(scroller => {

          scroller.scrollTo({ xOffset: this.topRightScroller.currentOffset().xOffset + offset, yOffset: 0 })

        })

      })

      return { offsetRemain: offset }

    })

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

}

// 下部分右侧内容显示区域纵向List(ListItem(List))

@Builder

rightList() {

  List({ initialIndex: 0, scroller: this.bottomRightScroller }) {

    // 通过LazyForEach加载每一行

    LazyForEach(this.dataSource, (item: ShowData, index: number) => {

      ListItemGroup({ header: this.rightStickyHeader(index) }) {

        ForEach(item.sub, (subItem: string, index1: number) => {

          // 自定义ListItem中包含横向滚动List

          ItemComponent({

            scroller: item.scrollerArray![index1],

            scrollCallBack: (value) => {

              // value为子List横向滚动onScrollFrameBegin回传offset,在手指拖动时保持联动一致

              // 顶部车型List跟随联动

              this.topRightScroller.scrollTo({ xOffset: value, yOffset: 0 })

              // 通过对象保存的Scroller数组跟随保持联动

              this.dataSource.getAllData().forEach(showData => {

                showData.scrollerArray!.forEach(scroller => {

                  if (scroller != item.scrollerArray![index1]) {

                    scroller.scrollTo({ xOffset: value, yOffset: 0 })

                  }

                })

              })

            },

            remainOffsetCallBack: (value) => {

              // 滚动过程中回传保持同步的offset值

              this.remainOffset = value

            }

          })

        }, (item: string) => item)

      }

    }, (item: ShowData, index: number) => item.sticky! + index)

  }

  .......

  .onScrollFrameBegin((offset: number, state: ScrollState) => {

    // 内容List纵向滚动带动左侧标题List跟随滚动

    this.bottomLeftScroller.scrollTo({

      xOffset: 0,

      yOffset: this.bottomRightScroller.currentOffset().yOffset + offset,

      animation: false

    })

    return { offsetRemain: offset }

  })

    .onScroll(() => {

      // 内容List纵向滚动过程中,每一行中子List的Scroller滚动到remainOffset与已显示的行位置保持一致

      this.dataSource.getAllData().forEach(showData => {

        showData.scrollerArray!.forEach(scroller => {

          scroller.scrollTo({ xOffset: this.remainOffset, yOffset: 0 })

        })

      })

    })

  .......

}

@Component

struct ItemComponent {

  private arr: string[] = [

    '1', '2', '3', '4', '5', '6', '7', '8']

  private dataSource = new CommonDataSource<string>()

  private scroller?: Scroller = undefined // 内容行List绑定Scroller

  private scrollCallBack?: (param: number) => void // 触摸滚动过程中回调实时offset

  private remainOffsetCallBack?: (param: number) => void // 滚动时回调同步offset

  aboutToAppear(): void {

    this.dataSource.setData(this.arr)

  }

  // 下部分参数列表每行数据List

  @Builder

  RightSingleLineList() {

    List({ scroller: this.scroller }) {

      LazyForEach(this.dataSource, (item: string, index: number) => {

        ListItem() {

          ......

        }

        .width(RightItemWidth)

      }, (item: string) => item)

    }

    ......

    .onScroll(() => {

      // 通过callBack回调行在横向滚动时,Scroller当前的offset

      if (this.remainOffsetCallBack)

        this.remainOffsetCallBack(this.scroller!.currentOffset().xOffset)

    })

      .onScrollFrameBegin((offset: number, state: ScrollState) => {

        // 触摸滚动实时跟随回调

        if (this.scrollCallBack) {

          this.scrollCallBack(this.scroller!.currentOffset().xOffset + offset)

        }

        return { offsetRemain: offset }

      })

  }

  build() {

    Column() {

      this.RightSingleLineList()

      Line().width("100%").height(0.5).backgroundColor(0xeeeeee)

    }.height(ItemHeight)

  }

}

其他常见问题

1,滑动卡顿

LazyForEach数据懒加载:数据量大的List尽量用LazyForEach加载数据,可明显优化性能,经过测试列数为100以上,LazyForEach也无明显卡顿。

2,错位分析

查看左右List行高是否一致,ListItemGroup高度是否一致;onScrollFrameBegin联动回调中是否跟随保持一致。

3,嵌套滚动

如需要外层附加其他滚动,可运用嵌套属性.nestedScroll进行联动。

写在最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。

希望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

如果你是一名有经验的资深Android移动开发、Java开发、前端开发、对鸿蒙感兴趣以及转行人员,可以直接领取这份资料

请点击→纯血版全套鸿蒙HarmonyOS学习文档

鸿蒙(HarmonyOS NEXT)5.0最新学习路线

在这里插入图片描述

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

《鸿蒙 (HarmonyOS)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V3.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档

在这里插入图片描述

  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值