HarmonyOS鸿蒙Next中List组件onScroll事件:下拉回弹后图标状态异常,收起状态无法恢复展开
HarmonyOS鸿蒙Next中List组件onScroll事件:下拉回弹后图标状态异常,收起状态无法恢复展开 实现了一个首页,顶部有8个功能图标,正常状态下分两行显示。当List向上滚动到一定距离时,图标会收起成一行显示,这个功能在手动慢滚动时工作正常。但是,在以下特定操作序列下会出现异常:
- 初始状态(图标为两行展开状态)
- 用户下拉List到一定程度
- 松手让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
代码中主要有以下几个问题:
-
onScroll回调中,需要判断滚动状态,List的回弹状态不响应handleScroll方法,同时,scrollOffset=0时也不需要响应;
-
onScrollStop回调中,totalScrollY需要在判断后置为0。
-
handleScroll方法中collapseThreshold和expandThreshold直接给初始化值,具体的值可以根据需要调整。
-
handleScroll方法中增加判断,即回弹状态时不响应展开,折叠操作,
if (scrollState != ScrollState.Fling)
- 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
用户手册
快速入门
本手册将帮助您快速了解系统的基本操作流程。
登录系统
- 打开浏览器,输入系统网址
- 在登录页面输入您的用户名和密码
- 点击"登录"按钮进入系统
主要功能
- 仪表盘:查看系统概览和关键指标
- 用户管理:管理用户账户和权限
- 数据报表:生成和查看各类业务报表
- 系统设置:配置系统参数和选项
详细操作指南
创建新项目
- 在左侧菜单栏点击"项目管理"
- 点击右上角的"新建项目"按钮
- 填写项目信息:
- 项目名称
- 项目描述
- 开始日期
- 结束日期
- 点击"保存"完成创建
用户权限设置
系统支持以下权限级别:
- 管理员:拥有所有权限
- 编辑者:可以创建和编辑内容
- 查看者:只能查看内容
- 访客:受限访问权限
常见问题
忘记密码怎么办?
如果您忘记了密码,请点击登录页面的"忘记密码"链接,按照提示重置密码。
如何导出数据?
在数据报表页面,选择需要导出的数据范围,然后点击"导出"按钮,选择导出格式(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
中的重置逻辑应该与滚动处理保持一致,避免状态不一致。当前实现中,滚动过程中的状态切换与停止后的重置逻辑存在竞态条件。