---
name: "ScrollView"
mode: "component"
versionRequirements:
  - { name: "@ray-js/ray", version: "0.5.10" }
title: "ScrollView - 滚动容器"
---

## ScrollView

> [VERSION] @ray-js/ray >= 0.5.10

### 描述

可滚动视图容器，支持横向或纵向滚动，可配置下拉刷新、滚动事件监听等功能。

### 属性

| 属性 | 类型 | 必填 | 默认值 | 最低版本 | 描述 |
| --- | --- | --- | --- | --- | --- |
| `scrollX` | `boolean` | 否 | `false` | - | 允许横向滚动 |
| `scrollY` | `boolean` | 否 | `false` | - | 允许纵向滚动 |
| `upperThreshold` | `number \| string` | 否 | `50` | - | 距顶部/左边多远时，触发 scrolltoupper 事件 |
| `lowerThreshold` | `number \| string` | 否 | `50` | - | 距底部/右边多远时，触发 scrolltolower 事件 |
| `scrollTop` | `number \| string` | 否 | `0` | - | 设置竖向滚动条位置 |
| `scrollLeft` | `number \| string` | 否 | `0` | - | 设置横向滚动条位置 |
| `scrollIntoView` | `string` | 否 | - | - | 值应为某子元素 id（id 不能以数字开头）。设置哪个方向可滚动，则在哪个方向滚动到该元素 |
| `scrollIntoViewOffset` | `number` | 否 | `0` | `1.7.58` | 跳转到 scrollIntoView 目标节点时的额外偏移，单位 px |
| `scrollWithAnimation` | `boolean` | 否 | `false` | - | 在设置滚动条位置时使用动画过渡 |
| `onScroll` | `(event: ScrollEvent) => void` | 否 | - | - | 滚动时触发 |
| `onScrollToUpper` | `(event: ScrolltoupperEvent) => void` | 否 | - | - | 滚动到顶部/左边时触发 |
| `onScrollToLower` | `(event: ScrolltolowerEvent) => void` | 否 | - | - | 滚动到底部/右边时触发 |
| `refresherEnabled` | `boolean` | 否 | `false` | `0.9.3` | 开启自定义下拉刷新 |
| `refresherThreshold` | `number` | 否 | `45` | `0.9.3` | 设置自定义下拉刷新阈值 |
| `refresherDefaultStyle` | `"black" \| "white" \| "none"` | 否 | `"black"` | `0.9.3` | 设置自定义下拉刷新默认样式，支持设置 black、white、none，none 表示不使用默认样式 |
| `refresherBackground` | `string` | 否 | `"#FFF"` | `0.9.3` | 设置自定义下拉刷新区域背景颜色 |
| `refresherTriggered` | `boolean` | 否 | `false` | `0.9.3` | 设置当前下拉刷新状态，true 表示下拉刷新已经被触发，false 表示下拉刷新未被触发 |
| `hideScrollbar` | `boolean` | 否 | `true` | - | 隐藏滚动条 |
| `bounces` | `boolean` | 否 | `true` | `1.7.58` | 是否启用iOS滚动回弹效果（iOS 16.0+ 完全支持） |
| `onRefresherpulling` | `(event: RefresherPullingEvent) => void` | 否 | - | `0.9.3` | 自定义下拉刷新控件被下拉时触发 |
| `onRefresherrefresh` | `(event: RefresherRefreshEvent) => void` | 否 | - | `0.9.3` | 自定义下拉刷新被触发时触发 |
| `onRefresherrestore` | `(event: RefresherRestoreEvent) => void` | 否 | - | `0.9.3` | 自定义下拉刷新被复位时触发 |
| `onRefresherabort` | `(event: RefresherAbortEvent) => void` | 否 | - | `0.9.3` | 自定义下拉刷新被中止时触发 |

### 引用对象

##### `interface` ScrollEvent

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `detail` | `ScrollDetail` | 滚动事件详情数据 |

##### `interface` ScrolltoupperEvent

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `detail` | `ScrollDirectionDetail` | 滚动事件详情数据 |

##### `interface` ScrolltolowerEvent

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `detail` | `ScrollDirectionDetail` | 滚动事件详情数据 |

##### `interface` RefresherPullingEvent

```typescript
export interface RefresherPullingEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherpulling';
}
```

##### `interface` RefresherRefreshEvent

```typescript
export interface RefresherRefreshEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherrefresh';
}
```

##### `interface` RefresherRestoreEvent

```typescript
export interface RefresherRestoreEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherrestore';
}
```

##### `interface` RefresherAbortEvent

```typescript
export interface RefresherAbortEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherabort';
}
```

##### `interface` ScrollDetail

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `scrollLeft` | `number` | 横向滚动位置 |
| `scrollTop` | `number` | 纵向滚动位置 |
| `scrollHeight` | `number` | 滚动内容高度 |
| `scrollWidth` | `number` | 滚动内容宽度 |
| `deltaX` | `number` | 横向滚动变化量 |
| `deltaY` | `number` | 纵向滚动变化量 |

##### `interface` BaseEvent

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `type` | `string` | 事件类型 |
| `timeStamp` | `number` | 页面打开到触发事件所经过的毫秒数 |
| `target` | `Target` | 触发事件的源组件 |
| `currentTarget` | `Target` | 当前组件的一些属性值集合 |
| `mark` | `any` | 事件标记数据 |

##### `interface` ScrollDirectionDetail

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `direction` | `"top" \| "left" \| "bottom" \| "right"` | 滚动方向 |

##### `interface` Target

| 属性 | 类型 | 描述 |
| --- | --- | --- |
| `id` | `string` | 事件源组件的id |
| `dataset` | `Record<string, unknown>` | 事件源组件上的 `dataset` 自定义属性组成的集合 |


### 示例代码

#### 基础用法

```tsx
import React from 'react';
import { ScrollView, View, Text } from '@ray-js/ray';

export default function BasicScrollView() {
  const handleScrollToUpper = () => {
    console.log('已滚动到顶部');
  };

  const handleScrollToLower = () => {
    console.log('已滚动到底部');
  };

  return (
    <ScrollView
      scrollY
      style={{ height: '400rpx', backgroundColor: '#f5f5f5' }}
      onScrollToUpper={handleScrollToUpper}
      onScrollToLower={handleScrollToLower}
    >
      {Array.from({ length: 10 }, (_, i) => (
        <View
          key={i}
          style={{
            height: '100rpx',
            margin: '10rpx',
            backgroundColor: '#fff',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text>列表项 {i + 1}</Text>
        </View>
      ))}
    </ScrollView>
  );
}
```

#### 下拉刷新

```tsx
import React, { useState, useCallback } from 'react';
import { ScrollView, View, Text } from '@ray-js/ray';

export default function PullToRefreshDemo() {
  const [list, setList] = useState(() =>
    Array.from({ length: 10 }, (_, i) => `初始数据 ${i + 1}`)
  );
  const [refreshing, setRefreshing] = useState(false);
  const [loading, setLoading] = useState(false);

  const loadData = useCallback((startIndex: number) => {
    return new Promise<string[]>((resolve) => {
      setTimeout(() => {
        const newList = Array.from(
          { length: 10 },
          (_, i) => `数据 ${startIndex + i + 1}`
        );
        resolve(newList);
      }, 1000);
    });
  }, []);

  const handleRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      const newList = await loadData(-1);
      setList(prevList => [...newList, ...prevList]);
    } finally {
      setRefreshing(false);
    }
  }, [loadData]);

  const handleScrollToLower = useCallback(async () => {
    if (loading) return;
    setLoading(true);
    try {
      const newList = await loadData(list.length);
      setList(prevList => [...prevList, ...newList]);
    } finally {
      setLoading(false);
    }
  }, [loading, list.length, loadData]);

  return (
    <ScrollView
      scrollY
      style={{ height: '600rpx', backgroundColor: '#f5f5f5' }}
      refresherEnabled
      refresherThreshold={50}
      refresherTriggered={refreshing}
      refresherBackground="#f5f5f5"
      onRefresherrefresh={handleRefresh}
      onScrollToLower={handleScrollToLower}
      lowerThreshold={100}
    >
      {list.map((item, index) => (
        <View
          key={index}
          style={{
            height: '100rpx',
            margin: '10rpx 20rpx',
            backgroundColor: '#fff',
            borderRadius: '8rpx',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text>{item}</Text>
        </View>
      ))}
      {loading && (
        <View
          style={{
            height: '80rpx',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text style={{ color: '#999', fontSize: '28rpx' }}>加载中...</Text>
        </View>
      )}
    </ScrollView>
  );
}
```

#### 横向滚动

```tsx
import React from 'react';
import { ScrollView, View, Text } from '@ray-js/ray';

export default function HorizontalScrollDemo() {
  const cards = ['卡片1', '卡片2', '卡片3', '卡片4', '卡片5'];

  return (
    <ScrollView
      scrollX
      style={{
        width: '100%',
        height: '200rpx',
        whiteSpace: 'nowrap',
      }}
      onScroll={(e) => console.log('横向滚动位置:', e.detail.scrollLeft)}
    >
      {cards.map((card, index) => (
        <View
          key={index}
          style={{
            width: '300rpx',
            height: '180rpx',
            marginRight: '20rpx',
            backgroundColor: '#1890ff',
            borderRadius: '12rpx',
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text style={{ color: '#fff', fontSize: '32rpx' }}>{card}</Text>
        </View>
      ))}
    </ScrollView>
  );
}
```

#### 滚动到指定位置

```tsx
import React, { useState } from 'react';
import { ScrollView, View, Text, Button } from '@ray-js/ray';

export default function ScrollIntoViewDemo() {
  const [targetId, setTargetId] = useState('');
  const sections = ['section-a', 'section-b', 'section-c', 'section-d'];

  return (
    <View>
      <View style={{ display: 'flex', marginBottom: '20rpx' }}>
        {sections.map((id) => (
          <Button
            key={id}
            size="mini"
            style={{ marginRight: '10rpx' }}
            onClick={() => setTargetId(id)}
          >
            跳转 {id.split('-')[1].toUpperCase()}
          </Button>
        ))}
      </View>
      <ScrollView
        scrollY
        scrollWithAnimation
        scrollIntoView={targetId}
        scrollIntoViewOffset={10}
        style={{ height: '400rpx', backgroundColor: '#f5f5f5' }}
      >
        {sections.map((id, index) => (
          <View
            key={id}
            id={id}
            style={{
              height: '300rpx',
              margin: '10rpx',
              backgroundColor: ['#e6f7ff', '#fff7e6', '#f6ffed', '#fff1f0'][index],
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <Text style={{ fontSize: '36rpx' }}>区域 {id.split('-')[1].toUpperCase()}</Text>
          </View>
        ))}
      </ScrollView>
    </View>
  );
}
```


### 常见问题

#### 为何 scroll-view 在 popup 扩展组件中无法滑动？

popup 组件上加上 `disableScroll` 属性并将值设为 `false` 才能滑动。

#### 如何监听 scroll-view 滚动到底部？

可以直接在 `onScroll` 方法中进行处理，使用 `onScrollToLower` 监听 `scrollView` 的滚动高度来进行判断是否滑动到了底部。
`scrollHeight` 是 `scrollView` 里面所有 `View` 的高度和，`scrollTop` 是滚动的值；
