鸿蒙Next实现仿电商首页菜单横向滚动高度自适应 原创 精华

auhgnixgnahz
发布于 2025-8-14 17:45
浏览
1收藏

天下产品一大”借鉴“,可以发现某宝、东、鱼等APP的首页菜单,横向滚动第一页是一行展示5个完整的菜单和一小部分第二页的一个菜单,第二页是三行展示菜单,左右滑动可动态调整菜单栏的高度。
看一下模仿实现效果:
鸿蒙Next实现仿电商首页菜单横向滚动高度自适应-鸿蒙开发者社区

实现思路:
1.首先想到使用Swiper+Grid实现,通过设置Swiper的nextMargin可以显示出第二页的一部分,但是由于设置了该属性,滑动过程中第一页和第二页的Grid间隔是一样的,和目标不一致,如果滑动时修改,连贯性不好,因此该方案被否定
2.使用横向的List+Grid实现,一个ListItem+一个Grid实现,可以设置第一个ListItem的宽度不充满屏幕,即可露出第二Item
3.观察发现菜单高度最小和最大即两个Grid的高度,因此只要根据滑动距离,动态设置菜单的高度
4.计算横向滚动偏移量总和:当前水平滑动偏移 + 新增偏移量
5.动态变化的高度 = (横向滚动偏移/ 第一个ListItem的宽度) * 菜单高度最大和最小的高度差
6.根据计算结果更新容器高度(当前高度 = 最小高度 + 动态变化高度)
实现源码:

let tabs:string[]=['首页','推荐','其他'];
let numberList: number[][] = [
  [1, 2, 3, 4, 5],
  [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
];
//实现根据内容高度随滑动距离变动的效果
@Entry
@ComponentV2
struct Index {
  private listScroller: ListScroller = new ListScroller()
  @Local currentIndex: number = 0
  @Local containerWidth:number = 0 ; // 横向List容器宽度
  @Local containerHeight: number = 0  // 横向List容器的高度  根据滑动动态改变
  @Local containerMaxHeight: number = 0  //第二页 三行内容的高度
  @Local containerMinHeight: number = 0  //第一页 一行内容的高度
  build() {
    Column() {
      //顶部Tab标签
      Row(){
        Tabs(){
          ForEach(tabs,(item: string, index:number)=>{
            TabContent() {
            }.tabBar(item)
          })
        }
        .width('40%')
        .height('auto')
      }.width('100%')
     //滚动菜单
      Scroll(){
        List({ scroller: this.listScroller }){
          ListItem(){
            Child({numberList:numberList[0],childGridHeight:(contentHeight:number)=>{
              // 单行高度
              this.containerMinHeight = contentHeight
              this.containerHeight = contentHeight
              console.log('单行高度:'+contentHeight)
            }})
          }.width('90%')
          ListItem(){
            Child({numberList:numberList[1],childGridHeight:(contentHeight:number)=>{
              // 三行高度
              this.containerMaxHeight =contentHeight
              console.log('最高高度:'+contentHeight)
            }})
          }.width('100%')
        }
        .scrollBar(BarState.Off)
        .listDirection(Axis.Horizontal)
        .scrollSnapAlign(ScrollSnapAlign.START)
        .edgeEffect(EdgeEffect.None)
        .onAreaChange( (oldValue: Area, newValue: Area) =>{
          if (!this.containerWidth) {
            this.containerWidth = Number(newValue.width)
            // this.containerHeight = Number(newValue.height)
          }
        })
        .onWillScroll((scrollOffset: number, scrollState: ScrollState, scrollSource: ScrollSource) => {
          console.log('当前滚动位置:'+this.listScroller.currentOffset().xOffset)
          // 计算横向滚动偏移量总和:当前水平滑动偏移 + 新增偏移量
          let offsetX = this.listScroller.currentOffset().xOffset + scrollOffset
          console.log('list滚动偏移量:'+offsetX)
          if (offsetX > 0) {
            //动态变化的高度 = (横向滚动偏移/ 第一个ListItem的宽度) * 菜单高度最大和最小的高度差
            let h = offsetX / (this.containerWidth*0.9) * (this.containerMaxHeight - this.containerMinHeight)
            // 根据计算结果更新容器高度(当前高度 = 最小高度 + 动态变化高度)
            this.containerHeight = this.containerMinHeight + h
          } else {
            this.containerHeight = this.containerMinHeight
          }
          console.log('list动态计算的高度:'+ this.containerHeight )
        })
        .onScrollIndex((start, end) => {
          this.currentIndex = start
        })
      }.height(this.containerHeight || 'auto')
      //菜单页指示器
      Row({ space: 5 }) {
        ForEach(new Array(2).fill(0), (item: number, index: number) => {
          Circle()
            .width(5)
            .height(5)
            .fill(this.currentIndex === index ? '#0A59F7' : '#660a59f7')
            .visibility(index === 0 ? Visibility.Visible : Visibility.None)
            .onClick(() => {
              this.listScroller.scrollToIndex(index, true)
              this.currentIndex = index
            })
          Image($r('app.media.slide_fill'))
            .borderRadius(5)
            .width(15)
            .height(5)
            .fillColor(this.currentIndex === index ? '#0A59F7' : '#660a59f7')
            .visibility(index === 1 ? Visibility.Visible : Visibility.None)
            .onClick(() => {
              this.listScroller.scrollToIndex(index, true)
              this.currentIndex = index
            })
        })
      }
      .margin({ top: 12, bottom: 16 })
      .height(8)
      //其他内容
      Scroll(){
        Column(){
          Image($r('app.media.img_swiper_heght_test')).width('100%').objectFit(ImageFit.Fill)
        }
      }
    }
  }
}

@ComponentV2
struct Child {
  @Require @Param numberList: number[];
  @Event childGridHeight:(contentHeight:number)=>void
  build() {
    Grid() {
      ForEach(this.numberList, (item: number) => {
        GridItem() {
          Column() {
            Image($r('app.media.startIcon'))
              .width(40)
              .height(40)
            Text('Menu' + item)
              .fontSize(15)
          }
        }
      })
    }
    .rowsGap(20)
    .scrollBar(BarState.Off)
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
    .onAreaChange( (oldValue: Area, newValue: Area) =>{
      //保证只赋值一次
      if (oldValue.height==0) {
        this.childGridHeight(Number(newValue.height))
      }
    })
  }
}

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
标签
2
收藏 1
回复
举报
回复
    相关推荐