import { useEffect, useRef, useContext, forwardRef, useMemo, createElement, ReactNode, useId } from 'react' import { Animated, StyleSheet, View, NativeSyntheticEvent, ViewStyle, LayoutChangeEvent, useAnimatedValue } from 'react-native' import { ScrollViewContext, StickyContext } from './context' import useNodesRef, { HandlerRef } from './useNodesRef' import { splitProps, splitStyle, useTransformStyle, wrapChildren, useLayout, extendObject } from './utils' import { error } from '@mpxjs/utils' import useInnerProps, { getCustomEvent } from './getInnerListeners' interface StickyHeaderProps { children?: ReactNode; style?: ViewStyle; padding?: [number, number, number, number]; 'offset-top'?: number; 'enable-var'?: boolean; 'external-var-context'?: Record; 'parent-font-size'?: number; 'parent-width'?: number; 'parent-height'?: number; bindstickontopchange?: (e: NativeSyntheticEvent) => void; } const _StickyHeader = forwardRef, StickyHeaderProps>((stickyHeaderProps: StickyHeaderProps = {}, ref): JSX.Element => { const { textProps, innerProps: props = {} } = splitProps(stickyHeaderProps) const { style, bindstickontopchange, padding = [0, 0, 0, 0], 'offset-top': offsetTop = 0, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props const { scrollOffset, gestureRef: scrollViewRef } = useContext(ScrollViewContext) const stickyContext = useContext(StickyContext) const { registerStickyHeader, unregisterStickyHeader } = stickyContext const headerRef = useRef(null) const isStickOnTopRef = useRef(false) const id = useId() const { normalStyle, hasVarDec, varContextRef, hasSelfPercent, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }) const { layoutRef, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: headerRef, onLayout }) const { textStyle, innerStyle = {} } = splitStyle(normalStyle) const headerTopAnimated = useAnimatedValue(0) // harmony animatedValue 不支持通过 _value 访问 const headerTopRef = useRef(0) useEffect(() => { registerStickyHeader({ key: id, updatePosition }) return () => { unregisterStickyHeader(id) } }, []) function updatePosition () { if (headerRef.current) { if (scrollViewRef && scrollViewRef.current) { headerRef.current.measureLayout( scrollViewRef.current, (left: number, top: number) => { Animated.timing(headerTopAnimated, { toValue: top, duration: 0, useNativeDriver: true }).start() headerTopRef.current = top } ) } else { error('StickyHeader measureLayout error: scrollViewRef is not a valid native component reference') } } } function onLayout (e: LayoutChangeEvent) { updatePosition() } useNodesRef(props, ref, headerRef, { style: normalStyle }) useEffect(() => { if (!bindstickontopchange || !scrollOffset) return const listener = scrollOffset.addListener((state: { value: number }) => { const currentScrollValue = state.value const newIsStickOnTop = currentScrollValue > headerTopRef.current if (newIsStickOnTop !== isStickOnTopRef.current) { isStickOnTopRef.current = newIsStickOnTop bindstickontopchange( getCustomEvent('stickontopchange', {}, { detail: { isStickOnTop: newIsStickOnTop }, layoutRef }, props)) } }) return () => { scrollOffset.removeListener(listener) } }, [bindstickontopchange, scrollOffset]) const animatedStyle = useMemo(() => { if (scrollOffset) { const translateY = Animated.subtract(scrollOffset, headerTopAnimated).interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolateLeft: 'clamp', extrapolateRight: 'extend' }) const finalTranslateY = offsetTop === 0 ? translateY : Animated.add( translateY, Animated.subtract(scrollOffset, headerTopAnimated).interpolate({ inputRange: [0, 1], outputRange: [0, offsetTop], extrapolate: 'clamp' }) ) return { transform: [{ translateY: finalTranslateY }] } } }, [scrollOffset, headerTopAnimated, offsetTop]) const innerProps = useInnerProps(extendObject({}, props, { ref: headerRef, style: extendObject({}, styles.content, innerStyle, animatedStyle, { paddingTop: padding[0] || 0, paddingRight: padding[1] || 0, paddingBottom: padding[2] || 0, paddingLeft: padding[3] || 0 }) }, layoutProps), [], { layoutRef }) return ( createElement( Animated.View, innerProps, wrapChildren( props, { hasVarDec, varContext: varContextRef.current, textStyle, textProps } ) ) ) }) const styles = StyleSheet.create({ content: { width: '100%', zIndex: 10, // harmony 需要手动设置 relative, zIndex 才生效 position: 'relative' } }) _StickyHeader.displayName = 'MpxStickyHeader' export default _StickyHeader