UNPKG

@mpxjs/webpack-plugin

Version:

mpx compile core

260 lines (259 loc) 11.1 kB
import React, { forwardRef, useRef, useState, useMemo, useEffect, useCallback, createElement } from 'react'; import { StyleSheet, View } from 'react-native'; import Reanimated, { useAnimatedRef, useScrollViewOffset } from 'react-native-reanimated'; import { useTransformStyle, splitStyle, splitProps, useLayout, usePrevious, isAndroid, isIOS, isHarmony, extendObject } from '../utils'; import useNodesRef from '../useNodesRef'; import PickerIndicator from './pickerViewIndicator'; import PickerMask from './pickerViewMask'; import MpxPickerVIewColumnItem from './pickerViewColumnItem'; import { PickerViewColumnAnimationContext } from '../mpx-picker-view/pickerVIewContext'; import { calcHeightOffsets } from './pickerViewFaces'; const visibleCount = 5; const _PickerViewColumn = forwardRef((props, ref) => { const { columnData, columnIndex, initialIndex, onSelectChange, style, wrapperStyle, pickerMaskStyle, pickerIndicatorStyle, 'enable-var': enableVar, 'external-var-context': externalVarContext } = props; const { normalStyle, hasSelfPercent, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext }); const { textStyle = {} } = splitStyle(normalStyle); const { textProps = {} } = splitProps(props); const scrollViewRef = useAnimatedRef(); const offsetYShared = useScrollViewOffset(scrollViewRef); useNodesRef(props, ref, scrollViewRef, { style: normalStyle }); const { height: pickerH, itemHeight } = wrapperStyle; const [itemRawH, setItemRawH] = useState(itemHeight); const maxIndex = useMemo(() => columnData.length - 1, [columnData]); const prevScrollingInfo = useRef({ index: initialIndex, y: 0 }); const dragging = useRef(false); const scrolling = useRef(false); const timerResetPosition = useRef(null); const timerScrollTo = useRef(null); const timerClickOnce = useRef(null); const activeIndex = useRef(initialIndex); const prevIndex = usePrevious(initialIndex); const prevMaxIndex = usePrevious(maxIndex); const { layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef }); const paddingHeight = useMemo(() => Math.round((pickerH - itemHeight) / 2), [pickerH, itemHeight]); const snapToOffsets = useMemo(() => Array.from({ length: maxIndex + 1 }, (_, i) => i * itemRawH), [maxIndex, itemRawH]); const contentContainerStyle = useMemo(() => { return [{ paddingVertical: paddingHeight }]; }, [paddingHeight]); const getIndex = useCallback((y) => { const calc = Math.round(y / itemRawH); return Math.max(0, Math.min(calc, maxIndex)); }, [itemRawH, maxIndex]); const clearTimerResetPosition = useCallback(() => { if (timerResetPosition.current) { clearTimeout(timerResetPosition.current); timerResetPosition.current = null; } }, []); const clearTimerScrollTo = useCallback(() => { if (timerScrollTo.current) { clearTimeout(timerScrollTo.current); timerScrollTo.current = null; } }, []); const clearTimerClickOnce = useCallback(() => { if (timerClickOnce.current) { clearTimeout(timerClickOnce.current); timerClickOnce.current = null; } }, []); useEffect(() => { return () => { clearTimerResetPosition(); clearTimerScrollTo(); }; }, []); useEffect(() => { if (!scrollViewRef.current || !itemRawH || dragging.current || scrolling.current || prevIndex == null || initialIndex === prevIndex || initialIndex === activeIndex.current || maxIndex !== prevMaxIndex) { return; } clearTimerScrollTo(); timerScrollTo.current = setTimeout(() => { scrollViewRef.current?.scrollTo({ x: 0, y: initialIndex * itemRawH, animated: false }); activeIndex.current = initialIndex; }, isIOS ? 0 : 200); }, [itemRawH, maxIndex, initialIndex]); const onContentSizeChange = useCallback((_w, h) => { const y = initialIndex * itemRawH; if (y <= h) { clearTimerScrollTo(); timerScrollTo.current = setTimeout(() => { scrollViewRef.current?.scrollTo({ x: 0, y, animated: false }); activeIndex.current = initialIndex; }, 0); } }, [itemRawH, initialIndex]); const onItemLayout = useCallback((e) => { const { height: rawH } = e.nativeEvent.layout; const roundedH = Math.round(rawH); if (roundedH && roundedH !== itemRawH) { setItemRawH(roundedH); } }, [itemRawH]); const resetScrollPosition = useCallback((y) => { if (dragging.current || scrolling.current) { return; } scrolling.current = true; const targetIndex = getIndex(y); scrollViewRef.current?.scrollTo({ x: 0, y: targetIndex * itemRawH, animated: false }); }, [itemRawH, getIndex]); const onMomentumScrollBegin = useCallback(() => { isIOS && clearTimerResetPosition(); scrolling.current = true; }, []); const onMomentumScrollEnd = useCallback((e) => { scrolling.current = false; const { y: scrollY } = e.nativeEvent.contentOffset; if (isIOS && scrollY % itemRawH !== 0) { return resetScrollPosition(scrollY); } const calcIndex = getIndex(scrollY); if (calcIndex !== activeIndex.current) { activeIndex.current = calcIndex; onSelectChange(calcIndex); } }, [itemRawH, getIndex, onSelectChange, resetScrollPosition]); const onScrollBeginDrag = useCallback(() => { isIOS && clearTimerResetPosition(); dragging.current = true; prevScrollingInfo.current = { index: activeIndex.current, y: activeIndex.current * itemRawH }; }, [itemRawH]); const onScrollEndDrag = useCallback((e) => { dragging.current = false; if (!isAndroid) { const { y } = e.nativeEvent.contentOffset; if (y % itemRawH === 0 || (isHarmony && y > snapToOffsets[maxIndex])) { onMomentumScrollEnd({ nativeEvent: { contentOffset: { y } } }); } else if (y > 0 && y < snapToOffsets[maxIndex]) { timerResetPosition.current = setTimeout(() => { resetScrollPosition(y); }, 10); } } }, [itemRawH, maxIndex, snapToOffsets, onMomentumScrollEnd, resetScrollPosition]); const onScroll = useCallback((e) => { // 全局注册的振动触感 hook const pickerVibrate = global.__mpx?.config?.rnConfig?.pickerVibrate; if (typeof pickerVibrate !== 'function') { return; } const { y } = e.nativeEvent.contentOffset; const { index: prevIndex, y: _y } = prevScrollingInfo.current; if (dragging.current || scrolling.current) { if (Math.abs(y - _y) >= itemRawH) { const currentId = getIndex(y); if (currentId !== prevIndex) { prevScrollingInfo.current = { index: currentId, y: currentId * itemRawH }; // vibrateShort({ type: 'selection' }) pickerVibrate(); } } } }, [itemRawH, getIndex]); const offsetHeights = useMemo(() => calcHeightOffsets(itemRawH), [itemRawH]); const calcOffset = useCallback((y) => { const baselineY = activeIndex.current * itemRawH + pickerH / 2; const diff = Math.abs(y - baselineY); const positive = y - baselineY > 0 ? 1 : -1; const [h1, h2, h3] = offsetHeights; if (diff > h1 && diff < h3) { if (diff < h2) { return 1 * positive; } else { return 2 * positive; } } return false; }, [offsetHeights]); /** * 和小程序表现对齐,点击(不滑动)非焦点选项自动滚动到对应位置 */ const onClickOnceItem = useCallback((e) => { const { locationY } = e.nativeEvent || {}; const offsetIndex = calcOffset(locationY); if (dragging.current || !offsetIndex) { return; } const targetIndex = activeIndex.current + offsetIndex; if (targetIndex < 0 || targetIndex > maxIndex) { return; } const y = targetIndex * itemRawH; scrollViewRef.current?.scrollTo({ x: 0, y, animated: true }); if (isAndroid) { // Android scrollTo 不会自动触发 onMomentumScrollEnd,需要手动触发 clearTimerClickOnce(); timerClickOnce.current = setTimeout(() => { onMomentumScrollEnd({ nativeEvent: { contentOffset: { y } } }); }, 250); } }, [itemRawH, maxIndex, calcOffset, onMomentumScrollEnd]); const renderInnerchild = () => columnData.map((item, index) => { return (<MpxPickerVIewColumnItem key={index} item={item} index={index} itemHeight={itemHeight} textStyle={textStyle} textProps={textProps} visibleCount={visibleCount} onItemLayout={onItemLayout}/>); }); const renderScollView = () => { const innerProps = extendObject({}, layoutProps, { ref: scrollViewRef, bounces: true, horizontal: false, nestedScrollEnabled: true, removeClippedSubviews: false, showsVerticalScrollIndicator: false, showsHorizontalScrollIndicator: false, scrollEventThrottle: 16, style: styles.scrollView, decelerationRate: 'fast', snapToOffsets: snapToOffsets, onTouchEnd: onClickOnceItem, onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, onContentSizeChange, contentContainerStyle }); return createElement(PickerViewColumnAnimationContext.Provider, { value: offsetYShared }, createElement(Reanimated.ScrollView, innerProps, renderInnerchild())); }; const renderIndicator = () => (<PickerIndicator itemHeight={itemHeight} indicatorItemStyle={pickerIndicatorStyle}/>); const renderMask = () => (<PickerMask itemHeight={itemHeight} maskContainerStyle={pickerMaskStyle}/>); return (<View style={[styles.wrapper, normalStyle]}> {renderScollView()} {renderMask()} {renderIndicator()} </View>); }); const styles = StyleSheet.create({ wrapper: { display: 'flex', flex: 1 }, scrollView: { width: '100%' } }); _PickerViewColumn.displayName = 'MpxPickerViewColumn'; export default _PickerViewColumn;