react-native 封装一个基于 SectionList 的带有悬浮按钮的 SuspensionList

612 阅读2分钟

有一个需求就是点击某一个地方跳转到指定的 section 头部,我想很简单,于是我就是开始写了,因为我知道 sectionList 中有一个方法可以直接用:

这个就可以满足上面的需求,但是 UI 告诉我,不仅仅是这样,滑动的时候也会跟着变,于是我找呀找没有找到,于是我开始封装,我知道有一个方法能够获取当前组件距离屏幕顶部的距离:

然后我就想到我在每一个 section 头部组件中获取当前头部组件距离顶部的距离,这样滑动的时候就可以知道什么时候对应的标题发生改变。下面是源码:

//@ts-check

import React, {useCallback, useRef, useState} from 'react';
import {View, SectionList} from 'react-native';
import styles from './styles';

const sectionTops = []; // section距离上边的距离
let sectionListTop = 0; // sectionList 距离上边的距离
/**
 * @typedef {{
 *    Suspension: (currentIndex: number, jumpToCurrentIndex: (index: number) => void) => React.ReactElement | null,
 *    uniqueSectionKey: string,
 *    distanceTop: number,
 *    suspensionStyle?: import('react-native').ViewStyle
 *  }
 *  & import('react-native').SectionListProps
 * } SuspensionListProps
 * @param {SuspensionListProps} param0 props
 */
const SuspensionList = ({
  onScroll,
  uniqueSectionKey, // 这个对应的是section所在的下标,不然会出现问题
  sections,
  renderSectionHeader,
  Suspension, // 悬浮组件
  onScrollBeginDrag,
  distanceTop, // 这个是悬浮的组件距离上面组件的距离
  onMomentumScrollEnd,
  ListHeaderComponent,
  suspensionStyle, // 悬浮组件的样式
  ...props
}) => {
  const refs = [];
  const sectionListRef = useRef();
  const containerRef = useRef();
  const [selectedTabIndex, setTabIndex] = useState(0);
  const [currentY, setCurrentY] = useState(0);

  const onLayout = useCallback(
    (sectionIndex) => {
      // 当绘制完成后,记录每一个头部距离顶部的距离
      refs[sectionIndex]?.measure((x, y, width, height, pageX, pageY) => {
        sectionTops[sectionIndex] = pageY;
      });
    },
    [refs],
  );
  // 将所有的section组件的ref保存起来
  const getSectionHeaderRef = useCallback(
    (sectionIndex, el) => {
      refs[sectionIndex] = el;
    },
    [refs],
  );
  const _renderSectionHeader = useCallback(
    ({section}) => {
      const sectionIndex = section[uniqueSectionKey];
      return (
        <View
          onLayout={() => onLayout(sectionIndex)}
          ref={(el) => getSectionHeaderRef(sectionIndex, el)}>
          {renderSectionHeader?.({section})}
        </View>
      );
    },
    [getSectionHeaderRef, onLayout, uniqueSectionKey, renderSectionHeader],
  );
  const _onScroll = useCallback(
    (nativeEvent) => {
      const {
        nativeEvent: {
          contentOffset: {y},
        },
      } = nativeEvent;
      setCurrentY(y);
      onScroll?.(nativeEvent);
      // @ts-ignore
      if (!sectionListRef.current.isSlide) {
        return;
      }
      // 如果小于当前section下一个距离上面的高度,那么就设置,如果都不满足就是最后一个
      for (let i = 1; i < sectionTops.length; i++) {
        if (y <= sectionTops[i] - distanceTop) {
          setTabIndex(i - 1);
          break;
        } else {
          setTabIndex(sectionTops.length - 1);
        }
      }
    },
    [onScroll, distanceTop],
  );
  const jumpToCurrentIndex = useCallback(
    (index) => {
      // @ts-ignore
      sectionListRef.current?.scrollToLocation({
        itemIndex: 0,
        sectionIndex: index,
        viewPosition: 0,
        viewOffset: distanceTop - sectionListTop,
      });
      setTabIndex(index);
    },
    [distanceTop],
  );
  const SuspensionView = useCallback(() => {
    return (
      <View style={[styles.suspensionStyle, suspensionStyle]}>
        {Suspension?.(selectedTabIndex, jumpToCurrentIndex)}
      </View>
    );
  }, [Suspension, jumpToCurrentIndex, selectedTabIndex, suspensionStyle]);
  const _onScrollBeginDrag = useCallback(
    (event) => {
      onScrollBeginDrag?.(event);
      // @ts-ignore
      sectionListRef.current.isSlide = true;
    },
    [onScrollBeginDrag],
  );
  const _onMomentumScrollEnd = useCallback(
    (event) => {
      onMomentumScrollEnd?.(event);
      // @ts-ignore
      sectionListRef.current.isSlide = false;
    },
    [onMomentumScrollEnd],
  );
  const _ListHeaderComponent = useCallback(() => {
    // @ts-ignore
    return ListHeaderComponent?.(selectedTabIndex, jumpToCurrentIndex) || null;
  }, [ListHeaderComponent, selectedTabIndex, jumpToCurrentIndex]);
  const _onLayout = useCallback(() => {
    // @ts-ignore
    containerRef.current?.measure?.((x, y, width, height, pageX, pageY) => {
      sectionListTop = pageY;
    });
  }, []);
  return (
    <View style={styles.container} onLayout={_onLayout} ref={containerRef}>
      {currentY >= distanceTop && SuspensionView()}
      <SectionList
        sections={sections}
        onScroll={_onScroll}
        onScrollBeginDrag={_onScrollBeginDrag}
        ListHeaderComponent={_ListHeaderComponent}
        ref={sectionListRef}
        onMomentumScrollEnd={_onMomentumScrollEnd}
        renderSectionHeader={_renderSectionHeader}
        {...props}
      />
    </View>
  );
};

export default SuspensionList;

下面是 styles.js 文件:

import {StyleSheet} from 'react-native';
const styles = StyleSheet.create({
  container: {flex: 1},
  suspensionStyle: {
    position: 'absolute',
    zIndex: 9,
    width: '100%',
  },
});
export default styles;

就这样就得到了封面大图的效果。