前言

Scroll 是 ArkUI 中最基础的滚动容器,当内容超过容器尺寸时自动提供滚动能力。区别于 List(专为列表设计),Scroll 支持任意布局内容的垂直/水平滚动,并提供丰富的边缘效果、滚动条控制及滚动事件回调。本文通过完整示例逐一演示。

运行效果

初始状态(垂直滚动)

idle

切换水平滚动模式

done

核心 API 一览

API说明
Scroll(scroller?)滚动容器,可传入 Scroller 控制器
.scrollable(ScrollDirection)滚动方向:Vertical / Horizontal / Free / None
.scrollBar(BarState)滚动条可见性:Auto / On / Off
.edgeEffect(EdgeEffect)边缘效果:Spring(弹性)/ Fade(渐隐)/ None
.onScroll(xOff, yOff)滚动偏移量回调
.onScrollEdge(side)到达边缘时回调
scroller.scrollTo({xOffset, yOffset})编程式滚动到指定位置
scroller.scrollEdge(Edge.Top)滚动到顶部/底部
scroller.currentOffset()获取当前滚动偏移量

完整示例代码

@Entry
@Component
struct Index {
  @State scrollDir: ScrollDirection = ScrollDirection.Vertical
  @State edgeEffectIndex: number = 0
  @State scrollOffsetY: number = 0
  @State scrollOffsetX: number = 0
  @State reachedEdge: string = ''

  private scroller: Scroller = new Scroller()

  private edgeEffects: EdgeEffect[] = [EdgeEffect.Spring, EdgeEffect.Fade, EdgeEffect.None]
  private edgeLabels: string[] = ['Spring', 'Fade', 'None']

  private colorList: string[] = [
    '#ff6b6b', '#ff9f43', '#feca57', '#48dbfb',
    '#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3',
    '#1dd1a1', '#c8d6e5', '#576574', '#341f97',
  ]

  build() {
    Column({ space: 0 }) {
      // 控制栏
      Row({ space: 0 }) {
        // 边缘效果选择
        Row({ space: 6 }) {
          ForEach(this.edgeLabels, (label: string, idx: number) => {
            Text(label)
              .fontSize(12)
              .fontColor(this.edgeEffectIndex === idx ? '#fff' : '#333')
              .backgroundColor(this.edgeEffectIndex === idx ? '#0066ff' : '#eeeeee')
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .borderRadius(12)
              .onClick(() => { this.edgeEffectIndex = idx })
          })
        }
        .layoutWeight(1)

        // 方向切换
        Row({ space: 6 }) {
          Text('垂直')
            .fontSize(12)
            .fontColor(this.scrollDir === ScrollDirection.Vertical ? '#fff' : '#333')
            .backgroundColor(this.scrollDir === ScrollDirection.Vertical ? '#0066ff' : '#eeeeee')
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(12)
            .onClick(() => {
              this.scrollDir = ScrollDirection.Vertical
              this.scroller.scrollEdge(Edge.Top)
            })

          Text('水平')
            .fontSize(12)
            .fontColor(this.scrollDir === ScrollDirection.Horizontal ? '#fff' : '#333')
            .backgroundColor(this.scrollDir === ScrollDirection.Horizontal ? '#0066ff' : '#eeeeee')
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(12)
            .onClick(() => {
              this.scrollDir = ScrollDirection.Horizontal
              this.scroller.scrollEdge(Edge.Start)
            })

          Button('回顶部')
            .fontSize(12)
            .height(32)
            .backgroundColor('#ff6600')
            .onClick(() => {
              this.scroller.scrollEdge(Edge.Top)
            })
        }
      }
      .width('100%')
      .height(52)
      .padding({ left: 12, right: 12 })
      .backgroundColor('#f8f8f8')
      .border({ width: { bottom: 1 }, color: '#e0e0e0' })

      // 偏移量显示
      Row({ space: 8 }) {
        Text('X: ' + Math.round(this.scrollOffsetX).toString())
          .fontSize(12).fontColor('#0066ff')
        Text('Y: ' + Math.round(this.scrollOffsetY).toString())
          .fontSize(12).fontColor('#ff6600')
        if (this.reachedEdge.length > 0) {
          Text('到达: ' + this.reachedEdge)
            .fontSize(12).fontColor('#00aa44')
        }
      }
      .width('100%')
      .height(32)
      .padding({ left: 16 })

      // Scroll 主体
      Scroll(this.scroller) {
        if (this.scrollDir === ScrollDirection.Vertical) {
          // 垂直方向:色块列表
          Column({ space: 12 }) {
            ForEach(this.colorList, (color: string, idx: number) => {
              Column() {
                Text('色块 ' + (idx + 1).toString())
                  .fontSize(18)
                  .fontColor('#ffffff')
                  .fontWeight(FontWeight.Bold)
                Text(color)
                  .fontSize(12)
                  .fontColor('rgba(255,255,255,0.7)')
                  .margin({ top: 4 })
              }
              .width('100%')
              .height(80)
              .backgroundColor(color)
              .borderRadius(12)
              .justifyContent(FlexAlign.Center)
            })
          }
          .width('100%')
          .padding({ left: 16, right: 16 })
        } else {
          // 水平方向:横排色块
          Row({ space: 12 }) {
            ForEach(this.colorList, (color: string, idx: number) => {
              Column() {
                Text((idx + 1).toString())
                  .fontSize(24)
                  .fontColor('#ffffff')
                  .fontWeight(FontWeight.Bold)
                Text(color)
                  .fontSize(10)
                  .fontColor('rgba(255,255,255,0.8)')
                  .margin({ top: 4 })
              }
              .width(120)
              .height('100%')
              .backgroundColor(color)
              .borderRadius(12)
              .justifyContent(FlexAlign.Center)
            })
          }
          .height('100%')
          .padding({ top: 16, bottom: 16 })
        }
      }
      .scrollable(this.scrollDir)
      .scrollBar(BarState.Auto)
      .edgeEffect(this.edgeEffects[this.edgeEffectIndex])
      .onScroll((_x: number, _y: number) => {
        let offset = this.scroller.currentOffset()
        this.scrollOffsetX = offset.xOffset
        this.scrollOffsetY = offset.yOffset
      })
      .onScrollEdge((side: Edge) => {
        if (side === Edge.Top) {
          this.reachedEdge = '顶部'
        } else if (side === Edge.Bottom) {
          this.reachedEdge = '底部'
        } else if (side === Edge.Start) {
          this.reachedEdge = '左侧'
        } else if (side === Edge.End) {
          this.reachedEdge = '右侧'
        }
      })
      .onScrollEnd(() => {
        this.reachedEdge = ''
      })
      .width('100%')
      .layoutWeight(1)
      .padding({ top: 8, bottom: 8 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffffff')
  }
}

关键知识点

1. Scroller 控制器:编程式滚动

Scroller 是实现“回到顶部“、“跳转到指定位置“等功能的关键:

private scroller: Scroller = new Scroller()

// 绑定到 Scroll
Scroll(this.scroller) { ... }

// 编程式控制
this.scroller.scrollTo({ xOffset: 0, yOffset: 500 })  // 滚动到 y=500
this.scroller.scrollEdge(Edge.Top)                     // 回到顶部
this.scroller.scrollEdge(Edge.Bottom)                  // 滚到底部
let pos = this.scroller.currentOffset()                // 获取当前偏移

2. onScroll 回调的参数

.onScroll(xOffset, yOffset) 传入的是本次滚动的增量,不是总偏移量。要获取当前绝对位置,需调用 scroller.currentOffset():

.onScroll((_dx: number, _dy: number) => {
  // _dx/_dy 是增量,用 currentOffset() 获取绝对位置
  let pos = this.scroller.currentOffset()
  this.currentY = pos.yOffset
})

3. 边缘效果(EdgeEffect)

值视觉效果适用场景
EdgeEffect.Spring超出边界后弹性回弹iOS 风格,最常用
EdgeEffect.Fade边缘渐隐遮罩偏 Android 风格
EdgeEffect.None无边缘效果,硬停止精确控制场景

4. 切换滚动方向

切换滚动方向时需要重置滚动位置,否则偏移量残留会导致内容不可见:

// 切换到水平方向时,重置滚动位置
this.scrollDir = ScrollDirection.Horizontal
this.scroller.scrollEdge(Edge.Start)  // 回到最左侧

5. Scroll vs List 的选型

维度ScrollList
内容类型任意布局(Column/Row/Grid 等)仅列表项
性能全部子节点同时渲染虚拟化,只渲染可见项
适用数据量较少(<100 项)任意(千级以上)
分组吸顶不支持支持(ListItemGroup)

小结

  • Scroll 适合内容数量有限的任意布局滚动场景,不具备虚拟化能力
  • 通过 Scroller 控制器可以编程式滚动到任意位置
  • onScroll 回调参数是增量;绝对位置需用 scroller.currentOffset() 获取
  • edgeEffect 选 Spring 最接近系统原生体验
  • 长列表(数量大、相似结构)请使用 List,而非 Scroll + Column + ForEach

更多推荐