HarmonyOS鸿蒙Next中List组件onScroll事件:下拉回弹后图标状态异常,收起状态无法恢复展开

HarmonyOS鸿蒙Next中List组件onScroll事件:下拉回弹后图标状态异常,收起状态无法恢复展开 实现了一个首页,顶部有8个功能图标,正常状态下分两行显示。当List向上滚动到一定距离时,图标会收起成一行显示,这个功能在手动慢滚动时工作正常。但是,在以下特定操作序列下会出现异常:

  1. 初始状态(图标为两行展开状态)
  2. 用户下拉List到一定程度
  3. 松手让List自动回弹到顶部

问题:此时图标突然变成收起状态(一行),且无法通过后续的上下滚动恢复到展开状态,此时列表距离图标还有好大的距离

技术实现

  • 使用List组件的onScroll监听滚动偏移量
  • 通过累计滚动距离totalScrollY控制图标展开/收起状态
  • 设置了滚动阈值(100px收起,30px展开)
  • 使用@State isIconRowCollapsed控制UI状态
@Entry
@Component
export struct Index {
  [@State](/user/State) isIconRowCollapsed: boolean = false
  [@State](/user/State) totalScrollY: number = 0 // 累计滚动距离
  [@State](/user/State) scrollProgress: number = 0 // 滚动进度,0-1之间
  private scrollThreshold: number = 100 // 滑动阈值,超过这个距离开始收起图标
  private maxScrollDistance: number = 300 // 最大滚动距离,用于计算进度
  // 统一使用同一个图标资源
  [@State](/user/State) unifiedIcon: Resource = $r('app.media.sports')

  // 假数据列表(不请求网络)
  [@State](/user/State) mockReminderCount: number = 5

  aboutToAppear() {
    // 初始状态重置
    this.totalScrollY = 0
    this.isIconRowCollapsed = false
    this.scrollProgress = 0
  }

  build() {
    Column() {
      // 顶部标题
      Row(){
        Text('示例页')
          .fontSize(20)
          .align(Alignment.Center)
      }.width('100%')
      .height(46)
      .justifyContent(FlexAlign.Center)
      .alignItems(VerticalAlign.Center)
      .backgroundColor(Color.White)

      this.buildStickyIconHeader()

      // 下方内容区域 - 使用List包含整个滚动内容
      List({space: 10}) {
        ForEach(new Array(this.mockReminderCount).fill(0), (_: number, index: number) => {
          if(index == 0){
            ListItem(){
              SportList().width('94%')
                .margin({left: '3%', right: '3%'})
            }.margin({top: this.isIconRowCollapsed?80: 10})
          }else{
            ListItem(){
              SportList()
                .width('94%')
                .margin({left: '3%', right: '3%'})
            }.borderRadius(16)
          }
        }, (_: number, idx: number) => idx.toString())
      }
      .layoutWeight(1)
      .width('100%')
      .backgroundColor('#F5F5F5')
      .padding({left: 0, right: 0, top: 0, bottom: 0})
      .sticky(StickyStyle.Header)
      .edgeEffect(EdgeEffect.Spring)
      .friction(0.6)
      .onScroll((scrollOffset: number, scrollState: ScrollState) => {
        this.handleScroll(scrollOffset, scrollState)
      })
      .onScrollStop(() => {
        if (this.totalScrollY <= 0 && this.isIconRowCollapsed) {
          this.totalScrollY = 0;
          this.isIconRowCollapsed = false;
          this.scrollProgress = 0;
        }
      })
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  buildHealthModule(label: string, icon: Resource, isCollapsed: boolean = false) {
    Column() {
      Image(icon)
        .width(isCollapsed ? 28 : 40)
        .height(isCollapsed ? 28 : 40)
        .fillColor("#ff94d69f")
        .scale({ x: isCollapsed ? 0.8 : 1.0, y: isCollapsed ? 0.8 : 1.0 })
        .animation({ duration: 300, curve: Curve.EaseInOut })
      if (!isCollapsed) {
        Text(label)
          .fontSize(14)
          .margin({top: 8})
          .textAlign(TextAlign.Center)
          .opacity(1)
      }
    }
    .width(isCollapsed ? '12.5%' : '25%')
    .height(isCollapsed ? 50 : 'auto')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .opacity(isCollapsed ? 0.9 : 1.0)
    .animation({ duration: 300, curve: Curve.EaseInOut })
  }

  @Builder
  buildStickyIconHeader() {

    Column() {
      if (this.isIconRowCollapsed) {
        Row() {
          this.buildHealthModule('模块1', this.unifiedIcon, true)
          this.buildHealthModule('模块2', this.unifiedIcon, true)
          this.buildHealthModule('模块3', this.unifiedIcon, true)
          this.buildHealthModule('模块4', this.unifiedIcon, true)
          this.buildHealthModule('模块5', this.unifiedIcon, true)
          this.buildHealthModule('模块6', this.unifiedIcon, true)
          this.buildHealthModule('模块7', this.unifiedIcon, true)
          this.buildHealthModule('模块8', this.unifiedIcon, true)
        }
        .width('100%')
        .height(60)
        .justifyContent(FlexAlign.SpaceEvenly)
        .padding({left: 8, right: 8, top: 8, bottom: 8})
        .backgroundColor($r('sys.color.point_color_checked'))
        .borderRadius(12)
        .margin({left: 16, right: 16})
        .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })
      } else {
        Column() {
          Row() {
            this.buildHealthModule('模块1', this.unifiedIcon)
            this.buildHealthModule('模块2', this.unifiedIcon)
            this.buildHealthModule('模块3', this.unifiedIcon)
            this.buildHealthModule('模块4', this.unifiedIcon)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
          .padding({left:16, right: 16, top: 16, bottom: 0})

          Row() {
            this.buildHealthModule('模块5', this.unifiedIcon)
            this.buildHealthModule('模块6', this.unifiedIcon)
            this.buildHealthModule('模块7', this.unifiedIcon)
            this.buildHealthModule('模块8', this.unifiedIcon)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
          .padding({left:16, right: 16, top: 16, bottom: 16})
        }
        .width('100%')
        .backgroundColor('#F5F5F5')
      }
    }
    .width('100%')
    .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
  }

  private handleScroll(scrollOffset: number, scrollState: ScrollState) {
    let newTotalScrollY = this.totalScrollY + scrollOffset;
    let shouldForceReset = false;
    if (newTotalScrollY < 0) {
      newTotalScrollY = 0;
      shouldForceReset = true;
    }

    const progress = Math.min(newTotalScrollY / this.maxScrollDistance, 1);
    this.scrollProgress = progress;

    const collapseThreshold = this.scrollThreshold;
    const expandThreshold = collapseThreshold * 0.3;

    if (newTotalScrollY !== this.totalScrollY || shouldForceReset) {
      this.totalScrollY = newTotalScrollY;
      if (shouldForceReset) {
        this.isIconRowCollapsed = false;
      } else if (this.totalScrollY > collapseThreshold && !this.isIconRowCollapsed) {
        this.isIconRowCollapsed = true;
      } else if (this.totalScrollY <= expandThreshold && this.isIconRowCollapsed) {
        this.isIconRowCollapsed = false;
      }
    }
  }
}

@Component
struct SportList {

  build() {
    Column() {
      // 主要信息区域
      Column() {
        Text(){
          Span('山川的呼唤,不止在顶峰的壮丽,更在于每一次迈开脚步的勇气和底气。').fontColor('#2C3E50').fontSize(16)
            .decoration({ type: TextDecorationType.None, color: Color.Black })
        }.fontSize(16)
        .fontColor('#2C3E50')
        .margin({ bottom: 8 })
        .width('100%')

        Text(){
          Span('山川的呼唤,不止在顶峰的壮丽,更在于每一次迈开脚步的勇气和底气。').fontSize(16).fontColor('#34495E').fontWeight(500)
            .decoration({ type: TextDecorationType.None, color: Color.Black })
        }.fontSize(16)
        .fontColor('#34495E')
        .margin({ bottom: 16 })
        .width('100%')

        Row() {
          Button('不再提醒', { type: ButtonType.Capsule })
            .fontSize(14)
            .fontColor($r('sys.color.comp_background_list_card'))
            .backgroundColor('#95A5A6')
            .width('45%')
            .borderRadius(20)
            .shadow({ radius: 4, color: '#1A95A5A6', offsetX: 0, offsetY: 2 })
            .onClick(() => {})

          Button('保存数据', { type: ButtonType.Capsule })
            .fontSize(14)
            .fontColor($r('sys.color.comp_background_list_card'))
            .backgroundColor('#2ECC71')
            .width('45%')
            .borderRadius(20)
            .shadow({ radius: 4, color: '#1A2ECC71', offsetX: 0, offsetY: 2 })
            .onClick(() => {})
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 16 })
      .backgroundColor('#F8F9FA')
      .borderRadius({ topLeft: 16, topRight: 16, bottomLeft: 0, bottomRight: 0 })
    }
    .width('100%')
    .backgroundColor('#F8F9FA')
    .borderRadius(16)
    .border({ width: 1, color: '#E8EAED' })
    .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })
    .clip(true)
  }
}

更多关于HarmonyOS鸿蒙Next中List组件onScroll事件:下拉回弹后图标状态异常,收起状态无法恢复展开的实战教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

代码中主要有以下几个问题:

  1. onScroll回调中,需要判断滚动状态,List的回弹状态不响应handleScroll方法,同时,scrollOffset=0时也不需要响应;

  2. onScrollStop回调中,totalScrollY需要在判断后置为0。

  3. handleScroll方法中collapseThreshold和expandThreshold直接给初始化值,具体的值可以根据需要调整。

  4. handleScroll方法中增加判断,即回弹状态时不响应展开,折叠操作,

if (scrollState != ScrollState.Fling) 
  1. handleScroll方法中,在设置完展开/折叠后,重置totalScrollY=0;
import { hilog } from "@kit.PerformanceAnalysisKit"


@Entry
@Component
export struct Page0204193935352918340 {
  @State isIconRowCollapsed: boolean = false
  @State totalScrollY: number = 0 // 累计滚动距离
  @State scrollProgress: number = 0 // 滚动进度,0-1之间
  private scrollThreshold: number = 100 // 滑动阈值,超过这个距离开始收起图标
  private maxScrollDistance: number = 300 // 最大滚动距离,用于计算进度
  // 统一使用同一个图标资源
  @State unifiedIcon: Resource = $r('app.media.startIcon')
  // 假数据列表(不请求网络)
  @State mockReminderCount: number = 5

  aboutToAppear() {
    // 初始状态重置
    this.totalScrollY = 0
    this.isIconRowCollapsed = false
    this.scrollProgress = 0
  }

  build() {
    Column() {
      // 顶部标题
      Row() {
        Text('示例页')
          .fontSize(20)
          .align(Alignment.Center)
      }
      .width('100%')
      .height(46)
      .justifyContent(FlexAlign.Center)
      .alignItems(VerticalAlign.Center)
      .backgroundColor(Color.White)

      this.buildStickyIconHeader()

      // 下方内容区域 - 使用List包含整个滚动内容
      List({ space: 10 }) {
        ForEach(new Array(this.mockReminderCount).fill(0), (_: number, index: number) => {
          if (index == 0) {
            ListItem() {
              SportList().width('94%')
                .margin({ left: '3%', right: '3%' })
            }.margin({ top: this.isIconRowCollapsed ? 80 : 10 })
          } else {
            ListItem() {
              SportList()
                .width('94%')
                .margin({ left: '3%', right: '3%' })
            }.borderRadius(16)
          }
        }, (_: number, idx: number) => idx.toString())
      }
      .layoutWeight(1)
      .width('100%')
      .backgroundColor('#F5F5F5')
      .padding({
        left: 0,
        right: 0,
        top: 0,
        bottom: 0
      })
      .edgeEffect(EdgeEffect.Spring)
      .friction(0.6)
      .onScroll((scrollOffset: number, scrollState: ScrollState) => {
        if (scrollOffset != 0) {
          this.handleScroll(scrollOffset, scrollState)
        }
      })
      .onScrollStop(() => {
        if (this.totalScrollY <= 0 && this.isIconRowCollapsed) {
          this.isIconRowCollapsed = false;
          this.scrollProgress = 0;
        }
        this.totalScrollY = 0;
      })
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  buildHealthModule(label: string, icon: Resource, isCollapsed: boolean = false) {
    Column() {
      Image(icon)
        .width(isCollapsed ? 28 : 40)
        .height(isCollapsed ? 28 : 40)
        .fillColor("#ff94d69f")
        .scale({ x: isCollapsed ? 0.8 : 1.0, y: isCollapsed ? 0.8 : 1.0 })
        .animation({ duration: 300, curve: Curve.EaseInOut })
      if (!isCollapsed) {
        Text(label)
          .fontSize(14)
          .margin({ top: 8 })
          .textAlign(TextAlign.Center)
          .opacity(1)
      }
    }
    .width(isCollapsed ? '12.5%' : '25%')
    .height(isCollapsed ? 50 : 'auto')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .opacity(isCollapsed ? 0.9 : 1.0)
    .animation({ duration: 300, curve: Curve.EaseInOut })
  }

  @Builder
  buildStickyIconHeader() {

    Column() {
      if (this.isIconRowCollapsed) {
        Row() {
          this.buildHealthModule('模块1', this.unifiedIcon, true)
          this.buildHealthModule('模块2', this.unifiedIcon, true)
          this.buildHealthModule('模块3', this.unifiedIcon, true)
          this.buildHealthModule('模块4', this.unifiedIcon, true)
          this.buildHealthModule('模块5', this.unifiedIcon, true)
          this.buildHealthModule('模块6', this.unifiedIcon, true)
          this.buildHealthModule('模块7', this.unifiedIcon, true)
          this.buildHealthModule('模块8', this.unifiedIcon, true)
        }
        .width('100%')
        .height(60)
        .justifyContent(FlexAlign.SpaceEvenly)
        .padding({
          left: 8,
          right: 8,
          top: 8,
          bottom: 8
        })
        .backgroundColor($r('sys.color.point_color_checked'))
        .borderRadius(12)
        .margin({ left: 16, right: 16 })
        .shadow({
          radius: 8,
          color: '#1A000000',
          offsetX: 0,
          offsetY: 2
        })
      } else {
        Column() {
          Row() {
            this.buildHealthModule('模块1', this.unifiedIcon)
            this.buildHealthModule('模块2', this.unifiedIcon)
            this.buildHealthModule('模块3', this.unifiedIcon)
            this.buildHealthModule('模块4', this.unifiedIcon)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
          .padding({
            left: 16,
            right: 16,
            top: 16,
            bottom: 0
          })

          Row() {
            this.buildHealthModule('模块5', this.unifiedIcon)
            this.buildHealthModule('模块6', this.unifiedIcon)
            this.buildHealthModule('模块7', this.unifiedIcon)
            this.buildHealthModule('模块8', this.unifiedIcon)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
          .padding({
            left: 16,
            right: 16,
            top: 16,
            bottom: 16
          })
        }
        .width('100%')
        .backgroundColor('#F5F5F5')
      }
    }
    .width('100%')
    .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
  }

  private handleScroll(scrollOffset: number, scrollState: ScrollState) {
    let newTotalScrollY = this.totalScrollY + scrollOffset;
    let shouldForceReset = false;
    if (newTotalScrollY < 0) {
      newTotalScrollY = 0;
      shouldForceReset = true;
    }

    const progress = Math.min(newTotalScrollY / this.maxScrollDistance, 1);
    this.scrollProgress = progress;

    const collapseThreshold = 0.5;
    const expandThreshold = 0.5;

    if (newTotalScrollY !== this.totalScrollY || shouldForceReset) {
      this.totalScrollY = newTotalScrollY;
      if (scrollState != ScrollState.Fling) {
        if (shouldForceReset) {
          this.isIconRowCollapsed = false;
        } else if (this.totalScrollY > 0 && Math.abs(this.totalScrollY) > collapseThreshold &&
          !this.isIconRowCollapsed) {
          this.isIconRowCollapsed = true;
          this.totalScrollY = 0;
        } else if (this.totalScrollY < 0 && this.totalScrollY < expandThreshold && this.isIconRowCollapsed) {
          this.isIconRowCollapsed = false;
          this.totalScrollY = 0;
        }
      }
    }
  }
}

@Component
struct SportList {
  build() {
    Column() {
      // 主要信息区域
      Column() {
        Text() {
          Span('山川的呼唤,不止在顶峰的壮丽,更在于每一次迈开脚步的勇气和底气。').fontColor('#2C3E50').fontSize(16)
            .decoration({ type: TextDecorationType.None, color: Color.Black })
        }.fontSize(16)
        .fontColor('#2C3E50')
        .margin({ bottom: 8 })
        .width('100%')

        Text() {
          Span('山川的呼唤,不止在顶峰的壮丽,更在于每一次迈开脚步的勇气和底气。')
            .fontSize(16)
            .fontColor('#34495E')
            .fontWeight(500)
            .decoration({ type: TextDecorationType.None, color: Color.Black })
        }.fontSize(16)
        .fontColor('#34495E')
        .margin({ bottom: 16 })
        .width('100%')

        Row() {
          Button('不再提醒', { type: ButtonType.Capsule })
            .fontSize(14)
            .fontColor($r('sys.color.comp_background_list_card'))
            .backgroundColor('#95A5A6')
            .width('45%')
            .borderRadius(20)
            .shadow({
              radius: 4,
              color: '#1A95A5A6',
              offsetX: 0,
              offsetY: 2
            })
            .onClick(() => {
            })

          Button('保存数据', { type: ButtonType.Capsule })
            .fontSize(14)
            .fontColor($r('sys.color.comp_background_list_card'))
            .backgroundColor('#2ECC71')
            .width('45%')
            .borderRadius(20)
            .shadow({
              radius: 4,
              color: '#1A2ECC71',
              offsetX: 0,
              offsetY: 2
            })
            .onClick(() => {
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%')
      .padding({
        left: 16,
        right: 16,
        top: 16,
        bottom: 16
      })
      .backgroundColor('#F8F9FA')
      .borderRadius({
        topLeft: 16,
        topRight: 16,
        bottomLeft: 0,
        bottomRight: 0
      })
    }
    .width('100%')
    .backgroundColor('#F8F9FA')
    .borderRadius(16)
    .border({ width: 1, color: '#E8EAED' })
    .shadow({
      radius: 8,
      color: '#1A000000',
      offsetX: 0,
      offsetY: 2
    })
    .clip(true)
  }
}

更多关于HarmonyOS鸿蒙Next中List组件onScroll事件:下拉回弹后图标状态异常,收起状态无法恢复展开的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


用户手册

快速入门

本手册将帮助您快速了解系统的基本操作流程。

登录系统

  1. 打开浏览器,输入系统网址
  2. 在登录页面输入您的用户名和密码
  3. 点击"登录"按钮进入系统

主要功能

  • 仪表盘:查看系统概览和关键指标
  • 用户管理:管理用户账户和权限
  • 数据报表:生成和查看各类业务报表
  • 系统设置:配置系统参数和选项

详细操作指南

创建新项目

  1. 在左侧菜单栏点击"项目管理"
  2. 点击右上角的"新建项目"按钮
  3. 填写项目信息:
    • 项目名称
    • 项目描述
    • 开始日期
    • 结束日期
  4. 点击"保存"完成创建

用户权限设置

系统支持以下权限级别:

  • 管理员:拥有所有权限
  • 编辑者:可以创建和编辑内容
  • 查看者:只能查看内容
  • 访客:受限访问权限

常见问题

忘记密码怎么办?

如果您忘记了密码,请点击登录页面的"忘记密码"链接,按照提示重置密码。

如何导出数据?

在数据报表页面,选择需要导出的数据范围,然后点击"导出"按钮,选择导出格式(Excel/PDF)。

技术支持

如果您在使用过程中遇到任何问题,请联系技术支持团队:

  • 电话:400-123-4567
  • 邮箱:support@example.com
  • 在线客服:工作日 9:00-18:00

最后更新:2024年1月

在HarmonyOS Next中,List组件的onScroll事件在下拉回弹时可能导致图标状态异常,收起后无法恢复展开。这通常与滚动位置监听和状态更新时机有关。需检查onScroll回调中的状态管理逻辑,确保在滚动结束或回弹完成后正确重置图标状态。可结合scrollEdge事件或使用动画监听器同步状态变更,避免异步更新导致的显示不一致。

问题出现在滚动状态处理逻辑中。当下拉回弹时,onScroll回调会收到多个负偏移量,导致totalScrollY累加值异常。

主要问题在于handleScroll方法中的滚动距离累加逻辑:

let newTotalScrollY = this.totalScrollY + scrollOffset;

当下拉回弹时,系统会连续触发多个负值的scrollOffset,这些值被累加到totalScrollY中,虽然最终通过newTotalScrollY < 0检查重置为0,但在重置前的中间状态可能已经触发了收起条件。

建议修改为直接使用绝对滚动位置而非累加值。可以监听scrollState参数获取当前滚动状态,或者使用Scroll组件的onScrollFrameBegin事件来获取更精确的滚动控制。

另外,onScrollStop中的重置逻辑应该与滚动处理保持一致,避免状态不一致。当前实现中,滚动过程中的状态切换与停止后的重置逻辑存在竞态条件。

回到顶部