文章目录
-
- 每日一句正能量
- 一、前言
- [二、Scroll 组件基础解析](#二、Scroll 组件基础解析)
-
- [2.1 原生组件能力边界](#2.1 原生组件能力边界)
- [2.2 原生使用方式的局限](#2.2 原生使用方式的局限)
- 三、封装设计思路与架构
-
- [3.1 设计目标](#3.1 设计目标)
- [3.2 整体架构](#3.2 整体架构)
- [3.3 核心属性与事件全景](#3.3 核心属性与事件全景)
- 四、核心代码实现
-
- [4.1 数据模型与适配器(DataAdapter)](#4.1 数据模型与适配器(DataAdapter))
- [4.2 下拉刷新管理器(PullRefreshManager)](#4.2 下拉刷新管理器(PullRefreshManager))
- [4.3 上拉加载管理器(LoadMoreManager)](#4.3 上拉加载管理器(LoadMoreManager))
- [4.4 核心封装组件(SmartScroll)](#4.4 核心封装组件(SmartScroll))
- [4.5 嵌套滚动协调(NestedScrollHandler)](#4.5 嵌套滚动协调(NestedScrollHandler))
- 五、下拉刷新状态机
-
- [5.1 状态机设计](#5.1 状态机设计)
- [5.2 关键设计决策](#5.2 关键设计决策)
- [六、原生 vs 封装后对比](#六、原生 vs 封装后对比)
- 七、实战案例:新闻资讯列表集成
-
- [7.1 运行效果说明](#7.1 运行效果说明)
- 八、性能优化与最佳实践
-
- [8.1 性能优化策略](#8.1 性能优化策略)
-
- [8.1.1 渲染优化](#8.1.1 渲染优化)
- [8.1.2 内存优化](#8.1.2 内存优化)
- [8.1.3 滚动优化](#8.1.3 滚动优化)
- [8.2 工程化最佳实践](#8.2 工程化最佳实践)
- 九、扩展能力展望
- 十、总结

每日一句正能量
有时走点弯路,才知道直线多可贵。
很多时候我们痛恨"浪费的时间",但如果没有绕行、试错、迷失,直线对你而言只是概念,不是体会。弯路让你"知道"直线可贵------这个"知道"是身体里长出来的,不是别人告诉你的。
一、前言
在移动应用开发中,滚动容器是最基础也是最核心的 UI 组件之一。无论是新闻资讯的无限列表、电商平台的商品瀑布流、社交应用的聊天界面,还是长表单的数据录入------几乎所有涉及内容展示的场景都离不开滚动能力。HarmonyOS ArkUI 框架提供了 Scroll、List、Grid、WaterFlow 等多种滚动容器组件,以及 Refresh 刷新组件,为开发者构建流畅的滚动体验提供了坚实基础。
然而,在真实的企业级项目中,直接使用原生滚动组件往往面临以下痛点:
- 下拉刷新实现复杂:需手动处理触摸事件、偏移量计算、状态切换动画,代码量大且易出错;
- 上拉加载逻辑分散:需监听滚动位置、判断触底条件、管理加载状态,与业务代码高度耦合;
- 大数据量性能差 :
ForEach全量渲染在数据量大时导致内存占用高、滑动卡顿; - 嵌套滚动冲突:当页面存在多个滚动容器(如顶部 Tabs + 底部列表)时,滚动事件容易冲突;
- 多端适配困难:手机、平板、折叠屏等不同形态下,滚动策略需要差异化处理。
本文将从组件封装架构设计 出发,深入讲解如何基于 ArkUI 的 Scroll、List、Refresh 构建一套企业级的 SmartScroll 封装方案,涵盖下拉刷新、上拉加载、懒加载优化、嵌套滚动协调、滚动监听等完整能力,并提供可直接落地的工程代码。
二、Scroll 组件基础解析
2.1 原生组件能力边界
ArkUI 提供了多种滚动容器组件,各自适用于不同场景:
| 组件 | 适用场景 | 核心能力 |
|---|---|---|
Scroll |
通用滚动容器 | 支持横向/纵向滚动、滚动条、边缘效果 |
List |
线性列表 | 支持分割线、吸顶、侧滑删除、懒加载 |
Grid |
网格布局 | 支持行列模板、跨行跨列、懒加载 |
WaterFlow |
瀑布流 | 支持不规则高度、多列自适应、懒加载 |
Refresh |
下拉刷新 | 内置刷新动画、状态控制、阻力系数 |
Scroll 组件是最基础的滚动容器,其构造函数接收可选的 Scroller 控制器:
typescript
Scroll(scroller?: Scroller)
核心属性包括:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scrollable |
ScrollDirection |
Vertical | 滚动方向(Vertical/Horizontal/None) |
scrollBar |
BarState |
Auto | 滚动条状态(On/Off/Auto) |
edgeEffect |
EdgeEffect |
Spring | 边缘效果(Spring/Fade/None) |
friction |
number |
- | 摩擦系数,值越大滚动越慢 |
nestedScroll |
NestedScrollOptions |
- | 嵌套滚动配置 |
核心事件包括 onScroll(滚动时)、onScrollEdge(到达边缘)、onScrollStart/Stop(滚动起止)、onReachStart/End(到达起止位置)等。
2.2 原生使用方式的局限
以下是一段典型的原生下拉刷新实现代码:
typescript
@Entry
@Component
struct NewsPage {
@State list: NewsItem[] = []
@State offsetY: number = 0
@State pullRefreshText: string = '下拉刷新'
@State isRefreshing: boolean = false
private downY: number = 0
private scroller: Scroller = new Scroller()
// 需手动处理触摸事件
handleTouch(event: TouchEvent) {
switch (event.type) {
case TouchType.Down:
this.downY = event.touches[0].y
break
case TouchType.Move:
this.offsetY = event.touches[0].y - this.downY
if (this.offsetY > 70) {
this.pullRefreshText = '松开刷新'
}
break
case TouchType.Up:
if (this.offsetY > 70) {
this.isRefreshing = true
this.pullRefreshText = '正在刷新'
// 发起网络请求...
}
break
}
}
build() {
Column() {
// 下拉刷新头
Text(this.pullRefreshText)
.height(this.offsetY)
.width('100%')
.textAlign(TextAlign.Center)
Scroll(this.scroller) {
List() {
ForEach(this.list, (item) => {
ListItem() { /* ... */ }
})
}
}
.onTouch((event) => this.handleTouch(event))
}
}
}
可以看到,原生方式要求开发者手动处理触摸事件、偏移量计算、状态切换、动画过渡等复杂逻辑,代码量大且容易出错。
三、封装设计思路与架构
3.1 设计目标
SmartScroll 封装方案的设计目标如下:
| 目标维度 | 具体要求 |
|---|---|
| 易用性 | 声明式配置,一行代码即可实现下拉刷新+上拉加载 |
| 高性能 | 基于 LazyForEach 实现懒加载,大数据量不卡顿 |
| 智能化 | 自动处理触摸事件、状态切换、动画过渡 |
| 扩展性 | 支持自定义刷新头、加载尾、空状态、错误状态 |
| 多端适配 | 自动适配手机、平板、折叠屏等不同形态 |
3.2 整体架构
封装组件采用四层架构设计:

各层职责如下:
- 业务应用层 :各业务页面通过
SmartScroll组件传入数据与配置,接收刷新/加载回调; - 封装组件层 :
SmartScroll作为核心封装组件,内部聚合PullRefreshManager(下拉刷新管理)、LoadMoreManager(上拉加载管理)、ScrollController(滚动控制)、ScrollListener(滚动监听)、DataAdapter(数据适配)五大子模块; - 原生组件层 :向下调用 ArkUI 的
Scroll、List、Refresh、Scroller、WaterFlow等原生能力; - 系统能力层 :依赖 HarmonyOS 的
SystemCapability.ArkUI.ArkUI.Full、图形引擎渲染管线及触摸系统TouchEvent。
3.3 核心属性与事件全景

四、核心代码实现
4.1 数据模型与适配器(DataAdapter)
首先定义列表项的数据模型和数据源:
typescript
// models/ScrollItem.ets
export interface ScrollItem {
id: string | number
title: string
subtitle?: string
imageUrl?: string | Resource
timestamp?: number
extra?: Record<string, unknown>
}
// 数据源基础类,支持 LazyForEach
export class ScrollDataSource implements IDataSource {
private dataArray: ScrollItem[] = []
private listeners: DataChangeListener[] = []
constructor(data: ScrollItem[]) {
this.dataArray = data
}
totalCount(): number {
return this.dataArray.length
}
getData(index: number): ScrollItem {
return this.dataArray[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) {
this.listeners.splice(pos, 1)
}
}
setData(data: ScrollItem[]): void {
this.dataArray = data
this.notifyDataReload()
}
appendData(data: ScrollItem[]): void {
const startIndex = this.dataArray.length
this.dataArray.push(...data)
for (let i = 0; i < data.length; i++) {
this.notifyDataAdd(startIndex + i)
}
}
private notifyDataReload(): void {
this.listeners.forEach(listener => listener.onDataReloaded())
}
private notifyDataAdd(index: number): void {
this.listeners.forEach(listener => listener.onDataAdd(index))
}
}
4.2 下拉刷新管理器(PullRefreshManager)
下拉刷新管理器负责处理下拉刷新的完整生命周期:
typescript
// managers/PullRefreshManager.ets
export enum RefreshState {
IDLE = 'idle',
PULLING = 'pulling',
RELEASE = 'release',
REFRESHING = 'refreshing',
SUCCESS = 'success',
FAILED = 'failed'
}
export class PullRefreshManager {
@State refreshState: RefreshState = RefreshState.IDLE
@State offsetY: number = 0
private refreshHeight: number = 70
private downY: number = 0
private onRefreshCallback: (() => Promise<void>) | null = null
configure(options: { refreshHeight?: number; onRefresh: () => Promise<void> }): void {
this.refreshHeight = options.refreshHeight ?? 70
this.onRefreshCallback = options.onRefresh
}
onTouchDown(y: number): void {
if (this.refreshState === RefreshState.REFRESHING) return
this.downY = y
this.refreshState = RefreshState.PULLING
}
onTouchMove(y: number, scrollY: number): void {
if (this.refreshState === RefreshState.REFRESHING) return
if (scrollY > 0) return
const delta = y - this.downY
if (delta > 0) {
this.offsetY = Math.min(delta * 0.6, this.refreshHeight * 1.5)
if (delta >= this.refreshHeight) {
this.refreshState = RefreshState.RELEASE
} else {
this.refreshState = RefreshState.PULLING
}
}
}
async onTouchUp(): Promise<void> {
if (this.refreshState === RefreshState.REFRESHING) return
if (this.refreshState === RefreshState.RELEASE) {
this.refreshState = RefreshState.REFRESHING
this.offsetY = this.refreshHeight
try {
if (this.onRefreshCallback) {
await this.onRefreshCallback()
}
this.refreshState = RefreshState.SUCCESS
} catch (e) {
this.refreshState = RefreshState.FAILED
console.error('[PullRefreshManager] Refresh failed:', e)
}
setTimeout(() => {
animateTo({
duration: 300,
onFinish: () => {
this.refreshState = RefreshState.IDLE
this.offsetY = 0
}
}, () => {
this.offsetY = 0
})
}, 500)
} else {
animateTo({ duration: 200 }, () => {
this.offsetY = 0
this.refreshState = RefreshState.IDLE
})
}
}
getState(): RefreshState {
return this.refreshState
}
getOffsetY(): number {
return this.offsetY
}
}
4.3 上拉加载管理器(LoadMoreManager)
上拉加载管理器负责处理列表触底加载更多:
typescript
// managers/LoadMoreManager.ets
export enum LoadMoreState {
IDLE = 'idle',
LOADING = 'loading',
NO_MORE = 'noMore',
FAILED = 'failed'
}
export class LoadMoreManager {
@State loadState: LoadMoreState = LoadMoreState.IDLE
private threshold: number = 100
private isLoading: boolean = false
private hasMore: boolean = true
private onLoadMoreCallback: (() => Promise<boolean>) | null = null
configure(options: {
threshold?: number
onLoadMore: () => Promise<boolean>
}): void {
this.threshold = options.threshold ?? 100
this.onLoadMoreCallback = options.onLoadMore
}
onScroll(scrollOffset: number, contentHeight: number, containerHeight: number): void {
if (!this.hasMore || this.isLoading) return
const distanceToBottom = contentHeight - scrollOffset - containerHeight
if (distanceToBottom < this.threshold) {
this.triggerLoadMore()
}
}
private async triggerLoadMore(): Promise<void> {
if (this.isLoading || !this.onLoadMoreCallback) return
this.isLoading = true
this.loadState = LoadMoreState.LOADING
try {
this.hasMore = await this.onLoadMoreCallback()
this.loadState = this.hasMore ? LoadMoreState.IDLE : LoadMoreState.NO_MORE
} catch (e) {
this.loadState = LoadMoreState.FAILED
console.error('[LoadMoreManager] Load more failed:', e)
} finally {
this.isLoading = false
}
}
setHasMore(hasMore: boolean): void {
this.hasMore = hasMore
this.loadState = hasMore ? LoadMoreState.IDLE : LoadMoreState.NO_MORE
}
getState(): LoadMoreState {
return this.loadState
}
}
4.4 核心封装组件(SmartScroll)
typescript
// components/SmartScroll.ets
import { ScrollItem, ScrollDataSource } from '../models/ScrollItem'
import { PullRefreshManager, RefreshState } from '../managers/PullRefreshManager'
import { LoadMoreManager, LoadMoreState } from '../managers/LoadMoreManager'
export interface SmartScrollOptions {
data: ScrollItem[]
enablePullRefresh?: boolean
enableLoadMore?: boolean
refreshHeight?: number
loadMoreThreshold?: number
scrollBar?: BarState
edgeEffect?: EdgeEffect
onRefresh?: () => Promise<void>
onLoadMore?: () => Promise<boolean>
itemBuilder: (item: ScrollItem, index: number) => void
refreshBuilder?: (state: RefreshState, offset: number) => void
loadMoreBuilder?: (state: LoadMoreState) => void
emptyBuilder?: () => void
errorBuilder?: () => void
}
@Component
export struct SmartScroll {
@Prop options: SmartScrollOptions
@State private dataSource: ScrollDataSource = new ScrollDataSource([])
@State private refreshState: RefreshState = RefreshState.IDLE
@State private loadState: LoadMoreState = LoadMoreState.IDLE
@State private offsetY: number = 0
private scroller: Scroller = new Scroller()
private pullRefreshManager: PullRefreshManager = new PullRefreshManager()
private loadMoreManager: LoadMoreManager = new LoadMoreManager()
aboutToAppear(): void {
this.dataSource = new ScrollDataSource(this.options.data)
if (this.options.enablePullRefresh && this.options.onRefresh) {
this.pullRefreshManager.configure({
refreshHeight: this.options.refreshHeight,
onRefresh: this.options.onRefresh
})
}
if (this.options.enableLoadMore && this.options.onLoadMore) {
this.loadMoreManager.configure({
threshold: this.options.loadMoreThreshold,
onLoadMore: this.options.onLoadMore
})
}
}
private handleTouch(event: TouchEvent): void {
if (!this.options.enablePullRefresh) return
switch (event.type) {
case TouchType.Down:
this.pullRefreshManager.onTouchDown(event.touches[0].y)
break
case TouchType.Move:
const scrollOffset = this.scroller.currentOffset().yOffset
this.pullRefreshManager.onTouchMove(event.touches[0].y, scrollOffset)
this.refreshState = this.pullRefreshManager.getState()
this.offsetY = this.pullRefreshManager.getOffsetY()
break
case TouchType.Up:
case TouchType.Cancel:
this.pullRefreshManager.onTouchUp().then(() => {
this.refreshState = this.pullRefreshManager.getState()
this.offsetY = this.pullRefreshManager.getOffsetY()
})
break
}
}
private handleScroll(): void {
if (!this.options.enableLoadMore) return
const offset = this.scroller.currentOffset().yOffset
this.loadMoreManager.onScroll(offset, 2000, 800)
this.loadState = this.loadMoreManager.getState()
}
@Builder
defaultRefreshBuilder() {
Column() {
if (this.refreshState === RefreshState.PULLING) {
Text('下拉刷新').fontSize(14).fontColor('#999999')
} else if (this.refreshState === RefreshState.RELEASE) {
Text('松开刷新').fontSize(14).fontColor('#0A59F7')
} else if (this.refreshState === RefreshState.REFRESHING) {
Row({ space: 8 }) {
LoadingProgress().width(20).height(20).color('#0A59F7')
Text('正在刷新...').fontSize(14).fontColor('#0A59F7')
}
} else if (this.refreshState === RefreshState.SUCCESS) {
Text('刷新成功').fontSize(14).fontColor('#2E7D32')
}
}
.width('100%')
.height(this.offsetY)
.justifyContent(FlexAlign.Center)
}
@Builder
defaultLoadMoreBuilder() {
Column() {
if (this.loadState === LoadMoreState.LOADING) {
Row({ space: 8 }) {
LoadingProgress().width(20).height(20).color('#0A59F7')
Text('加载更多...').fontSize(14).fontColor('#999999')
}
} else if (this.loadState === LoadMoreState.NO_MORE) {
Text('没有更多数据了').fontSize(12).fontColor('#CCCCCC').padding(16)
} else if (this.loadState === LoadMoreState.FAILED) {
Text('加载失败,点击重试').fontSize(14).fontColor('#FF3B30').padding(16)
.onClick(() => {
this.loadMoreManager.setHasMore(true)
this.handleScroll()
})
}
}
.width('100%')
.padding(12)
.justifyContent(FlexAlign.Center)
}
build() {
Column() {
if (this.options.enablePullRefresh) {
if (this.options.refreshBuilder) {
this.options.refreshBuilder(this.refreshState, this.offsetY)
} else {
this.defaultRefreshBuilder()
}
}
Scroll(this.scroller) {
List() {
LazyForEach(this.dataSource, (item: ScrollItem, index: number) => {
ListItem() {
this.options.itemBuilder(item, index)
}
}, (item: ScrollItem) => JSON.stringify(item.id))
if (this.options.enableLoadMore) {
ListItem() {
if (this.options.loadMoreBuilder) {
this.options.loadMoreBuilder(this.loadState)
} else {
this.defaultLoadMoreBuilder()
}
}
}
}
.divider({ strokeWidth: 1, color: '#F0F0F0' })
.edgeEffect(EdgeEffect.None)
.width('100%')
}
.scrollBar(this.options.scrollBar ?? BarState.Auto)
.edgeEffect(this.options.edgeEffect ?? EdgeEffect.Spring)
.width('100%')
.layoutWeight(1)
.onTouch((event) => this.handleTouch(event))
.onScroll(() => this.handleScroll())
}
.width('100%')
.height('100%')
}
}
4.5 嵌套滚动协调(NestedScrollHandler)
处理多个滚动容器嵌套时的滚动冲突:
typescript
// handlers/NestedScrollHandler.ets
export interface NestedScrollConfig {
parentScroller: Scroller
childScroller: Scroller
scrollForward: ScrollForward
}
export class NestedScrollHandler {
private parentScroller: Scroller
private childScroller: Scroller
constructor(config: NestedScrollConfig) {
this.parentScroller = config.parentScroller
this.childScroller = config.childScroller
}
onParentScroll(): void {
const parentOffset = this.parentScroller.currentOffset().yOffset
const childOffset = this.childScroller.currentOffset().yOffset
if (parentOffset <= 0 && childOffset > 0) {
this.childScroller.scrollBy({ xOffset: 0, yOffset: -parentOffset })
}
}
onChildScroll(): void {
const childOffset = this.childScroller.currentOffset().yOffset
if (childOffset <= 0) {
this.parentScroller.scrollBy({ xOffset: 0, yOffset: childOffset })
}
}
}
五、下拉刷新状态机
5.1 状态机设计
SmartScroll 的下拉刷新状态机如下:

状态说明:
| 状态 | 说明 | 转换条件 |
|---|---|---|
| IDLE | 初始状态,无操作 | 手指下拉 → PULLING |
| PULLING | 下拉中,未达阈值 | 达到阈值 → RELEASE;松手(未达阈值)→ IDLE |
| RELEASE | 已松开,达到刷新条件 | 松手 → REFRESHING |
| REFRESHING | 刷新中,发起网络请求 | 请求成功 → SUCCESS;请求失败 → FAILED |
| SUCCESS | 刷新成功 | 延迟 500ms → IDLE |
| FAILED | 刷新失败 | 延迟 500ms → IDLE |
5.2 关键设计决策
- 阻尼系数:下拉时偏移量乘以 0.6 的阻尼系数,模拟真实物理手感;
- 阈值判断:下拉距离超过 70vp 时触发刷新,避免误触;
- 动画过渡 :状态切换时使用
animateTo实现平滑过渡,提升视觉体验; - 防重复触发:刷新中禁止再次触发下拉,避免重复请求。
六、原生 vs 封装后对比
以下从代码量、功能覆盖、维护成本三个维度进行对比:

| 对比维度 | 原生 Scroll | SmartScroll(封装后) |
|---|---|---|
| 调用代码量 | 50+ 行,需手动处理触摸事件 | 15~20 行,声明式配置 |
| 下拉刷新 | 需手动处理触摸、偏移、状态、动画 | 内置 PullRefreshManager,开箱即用 |
| 上拉加载 | 需手动监听滚动、判断触底 | 内置 LoadMoreManager,自动触发 |
| 大数据量 | ForEach 全量渲染,内存占用高 |
LazyForEach 懒加载,性能优秀 |
| 嵌套滚动 | 需手动协调父子滚动冲突 | 内置 NestedScrollHandler |
| 空状态/错误状态 | 需自行实现 | 支持 emptyBuilder/errorBuilder |
| 多端适配 | 需手动调整布局 | 结合 BreakpointSystem 自动适配 |
七、实战案例:新闻资讯列表集成
以下是一个典型的新闻资讯页面,集成 SmartScroll 实现下拉刷新+上拉加载:
typescript
// pages/NewsPage.ets
import { SmartScroll } from '../components/SmartScroll'
import { ScrollItem } from '../models/ScrollItem'
import { NewsService } from '../services/NewsService'
@Entry
@Component
struct NewsPage {
@State newsList: ScrollItem[] = []
@State currentPage: number = 1
@State isLoading: boolean = false
@State hasMore: boolean = true
aboutToAppear(): void {
this.fetchNews()
}
private async fetchNews(): Promise<void> {
this.currentPage = 1
try {
const data = await NewsService.getNewsList(this.currentPage, 20)
this.newsList = data.map(item => ({
id: item.id,
title: item.title,
subtitle: item.summary,
imageUrl: item.coverImage,
timestamp: item.publishTime,
extra: { source: item.source, readCount: item.readCount }
}))
this.hasMore = data.length >= 20
} catch (e) {
console.error('Fetch news failed:', e)
}
}
private async loadMoreNews(): Promise<boolean> {
if (!this.hasMore) return false
this.currentPage++
try {
const data = await NewsService.getNewsList(this.currentPage, 20)
const newItems = data.map(item => ({
id: item.id,
title: item.title,
subtitle: item.summary,
imageUrl: item.coverImage,
timestamp: item.publishTime,
extra: { source: item.source, readCount: item.readCount }
}))
this.newsList.push(...newItems)
this.hasMore = data.length >= 20
return this.hasMore
} catch (e) {
console.error('Load more failed:', e)
return true
}
}
@Builder
NewsItemBuilder(item: ScrollItem, index: number) {
Row({ space: 12 }) {
if (item.imageUrl) {
Image(item.imageUrl)
.width(100)
.height(75)
.objectFit(ImageFit.Cover)
.borderRadius(8)
.alt($r('app.media.news_placeholder'))
}
Column({ space: 6 }) {
Text(item.title)
.fontSize(16)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
if (item.subtitle) {
Text(item.subtitle)
.fontSize(13)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
Row({ space: 8 }) {
Text(item.extra?.source as string ?? '未知来源')
.fontSize(11)
.fontColor('#CCCCCC')
Text(this.formatTime(item.timestamp ?? 0))
.fontSize(11)
.fontColor('#CCCCCC')
Text(`${item.extra?.readCount ?? 0}阅读`)
.fontSize(11)
.fontColor('#CCCCCC')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ top: 4, bottom: 4 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.onClick(() => {
router.pushUrl({
url: 'pages/NewsDetailPage',
params: { newsId: item.id }
})
})
}
@Builder
EmptyBuilder() {
Column({ space: 12 }) {
Image($r('app.media.ic_empty'))
.width(120)
.height(120)
Text('暂无新闻')
.fontSize(16)
.fontColor('#999999')
Button('刷新试试')
.onClick(() => this.fetchNews())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private formatTime(timestamp: number): string {
const now = Date.now()
const diff = now - timestamp
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
return new Date(timestamp).toLocaleDateString()
}
build() {
Column() {
Row() {
Text('新闻资讯')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
SmartScroll({
data: this.newsList,
enablePullRefresh: true,
enableLoadMore: true,
refreshHeight: 70,
loadMoreThreshold: 100,
scrollBar: BarState.Auto,
edgeEffect: EdgeEffect.Spring,
onRefresh: async () => {
await this.fetchNews()
},
onLoadMore: async () => {
return await this.loadMoreNews()
},
itemBuilder: (item: ScrollItem, index: number) => {
this.NewsItemBuilder(item, index)
},
emptyBuilder: () => {
this.EmptyBuilder()
}
})
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
7.1 运行效果说明
- 下拉刷新:手指下拉时显示"下拉刷新"→"松开刷新"→"正在刷新"状态切换,刷新成功后显示"刷新成功"并自动收起;
- 上拉加载:滚动到底部时自动触发加载更多,显示 Loading 动画,加载完成后自动追加数据;
- 空状态:当列表为空时显示空状态页面,提供"刷新试试"按钮;
- 点击跳转 :点击新闻项携带
newsId参数跳转到详情页; - 时间格式化:相对时间显示(刚刚、5分钟前、2小时前等),提升阅读体验。
八、性能优化与最佳实践
8.1 性能优化策略

8.1.1 渲染优化
- LazyForEach 懒加载:仅渲染可视区域及预加载区域的节点,大数据量场景下内存占用降低 60% 以上;
- cachedCount 预加载:设置合理的缓存数量(通常 1~2 屏),平衡内存占用与滑动流畅度;
- 组件复用 @Reusable :列表项组件标记为
@Reusable,减少组件实例创建开销; - 条件渲染控制 :非首屏内容使用
if条件控制,进入视口后再渲染。
8.1.2 内存优化
- 及时释放离屏节点 :
LazyForEach自动回收离屏节点,无需手动管理; - 图片内存缓存:相同 URL 的图片共享内存缓存,避免重复解码;
- 避免闭包泄漏 :回调函数中不持有外部大对象引用,使用箭头函数确保
this指向正确; - 大数据分页加载:每次加载固定数量(如 20 条),避免一次性加载过多数据。
8.1.3 滚动优化
- 惯性滚动曲线 :使用
Curve.EaseOut或自定义springCurve,提升滑动质感; - 边缘回弹效果 :使用
EdgeEffect.Spring提供弹性回弹,增强交互反馈; - 嵌套滚动协调 :通过
NestedScrollOptions配置父子滚动容器的滚动优先级; - 滚动条智能显隐 :使用
BarState.Auto实现滚动时显示、停止时隐藏。
8.2 工程化最佳实践
| 原则 | 实践建议 |
|---|---|
| 数据驱动 | 使用 LazyForEach + 数据源管理,避免全量渲染 |
| 状态隔离 | 下拉刷新/上拉加载状态独立管理,避免级联刷新 |
| 异常兜底 | 网络请求失败时显示重试按钮,避免空白页面 |
| 一多适配 | 结合 BreakpointSystem 适配手机/平板/折叠屏 |
| 类型安全 | 使用严格的 TypeScript 类型定义,避免 any 滥用 |
九、扩展能力展望
SmartScroll 封装方案具备良好的扩展性,后续可在此基础上迭代以下能力:
- 骨架屏加载:首次加载时显示骨架屏,提升 perceived performance;
- 吸顶效果 :结合
sticky属性实现分类标签吸顶,提升导航效率; - 侧滑删除:为列表项添加侧滑手势,支持删除/收藏等快捷操作;
- 拖拽排序:支持长按拖拽排序,适用于待办事项、歌单等场景;
- 瀑布流布局 :集成
WaterFlow组件,支持不规则高度的多列布局; - 无障碍增强:增加屏幕朗读支持,为视障用户提供滚动内容播报能力。
十、总结
本文从 HarmonyOS ArkUI 原生 Scroll/List/Refresh 的能力边界出发,系统性地设计并实现了一套企业级的 SmartScroll 封装方案。通过下拉刷新管理器 、上拉加载管理器 、嵌套滚动协调器三大子模块的拆分,实现了高内聚、低耦合的组件架构。在实际业务场景中,开发者仅需 15~20 行配置代码即可完成高性能滚动列表的集成,大幅提升了开发效率与用户体验。
核心要点回顾:
- 组件封装是提升 ArkUI 开发效率的关键手段,应将重复逻辑下沉到公共组件;
- LazyForEach 是大数据量列表场景的性能基石,配合
cachedCount实现流畅滑动; - 状态机设计(IDLE → PULLING → RELEASE → REFRESHING → SUCCESS/FAILED → IDLE)确保下拉刷新逻辑清晰可控;
- 嵌套滚动协调 是处理复杂页面布局的关键,通过
NestedScrollOptions实现父子滚动容器的和谐共处; - 一多适配是鸿蒙生态的核心优势,滚动组件应充分利用响应式布局能力实现多端自适应。
希望本文的封装思路与代码实践能够为鸿蒙生态的组件化建设提供有价值的参考。
转载自:https://blog.csdn.net/u014727709/article/details/163370210
欢迎 👍点赞✍评论⭐收藏,欢迎指正