11-6 手势响应系统控制双重滚动

120 阅读2分钟

在这一章中,我们将学习如何使用 React Native 的手势响应系统来实现频道页面的双重滚动功能。以下是实现步骤:

一、手势响应系统概述

  1. React Native 官方手势响应系统

    • React Native 官方提供了一套手势响应系统,但它是基于 JavaScript 线程的,性能较差,且无法处理原生组件(如 ScrollView、Slider 等)的手势事件。
    • 官方手势响应系统无法阻止原生组件的默认行为,例如在 ScrollView 中创建一个不滚动的区域。
  2. react-native-gesture-handler

    • 社区提供了 react-native-gesture-handler 库,它将所有手势归纳为两种:持续手势(如拖动、旋转、捏合)和非持续手势(如点击、长按)。
    • 该库支持 iOS 的压力触控,并提供了专门的组件来监听特定的手势,如拖动组件、点击组件、长按组件等。

二、实现频道页面的双重滚动

1. 使用 PanGestureHandler 实现拖动效果

  1. 导入 PanGestureHandler

    import { PanGestureHandler } from 'react-native-gesture-handler';
    
  2. 创建动画值

    const translateY = new Animated.Value(0);
    
  3. 监听手势事件

    onGestureEvent = Animated.event(
      [{ nativeEvent: { translationY: this.translateY } }],
      { useNativeDriver: true }
    );
    
  4. 渲染组件

    <PanGestureHandler onGestureEvent={this.onGestureEvent}>
      <Animated.View
        style={[
          styles.container,
          { transform: [{ translateY: this.translateY }] },
        ]}
      >
        {/* 内容 */}
      </Animated.View>
    </PanGestureHandler>
    

2. 保存最后的高度

  1. 使用 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);
      }
    };
    
  2. 计算总的 Y 轴偏移值

    const scrollY = Animated.add(this.translateY, this.translateYOffset);
    

3. 限制滚动范围

  1. 使用 interpolate 限制滚动范围

    const HEADER_HEIGHT = 260;
    const RANGE = [-(HEADER_HEIGHT - this.headerHeight), 0];
    const scrollYRange = this.scrollY.interpolate({
      inputRange: RANGE,
      outputRange: RANGE,
      extrapolate: 'clamp',
    });
    
  2. 处理超出范围的情况

    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. 控制节目列表的滚动

  1. 嵌套 NativeViewGestureHandler

    <NativeViewGestureHandler ref={nativeRef}>
      <Animated.FlatList
        bounces={false}
        data={list}
        renderItem={this.renderItem}
        keyExtractor={this.keyExtractor}
      />
    </NativeViewGestureHandler>
    
  2. 设置 simultaneousHandlers

    <PanGestureHandler
      ref={this.panRef}
      simultaneousHandlers={[this.tapRef, this.nativeRef]}
      onGestureEvent={this.onGestureEvent}
      onHandlerStateChange={this.onHandlerStateChange}
    >
      {/* 内容 */}
    </PanGestureHandler>
    
  3. 传递 nativeRefpanRefTab 组件

    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 的滚动事件

  1. 监听滚动事件

    onScrollBeginDrag={this.onRegisterLastScroll}
    onScrollEndDrag={this.onRegisterLastScroll}
    
  2. 计算总的滚动偏移值

    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);
    
  3. 处理手势状态变化

    onHandlerStateChange = ({ nativeEvent }: PanGestureHandlerStateChangeEvent) => {
      const { translationY } = nativeEvent;
      translationY -= this.lastScrollYValue;
      this.translateYOffset.extractOffset();
      this.translateYOffset.setValue(translationY);
      this.translateYOffset.flattenOffset();
      this.translateY.setValue(0);
    };
    

6. 修改标题栏透明度

  1. 设置标题栏透明度

    navigation.setParams({
      opacity: this.scrollYRange.interpolate({
        inputRange: RANGE,
        outputRange: [1, 0],
      }),
    });
    
  2. 更新导航选项

    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 库实现了频道页面的双重滚动功能。通过嵌套多个手势响应组件,我们实现了在频道页面向上滑动时,节目列表无法滚动,直到频道信息组件完全不可见时,节目列表才能滚动的效果。下一节,我们将完成频道详情模块的开发,实现音频播放和弹幕功能。