React Native 嵌套滚动三件套
在 App 开发中,嵌套滚动是一个很常见的需求。比如一个可纵向滚动的页面中有一个可横向滚动的列表,列表中的每一项又是一个可纵向滚动的列表。这种情况下,我们就需要用到嵌套滚动。
譬如下拉刷新,向下滑动时,如果还没到顶部,列表内容滚动,如果已经到达顶部,则列表自身移动。
譬如 BottomSheet
,向上滑动时,如果 BottomSheet
还没完全展开,则 BottomSheet
移动,如果已经完全展开,则 BottomSheet
里面的列表滚动。
在 React Native,实现嵌套滚动非常不容易。
通常有两种实现方式:
借助 react-native-gesture-handler 和 reanimated 来实现。
封装原生 UI 组件来实现。
本文分享作者和他的伙伴们(以下也称作我们)通过封装原生 UI 组件来实现嵌套滚动的成果。
因为一共有三个组件,在 Andriod 都是通过 NestedScrolling API 来实现的,并且可以配合使用,所以我们将这三个组件称作嵌套滚动三件套。
NestedScrollView
NestedScrollView 用于实现题图的效果。
安装
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
注意为可滚动列表开启 nestedScrollEnabled 属性
NestedScrollView
和 NestedScrollViewHeader
都只有两三个属性,使用起来非常简单。
想要 sticky header 效果,可以配置 NestedScrollViewHeader
如下两个属性之一:
stickyHeaderBeginIndex
,它表示从第几个子组件开始,子组件将会被固定在顶部。stickyHeight
,它表示 header 多高的区域将会被固定在顶部,优先级高于stickyHeaderBeginIndex
。
PullToRefresh
React Native 内置的下拉刷新组件比较简陋,且 iOS 和 Android 平台的表现很不一致。幸运的是,它提供了一个 refreshControl
属性,可以用来自定义下拉刷新组件。
我们封装了一个原生组件 -- PullToRefresh,用来提供自定义下拉刷新的能力。
可以实现如下效果:
安装
yarn add @sdcx/pull-to-refresh
使用
PullToRefresh
在使用上主要有以下几个步骤:
WARNING
注意为可滚动列表开启 nestedScrollEnabled 属性
使用 PullToRefresh 提供的
RefreshControl
组件设置refreshControl
属性,并传递refreshing
和onRefresh
属性。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) }} /> } /> ) }
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> ) }
设置你自定义的下拉刷新样式为默认样式。通常是在 APP 启动时设置:
// index.js import { PullToRefresh } from '@sdcx/pull-to-refresh' import { LottiePullToRefreshHeader } from './LottiePullToRefreshHeader' PullToRefresh.setDefaultHeader(LottiePullToRefreshHeader)
此外,PullToRefresh 还支持上拉加载更多,使用方法和下拉刷新类似,具体参考它的文档。
BottomSheet
BottomSheet 是一个类似于 Android 原生的 BottomSheetBehavior 组件,我们在 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
注意为可滚动列表开启 nestedScrollEnabled 属性
如果你熟悉原生 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 }
源码
希望我们的经验对你有所帮助,如果你对我们的源码感兴趣,记得给颗星星哦。