問題描述

在長列表或複雜頁面中,如何正確使用 Scroll 組件?如何處理 Scroll 嵌套問題?如何實現流暢的滑動效果和彈性邊界?

關鍵字: Scroll、滑動容器、嵌套滑動、edgeEffect、佈局優化

解決方案

完整代碼

@Entry
@Component
struct ScrollDemo {
  @State records: string[] = Array.from({ length: 50 }, (_, i) => `記錄 ${i + 1}`);
  private scroller: Scroller = new Scroller();
  
  build() {
    Column() {
      // 頂部固定區域
      this.buildHeader()
      
      // 可滾動內容區域
      Scroll(this.scroller) {
        Column({ space: 12 }) {
          // 統計卡片
          this.buildStatsCard()
          
          // 篩選器
          this.buildFilters()
          
          // 記錄列表
          this.buildRecordList()
        }
        .width('100%')
        .padding(16)
      }
      .width('100%')
      .layoutWeight(1)
      .scrollable(ScrollDirection.Vertical) // 垂直滾動
      .scrollBar(BarState.Auto) // 自動顯示滾動條
      .scrollBarColor('#888') // 滾動條顏色
      .scrollBarWidth(4) // 滾動條寬度
      .edgeEffect(EdgeEffect.Spring) // 彈性邊界效果
      .friction(0.6) // 摩擦係數(越小滑動越遠)
      .onScroll((xOffset: number, yOffset: number) => {
        // 滾動監聽
        console.log(`滾動偏移: ${yOffset}`);
      })
      .onScrollEdge((side: Edge) => {
        // 滾動到邊界
        console.log(`滾動到邊界: ${side}`);
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')
  }
  
  @Builder
  buildHeader() {
    Row() {
      Text('記錄列表')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      Blank()
      
      Button('回到頂部')
        .fontSize(14)
        .onClick(() => {
          // 滾動到頂部
          this.scroller.scrollEdge(Edge.Top);
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
  }
  
  @Builder
  buildStatsCard() {
    Row({ space: 12 }) {
      Column() {
        Text('500')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff6b6b')
        Text('總收入')
          .fontSize(12)
          .fontColor('#999')
      }
      .layoutWeight(1)
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(12)
      
      Column() {
        Text('300')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#67c23a')
        Text('總支出')
          .fontSize(12)
          .fontColor('#999')
      }
      .layoutWeight(1)
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(12)
    }
    .width('100%')
  }
  
  @Builder
  buildFilters() {
    Row({ space: 8 }) {
      Text('全部').filterChip(true)
      Text('收入').filterChip(false)
      Text('支出').filterChip(false)
    }
    .width('100%')
  }
  
  @Builder
  buildRecordList() {
    Column({ space: 8 }) {
      ForEach(this.records, (record: string) => {
        Row() {
          Text(record)
            .fontSize(16)
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(8)
      })
    }
    .width('100%')
  }
}
​
// 擴展方法:篩選標籤樣式
@Extend(Text)
function filterChip(selected: boolean) {
  .fontSize(14)
  .padding({ left: 16, right: 16, top: 8, bottom: 8 })
  .backgroundColor(selected ? '#ff6b6b' : '#f5f5f5')
  .fontColor(selected ? Color.White : '#333')
  .borderRadius(16)
}
​
/**
 * Scroll嵌套解決方案
 */
@Component
struct NestedScrollDemo {
  private outerScroller: Scroller = new Scroller();
  private innerScroller: Scroller = new Scroller();
  
  build() {
    // 外層Scroll
    Scroll(this.outerScroller) {
      Column({ space: 16 }) {
        // 固定內容
        Text('外層內容')
          .width('100%')
          .height(200)
          .backgroundColor('#e3f2fd')
        
        // 內層Scroll(橫向)
        Scroll(this.innerScroller) {
          Row({ space: 12 }) {
            ForEach([1, 2, 3, 4, 5], (item: number) => {
              Text(`卡片${item}`)
                .width(150)
                .height(100)
                .backgroundColor('#fff3e0')
                .textAlign(TextAlign.Center)
                .borderRadius(8)
            })
          }
          .padding(16)
        }
        .width('100%')
        .scrollable(ScrollDirection.Horizontal) // 橫向滾動
        .scrollBar(BarState.Off) // 隱藏滾動條
        
        // 更多固定內容
        Text('更多外層內容')
          .width('100%')
          .height(400)
          .backgroundColor('#f3e5f5')
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .scrollable(ScrollDirection.Vertical) // 縱向滾動
  }
}
​
/**
 * Scroller控制器高級用法
 */
@Component
struct ScrollerControlDemo {
  private scroller: Scroller = new Scroller();
  @State currentOffset: number = 0;
  
  build() {
    Column() {
      // 控制按鈕
      Row({ space: 8 }) {
        Button('滾動到頂部')
          .onClick(() => {
            this.scroller.scrollEdge(Edge.Top);
          })
        
        Button('滾動到底部')
          .onClick(() => {
            this.scroller.scrollEdge(Edge.Bottom);
          })
        
        Button('滾動到指定位置')
          .onClick(() => {
            // 滾動到500px位置,動畫時長300ms
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset: 500,
              animation: {
                duration: 300,
                curve: Curve.EaseInOut
              }
            });
          })
        
        Button('滾動一頁')
          .onClick(() => {
            // 向下滾動一頁
            this.scroller.scrollPage({
              next: true,
              direction: Axis.Vertical
            });
          })
      }
      .padding(16)
      
      Text(`當前偏移: ${this.currentOffset.toFixed(0)}px`)
        .padding(8)
      
      // 滾動內容
      Scroll(this.scroller) {
        Column({ space: 12 }) {
          ForEach(Array.from({ length: 30 }, (_, i) => i), (index: number) => {
            Text(`內容 ${index + 1}`)
              .width('100%')
              .height(80)
              .backgroundColor('#e0f7fa')
              .textAlign(TextAlign.Center)
              .borderRadius(8)
          })
        }
        .padding(16)
      }
      .layoutWeight(1)
      .onScroll((xOffset: number, yOffset: number) => {
        this.currentOffset += yOffset;
      })
    }
    .width('100%')
    .height('100%')
  }
}

原理解析

1. Scroll 基本屬性

.scrollable(ScrollDirection.Vertical) // 滾動方向
.scrollBar(BarState.Auto) // 滾動條顯示策略
.edgeEffect(EdgeEffect.Spring) // 邊界效果
.friction(0.6) // 摩擦係數

2. Scroller 控制器

scroller.scrollEdge(Edge.Top) // 滾動到邊界
scroller.scrollTo({ yOffset: 500 }) // 滾動到指定位置
scroller.scrollPage({ next: true }) // 翻頁

3. 嵌套滑動

  • 外層 Scroll:縱向滾動
  • 內層 Scroll:橫向滾動
  • 不同方向不衝突

4. 滾動監聽

.onScroll((xOffset, yOffset) => {
  // 滾動偏移量
})
.onScrollEdge((side: Edge) => {
  // 滾動到邊界
})

最佳實踐

  1. 固定頭部: 頭部放在 Scroll 外面,避免滾動
  2. layoutWeight: Scroll 使用 layoutWeight(1)填充剩餘空間
  3. 邊界效果: 使用 EdgeEffect.Spring 提升體驗
  4. 滾動條: 長列表顯示滾動條,短內容隱藏
  5. 性能優化: 內容過多時使用 List+LazyForEach

避坑指南

  1. 嵌套方向: 嵌套 Scroll 必須方向不同
  2. height 設置: Scroll 必須有明確高度
  3. Column 嵌套: Scroll 內 Column 不要設置 height
  4. 滾動衝突: 同方向嵌套會導致滾動衝突
  5. 內存佔用: 內容過多時 Scroll 會全部渲染,考慮用 List

效果展示

  • 彈性邊界:滾動到頂部/底部時有彈性效果
  • 流暢滑動:摩擦係數 0.6,滑動流暢自然
  • 精確控制:通過 Scroller 實現各種滾動效果 相關資源
  • 鴻蒙學習資源