在这一章中,我们将学习如何使用 React Native 的手势响应系统来实现频道页面的双重滚动功能。以下是实现步骤:
一、手势响应系统概述
-
React Native 官方手势响应系统
- React Native 官方提供了一套手势响应系统,但它是基于 JavaScript 线程的,性能较差,且无法处理原生组件(如 ScrollView、Slider 等)的手势事件。
- 官方手势响应系统无法阻止原生组件的默认行为,例如在 ScrollView 中创建一个不滚动的区域。
-
react-native-gesture-handler
- 社区提供了
react-native-gesture-handler库,它将所有手势归纳为两种:持续手势(如拖动、旋转、捏合)和非持续手势(如点击、长按)。 - 该库支持 iOS 的压力触控,并提供了专门的组件来监听特定的手势,如拖动组件、点击组件、长按组件等。
- 社区提供了
二、实现频道页面的双重滚动
1. 使用 PanGestureHandler 实现拖动效果
-
导入
PanGestureHandlerimport { PanGestureHandler } from 'react-native-gesture-handler'; -
创建动画值
const translateY = new Animated.Value(0); -
监听手势事件
onGestureEvent = Animated.event( [{ nativeEvent: { translationY: this.translateY } }], { useNativeDriver: true } ); -
渲染组件
<PanGestureHandler onGestureEvent={this.onGestureEvent}> <Animated.View style={[ styles.container, { transform: [{ translateY: this.translateY }] }, ]} > {/* 内容 */} </Animated.View> </PanGestureHandler>
2. 保存最后的高度
-
使用
onHandlerStateChange保存最后的高度onHandlerStateChange = ({ nativeEvent }: PanGestureHandlerStateChangeEvent) => { if (nativeEvent.oldState === State.ACTIVE) { const { translationY } = nativeEvent; this.translateYOffset.extractOffset(); this.translateYOffset.setValue(translationY); this.translateYOffset.flattenOffset(); this.translateY.setValue(0); } }; -
计算总的 Y 轴偏移值
const scrollY = Animated.add(this.translateY, this.translateYOffset);
3. 限制滚动范围
-
使用
interpolate限制滚动范围const HEADER_HEIGHT = 260; const RANGE = [-(HEADER_HEIGHT - this.headerHeight), 0]; const scrollYRange = this.scrollY.interpolate({ inputRange: RANGE, outputRange: RANGE, extrapolate: 'clamp', }); -
处理超出范围的情况
if (lastValue < RANGE[0]) { this.lastTranslationYValue = RANGE[0]; Animated.spring(this.translateYOffset, { toValue: RANGE[0], tension: 40, friction: 5, useNativeDriver: true, }).start(); } else if (lastValue > RANGE[1]) { this.lastTranslationYValue = RANGE[1]; Animated.spring(this.translateYOffset, { toValue: RANGE[1], tension: 40, friction: 5, useNativeDriver: true, }).start(); }
4. 控制节目列表的滚动
-
嵌套
NativeViewGestureHandler<NativeViewGestureHandler ref={nativeRef}> <Animated.FlatList bounces={false} data={list} renderItem={this.renderItem} keyExtractor={this.keyExtractor} /> </NativeViewGestureHandler> -
设置
simultaneousHandlers<PanGestureHandler ref={this.panRef} simultaneousHandlers={[this.tapRef, this.nativeRef]} onGestureEvent={this.onGestureEvent} onHandlerStateChange={this.onHandlerStateChange} > {/* 内容 */} </PanGestureHandler> -
传递
nativeRef和panRef到Tab组件renderScene = ({ route }: { route: IRoute }) => { const { nativeRef, panRef, tapRef } = this.props; switch (route.key) { case 'introduction': return <Introduction />; case 'albums': return <List nativeRef={nativeRef} panRef={panRef} tapRef={tapRef} />; } };
5. 处理 FlatList 的滚动事件
-
监听滚动事件
onScrollBeginDrag={this.onRegisterLastScroll} onScrollEndDrag={this.onRegisterLastScroll} -
计算总的滚动偏移值
const lastScrollYValue = 0; const lastScrollY = new Animated.Value(0); onRegisterLastScroll = Animated.event( [{ nativeEvent: { contentOffset: { y: this.lastScrollY } } }], { useNativeDriver: true, listener: ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => { this.lastScrollYValue = nativeEvent.contentOffset.y; }, } ); const reverseLastScrollY = Animated.multiply(new Animated.Value(-1), this.lastScrollY); const scrollY = Animated.add(Animated.add(this.translateY, reverseLastScrollY), this.translateYOffset); -
处理手势状态变化
onHandlerStateChange = ({ nativeEvent }: PanGestureHandlerStateChangeEvent) => { const { translationY } = nativeEvent; translationY -= this.lastScrollYValue; this.translateYOffset.extractOffset(); this.translateYOffset.setValue(translationY); this.translateYOffset.flattenOffset(); this.translateY.setValue(0); };
6. 修改标题栏透明度
-
设置标题栏透明度
navigation.setParams({ opacity: this.scrollYRange.interpolate({ inputRange: RANGE, outputRange: [1, 0], }), }); -
更新导航选项
function getAlbumOptions({ route, }: { route: RouteProp<RootStackParamList, 'Album'>; }) { return { headerTitle: route.params.item.title, headerTransparent: true, headerTitleStyle: { opacity: route.params.opacity, }, headerBackground: () => ( <Animated.View style={[styles.headerBackground, { opacity: route.params.opacity }]} /> ), }; }
三、总结
在本节中,我们使用 react-native-gesture-handler 库实现了频道页面的双重滚动功能。通过嵌套多个手势响应组件,我们实现了在频道页面向上滑动时,节目列表无法滚动,直到频道信息组件完全不可见时,节目列表才能滚动的效果。下一节,我们将完成频道详情模块的开发,实现音频播放和弹幕功能。