import React, { forwardRef, useState, useMemo, useRef, useImperativeHandle, useEffect, } from 'react'; import type { ReactNode, ReactElement } from 'react'; import { View, Text, ScrollView, UIManager, findNodeHandle } from 'react-native'; import type { ScrollViewProps } from 'react-native'; import isNumber from 'lodash-es/isNumber'; import { useThemeFactory } from '../Theme'; import { Portal } from '../Portal'; import { useRefs } from '../hooks'; import constants, { COMPONENT_TYPE_KEY } from '../utils/constants'; import type { IndexBarProps, IndexBarInstance, IndexAnchorInstance, LayoutRectangle, } from './types'; import IndexBarContext from './IndexBarContext'; import { INDEX_ANCHORE_KEY } from './IndexAnchor'; import { createStyle } from './style'; const genAlphabet = () => { const indexList = []; const charCodeOfA = 'A'.charCodeAt(0); for (let i = 0; i < 26; i += 1) { indexList.push(String.fromCharCode(charCodeOfA + i)); } return indexList; }; const IndexBar = forwardRef((props, ref) => { const { children, sticky = true, zIndex, highlightColor, indexList = genAlphabet(), onChange, onSelect, ...rest } = props; const { styles } = useThemeFactory(createStyle); const [activeAnchor, setActiveAnchor] = useState(); const scrollViewRef = useRef(null); const [refs, setRefs] = useRefs(); // 获取 ScrollView 相对屏幕的位置 const getScrollViewOffset = () => { return new Promise<{ left: number; top: number }>(resolve => { UIManager.measure( findNodeHandle(scrollViewRef.current)!, (_x, _y, _width, _height, pageX, pageY) => { resolve({ left: pageX, top: pageY }); } ); }); }; const getActiveAnchor = (scrollViewTop: number, rects: LayoutRectangle[]) => { const relativeTops = rects.map(it => it.pageY - scrollViewTop); return relativeTops.filter(it => it < 0).length - 1; }; const getAnchorRects = () => Promise.all(Object.values(refs).map(anchor => anchor.getRect())); const onScroll: ScrollViewProps['onScroll'] = async () => { const rects = await getAnchorRects(); const { top: scrollViewTop, left: scrollViewLeft } = await getScrollViewOffset(); const active = getActiveAnchor(scrollViewTop, rects); setActiveAnchor(indexList[active]); if (sticky) { Object.values(refs).forEach((item, index) => { const currentRect = rects[index]; const currentState = item.state; const isActive = active === index; if (isActive === currentState.active) return; item.updateState({ top: scrollViewTop, left: scrollViewLeft, width: currentRect.width, height: currentRect.height, active: isActive, }); }); } }; const scrollTo = async (index: number | string) => { const activeIndex = indexList.findIndex(it => it === index); if (isNumber(activeIndex)) { onSelect?.(index); const rects = await getAnchorRects(); const activeRect = rects[activeIndex]; scrollViewRef.current?.scrollTo({ y: activeRect.y + 1, animated: false }); } }; useImperativeHandle(ref, () => ({ scrollTo, })); useEffect(() => { activeAnchor && onChange?.(activeAnchor); }, [activeAnchor]); const handleMapChildren = ($children: ReactNode): any => { return React.Children.toArray($children) .filter(React.isValidElement) .map((child: ReactElement) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (child.type?.[COMPONENT_TYPE_KEY] === INDEX_ANCHORE_KEY) { return React.cloneElement(child, { ref: setRefs(child.props.index), }); } if (child.props?.children) { const deepMap = handleMapChildren(child.props.children); return deepMap.length ? deepMap : child; } return child; }); }; const memoChildren = useMemo(() => handleMapChildren(children), [children]); const renderSidebar = () => { const sidebarHeight = styles.index.lineHeight! * indexList.length; const offsetTop = (constants.screenHeight - sidebarHeight) / 2; return ( {indexList?.map(index => { const active = index === activeAnchor; const highlightStyle = highlightColor ? { color: highlightColor } : null; return ( scrollTo(index)} style={[ styles.index, { top: offsetTop }, active && styles.indexActive, active && highlightStyle, ]} > {index} ); })} ); }; return ( {renderSidebar()} {memoChildren} ); }); export default IndexBar;