首页教程React Native

嵌套滚动三件套

React Native 嵌套滚动三件套

在 App 开发中,嵌套滚动是一个很常见的需求。比如一个可纵向滚动的页面中有一个可横向滚动的列表,列表中的每一项又是一个可纵向滚动的列表。这种情况下,我们就需要用到嵌套滚动。

image

譬如下拉刷新,向下滑动时,如果还没到顶部,列表内容滚动,如果已经到达顶部,则列表自身移动。

譬如 BottomSheet,向上滑动时,如果 BottomSheet 还没完全展开,则 BottomSheet 移动,如果已经完全展开,则 BottomSheet 里面的列表滚动。

在 React Native,实现嵌套滚动非常不容易。

通常有两种实现方式:

  1. 借助 react-native-gesture-handler 和 reanimated 来实现。
  2. 封装原生 UI 组件来实现。

本文分享作者和他的伙伴们(以下也称作 我们 )通过封装原生 UI 组件来实现嵌套滚动的成果。

因为一共有三个组件,在 Andriod 都是通过 NestedScrolling APIopen in new window 来实现的,并且可以配合使用,所以我们将这三个组件称作 嵌套滚动三件套

NestedScrollView

NestedScrollViewopen in new window 用于实现题图的效果。

安装

yarn add @sdcx/nested-scroll

使用

NestedScrollView 在使用上比较简单,将可滚动列表作为子组件放入 NestedScrollView 即可,如下:

import { NestedScrollView, NestedScrollViewHeader } from '@sdcx/nested-scroll' const App = () => { return ( <NestedScrollView> <NestedScrollViewHeader stickyHeaderBeginIndex={1}> <Image /> <TabBar /> </NestedScrollViewHeader> <PagerView> <FlatList nestedScrollEnabled /> <FlashList nestedScrollEnabled /> <ScrollView nestedScrollEnabled /> <WebView nestedScrollEnabled /> </PagerView> </NestedScrollView> ) }

WARNING

注意为可滚动列表开启 nestedScrollEnabledopen in new window 属性

NestedScrollViewNestedScrollViewHeader 都只有两三个属性,使用起来非常简单。

想要 sticky header 效果,可以配置 NestedScrollViewHeader 如下两个属性之一:

  • stickyHeaderBeginIndex,它表示从第几个子组件开始,子组件将会被固定在顶部。
  • stickyHeight,它表示 header 多高的区域将会被固定在顶部,优先级高于 stickyHeaderBeginIndex

PullToRefresh

React Native 内置的下拉刷新组件比较简陋,且 iOS 和 Android 平台的表现很不一致。幸运的是,它提供了一个 refreshControl 属性,可以用来自定义下拉刷新组件。

我们封装了一个原生组件 -- PullToRefreshopen in new window,用来提供自定义下拉刷新的能力。

可以实现如下效果:

安装

yarn add @sdcx/pull-to-refresh

使用

PullToRefresh 在使用上主要有以下几个步骤:

WARNING

注意为可滚动列表开启 nestedScrollEnabledopen in new window 属性

  1. 使用 PullToRefresh 提供的 RefreshControl 组件设置 refreshControl 属性,并传递 refreshingonRefresh 属性。
    import { RefreshControl } from '@sdcx/pull-to-refresh' function App() { const [refreshing, setRefreshing] = useState(false) return ( <FlatList nestedScrollEnabled refreshControl={ <RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true) setTimeout(() => { setRefreshing(false) }, 2000) }} /> } /> ) }
  2. PullToRefresh 提供的默认下拉刷新样式并不适用于你的 App,你需要自定义下拉刷新样式。 参考下面的例子即可:
    import { PullToRefreshHeader, PullToRefreshHeaderProps, PullToRefreshOffsetChangedEvent, PullToRefreshStateChangedEvent, PullToRefreshState, PullToRefreshStateIdle, PullToRefreshStateRefreshing, } from '@sdcx/pull-to-refresh' function LottiePullToRefreshHeader(props: PullToRefreshHeaderProps) { const [progress, setProgress] = useState(0) const lottieRef = useRef<Lottie>(null) const stateRef = useRef<PullToRefreshState>(PullToRefreshStateIdle) const onOffsetChanged = useCallback((event: PullToRefreshOffsetChangedEvent) => { const offset = event.nativeEvent.offset if (stateRef.current !== PullToRefreshStateRefreshing) { setProgress(Math.min(1, offset / 50)) } }, []) const onStateChanged = useCallback( (event: PullToRefreshStateChangedEvent) => { const state = event.nativeEvent.state stateRef.current = state if (state === PullToRefreshStateIdle) { lottieRef.current?.reset() setProgress(0) } else if (state === PullToRefreshStateRefreshing) { lottieRef.current?.play(progress) } else { HapticFeedback.trigger('impactLight') } }, [progress] ) return ( <PullToRefreshHeader style={styles.header} {...props} onOffsetChanged={onOffsetChanged} onStateChanged={onStateChanged}> <Lottie ref={lottieRef} style={{ height: 50 }} source={require('./square-loading.json')} autoPlay={false} speed={1} cacheStrategy={'strong'} loop progress={progress} /> </PullToRefreshHeader> ) }
  3. 设置你自定义的下拉刷新样式为默认样式。通常是在 APP 启动时设置:
    // index.js import { PullToRefresh } from '@sdcx/pull-to-refresh' import { LottiePullToRefreshHeader } from './LottiePullToRefreshHeader' PullToRefresh.setDefaultHeader(LottiePullToRefreshHeader)

此外,PullToRefresh 还支持上拉加载更多,使用方法和下拉刷新类似,具体参考它的文档open in new window。

BottomSheet

BottomSheetopen in new window 是一个类似于 Android 原生的 BottomSheetBehavioropen in new window 组件,我们在 API 设计上也尽量和 Android 原生保持一致。

可以实现如下效果:

安装

yarn add @sdcx/bottom-sheet

使用

BottomSheet 在使用上是非常简单的,没什么心智负担。

import BottomSheet from '@sdcx/bottom-sheet' const App = () => { return ( <View style={styles.container}> <ScrollView>...</ScrollView> <BottomSheet peeekHeight={200}> { // 在这里放置你的内容,可以是任何组件,如: } <View /> <PagerView> <FlatList nestedScrollEnabled /> <ScrollView nestedScrollEnabled /> <WebView nestedScrollEnabled /> </PagerView> </BottomSheet> </View> ) }

WARNING

注意为可滚动列表开启 nestedScrollEnabledopen in new window 属性

如果你熟悉原生 Android 开发,那么对 BottomSheet 的 API 并不陌生。

属性

  • peekHeight, 是指 BottomSheet 收起时,在屏幕上露出的高度,默认是 200。
  • state, 是指 BottomSheet 的状态,有三种状态:
    • 'collapsed',收起状态,此时 BottomSheet 的高度为 peekHeight
    • 'expanded',展开状态,此时 BottomSheet 的高度为父组件的高度或内容的高度,参考 fitToContents 属性。
    • 'hidden',隐藏状态,此时 BottomSheet 的高度为 0。
  • fitToContents,是指 BottomSheet 在展开时,是否适应内容的高度,默认是 false

回调

  • onStateChanged, 是指 BottomSheet 状态变化时的回调,它和 state 属性是一对,用来实现受控模式。
    export type BottomSheetState = 'collapsed' | 'expanded' | 'hidden' export interface StateChangedEventData { state: BottomSheetState } interface NativeBottomSheetProps extends ViewProps { onStateChanged?: (event: NativeSyntheticEvent<StateChangedEventData>) => void }
  • onSlide, 是指 BottomSheet 滑动时的回调,可以用它来实现一些动画效果。
    export interface OffsetChangedEventData { progress: number // 是指 BottomSheet 滑动的进度,范围是 0 到 1 offset: number // 是指 BottomSheet 滑动的距离,范围是 0 到 BottomSheet 的高度 expandedOffset: number // 是指 BottomSheet 展开时的 `offset` collapsedOffset: number // 是指 BottomSheet 收起时的 `offset` } interface NativeBottomSheetProps extends ViewProps { onSlide?: (event: NativeSyntheticEvent<OffsetChangedEventData>) => void }

源码

希望我们的经验对你有所帮助,如果你对我们的源码open in new window感兴趣,记得给颗星星哦。