diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/movable-view.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/movable-view.js index 77d53b44ed..f24b45bf8a 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/movable-view.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/movable-view.js @@ -2,6 +2,8 @@ const TAG_NAME = 'movable-view' module.exports = function ({ print }) { const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const androidEventLog = print({ platform: 'android', tag: TAG_NAME, isError: false, type: 'event' }) + const iosEventLog = print({ platform: 'ios', tag: TAG_NAME, isError: false, type: 'event' }) const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) const androidPropLog = print({ platform: 'android', tag: TAG_NAME, isError: false }) const iosPropLog = print({ platform: 'ios', tag: TAG_NAME, isError: false }) @@ -27,7 +29,7 @@ module.exports = function ({ print }) { android: androidPropLog }, { - test: /^(inertia|damping|animation)$/, + test: /^(damping|friction|scale|scale-min|scale-max|scale-value)$/, ios: iosPropLog, android: androidPropLog } @@ -36,6 +38,11 @@ module.exports = function ({ print }) { { test: /^(htouchmove|vtouchmove)$/, ali: aliEventLog + }, + { + test: /^(bindscale)$/, + ios: iosEventLog, + android: androidEventLog } ] } diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/scroll-view.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/scroll-view.js index a5e426e398..0987235c64 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/scroll-view.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/scroll-view.js @@ -53,7 +53,7 @@ module.exports = function ({ print }) { qa: qaPropLog }, { - test: /^(scroll-into-view|refresher-threshold|enable-passive|scroll-anchoring|using-sticky|fast-deceleration|enable-flex)$/, + test: /^(refresher-threshold|enable-passive|scroll-anchoring|using-sticky|fast-deceleration|enable-flex)$/, android: androidPropLog, ios: iosPropLog }, diff --git a/packages/webpack-plugin/lib/runtime/components/react/context.ts b/packages/webpack-plugin/lib/runtime/components/react/context.ts index 5bb14fc15d..39472e6744 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/context.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/context.ts @@ -33,6 +33,10 @@ export interface IntersectionObserver { } } +export interface ScrollViewContextValue { + gestureRef: React.RefObject | null +} + export const MovableAreaContext = createContext({ width: 0, height: 0 }) export const FormContext = createContext(null) @@ -52,3 +56,5 @@ export const IntersectionObserverContext = createContext(null) export const KeyboardAvoidContext = createContext(null) + +export const ScrollViewContext = createContext({ gestureRef: null }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx index 004afef42c..56881b4a39 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx @@ -34,7 +34,7 @@ * ✘ bindagreeprivacyauthorization * ✔ bindtap */ -import { createElement, useEffect, useRef, useState, ReactNode, forwardRef, useContext, JSX } from 'react' +import { createElement, useEffect, useRef, ReactNode, forwardRef, useContext, JSX } from 'react' import { View, StyleSheet, @@ -45,10 +45,12 @@ import { NativeSyntheticEvent } from 'react-native' import { warn } from '@mpxjs/utils' -import { getCurrentPage, splitProps, splitStyle, useLayout, useTransformStyle, wrapChildren, extendObject } from './utils' +import { GestureDetector, PanGesture } from 'react-native-gesture-handler' +import { getCurrentPage, splitProps, splitStyle, useLayout, useTransformStyle, wrapChildren, extendObject, useHover } from './utils' import useInnerProps, { getCustomEvent } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { RouteContext, FormContext } from './context' +import type { ExtendedViewStyle } from './types/common' export type Type = 'default' | 'primary' | 'warn' @@ -68,7 +70,7 @@ export interface ButtonProps { disabled?: boolean loading?: boolean 'hover-class'?: string - 'hover-style'?: ViewStyle & TextStyle & Record + 'hover-style'?: ExtendedViewStyle 'hover-start-time'?: number 'hover-stay-time'?: number 'open-type'?: OpenType @@ -83,8 +85,6 @@ export interface ButtonProps { children: ReactNode bindgetuserinfo?: (userInfo: any) => void bindtap?: (evt: NativeSyntheticEvent | unknown) => void - bindtouchstart?: (evt: NativeSyntheticEvent | unknown) => void - bindtouchend?: (evt: NativeSyntheticEvent | unknown) => void } const LOADING_IMAGE_URI = @@ -216,15 +216,16 @@ const Button = forwardRef, ButtonProps>((buttonPro style = {}, children, bindgetuserinfo, - bindtap, - bindtouchstart, - bindtouchend + bindtap } = props const pageId = useContext(RouteContext) const formContext = useContext(FormContext) + const enableHover = hoverClass !== 'none' + const { isHover, gesture } = useHover({ enableHover, hoverStartTime, hoverStayTime, disabled }) + let submitFn: () => void | undefined let resetFn: () => void | undefined @@ -233,27 +234,15 @@ const Button = forwardRef, ButtonProps>((buttonPro resetFn = formContext.reset } - const refs = useRef<{ - hoverStartTimer: ReturnType | undefined - hoverStayTimer: ReturnType | undefined - }>({ - hoverStartTimer: undefined, - hoverStayTimer: undefined - }) - - const [isHover, setIsHover] = useState(false) - const isMiniSize = size === 'mini' - const applyHoverEffect = isHover && hoverClass !== 'none' - const [color, hoverColor, plainColor, disabledColor] = TypeColorMap[type] - const normalBackgroundColor = disabled ? disabledColor : applyHoverEffect || loading ? hoverColor : color + const normalBackgroundColor = disabled ? disabledColor : isHover || loading ? hoverColor : color const plainBorderColor = disabled ? 'rgba(0, 0, 0, .2)' - : applyHoverEffect + : isHover ? `rgba(${plainColor},.6)` : `rgb(${plainColor})` @@ -261,14 +250,14 @@ const Button = forwardRef, ButtonProps>((buttonPro const plainTextColor = disabled ? 'rgba(0, 0, 0, .2)' - : applyHoverEffect + : isHover ? `rgba(${plainColor}, .6)` : `rgb(${plainColor})` const normalTextColor = type === 'default' - ? `rgba(0, 0, 0, ${disabled ? 0.3 : applyHoverEffect || loading ? 0.6 : 1})` - : `rgba(255 ,255 ,255 , ${disabled || applyHoverEffect || loading ? 0.6 : 1})` + ? `rgba(0, 0, 0, ${disabled ? 0.3 : isHover || loading ? 0.6 : 1})` + : `rgba(255 ,255 ,255 , ${disabled || isHover || loading ? 0.6 : 1})` const viewStyle = { borderWidth: 1, @@ -297,7 +286,7 @@ const Button = forwardRef, ButtonProps>((buttonPro {}, defaultStyle, style, - applyHoverEffect ? hoverStyle : {} + isHover ? hoverStyle : {} ) const { @@ -366,34 +355,6 @@ const Button = forwardRef, ButtonProps>((buttonPro } } - const setStayTimer = () => { - clearTimeout(refs.current.hoverStayTimer) - refs.current.hoverStayTimer = setTimeout(() => { - setIsHover(false) - clearTimeout(refs.current.hoverStayTimer) - }, hoverStayTime) - } - - const setStartTimer = () => { - clearTimeout(refs.current.hoverStartTimer) - refs.current.hoverStartTimer = setTimeout(() => { - setIsHover(true) - clearTimeout(refs.current.hoverStartTimer) - }, hoverStartTime) - } - - const onTouchStart = (evt: NativeSyntheticEvent) => { - bindtouchstart && bindtouchstart(evt) - if (disabled) return - setStartTimer() - } - - const onTouchEnd = (evt: NativeSyntheticEvent) => { - bindtouchend && bindtouchend(evt) - if (disabled) return - setStayTimer() - } - const handleFormTypeFn = () => { if (formType === 'submit') { submitFn && submitFn() @@ -418,8 +379,6 @@ const Button = forwardRef, ButtonProps>((buttonPro }, layoutProps, { - bindtouchstart: (bindtouchstart || !disabled) && onTouchStart, - bindtouchend: (bindtouchend || !disabled) && onTouchEnd, bindtap: !disabled && onTap } ), @@ -442,7 +401,7 @@ const Button = forwardRef, ButtonProps>((buttonPro } ) - return createElement(View, innerProps, loading && createElement(Loading, { alone: !children }), + const baseButton = createElement(View, innerProps, loading && createElement(Loading, { alone: !children }), wrapChildren( props, { @@ -453,6 +412,10 @@ const Button = forwardRef, ButtonProps>((buttonPro } ) ) + + return enableHover + ? createElement(GestureDetector, { gesture: gesture as PanGesture }, baseButton) + : baseButton }) Button.displayName = 'MpxButton' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx index b6410e9034..905385b43e 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx @@ -11,7 +11,7 @@ * ✘ scale-min * ✘ scale-max * ✘ scale-value - * ✘ animation + * ✔ animation * ✔ bindchange * ✘ bindscale * ✔ htouchmove diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx index db6097df5d..7e43b48c4b 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx @@ -3,10 +3,10 @@ * ✔ nodes */ import { View, ViewProps, ViewStyle } from 'react-native' -import { useRef, forwardRef, JSX, useState } from 'react' +import { useRef, forwardRef, JSX, useState, createElement } from 'react' import useInnerProps from '../getInnerListeners' import useNodesRef, { HandlerRef } from '../useNodesRef' // 引入辅助函数 -import { useTransformStyle, useLayout } from '../utils' +import { useTransformStyle, useLayout, extendObject } from '../utils' import { WebView, WebViewMessageEvent } from 'react-native-webview' import { generateHTML } from './html' @@ -91,28 +91,22 @@ const _RichText = forwardRef, _RichTextProps>(( layoutRef }) - const innerProps = useInnerProps(props, { + const innerProps = useInnerProps(props, extendObject({ ref: nodeRef, - style: { ...normalStyle, ...layoutStyle }, - ...layoutProps - }, [], { + style: extendObject(normalStyle, layoutStyle) + }, layoutProps), [], { layoutRef }) const html: string = typeof nodes === 'string' ? nodes : jsonToHtmlStr(nodes) - return ( - - { - setWebViewHeight(+event.nativeEvent.data) - }} - > - - + return createElement(View, innerProps, + createElement(WebView, { + source: { html: generateHTML(html) }, + onMessage: (event: WebViewMessageEvent) => { + setWebViewHeight(+event.nativeEvent.data) + } + }) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx index 918a0cfb59..6a7ab2e997 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx @@ -33,13 +33,13 @@ */ import { ScrollView } from 'react-native-gesture-handler' import { View, RefreshControl, NativeSyntheticEvent, NativeScrollEvent, LayoutChangeEvent, ViewStyle } from 'react-native' -import { JSX, ReactNode, RefObject, useRef, useState, useEffect, forwardRef, useContext, createElement } from 'react' +import { JSX, ReactNode, RefObject, useRef, useState, useEffect, forwardRef, useContext, createElement, useMemo } from 'react' import { useAnimatedRef } from 'react-native-reanimated' import { warn } from '@mpxjs/utils' import useInnerProps, { getCustomEvent } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { splitProps, splitStyle, useTransformStyle, useLayout, wrapChildren, extendObject, flatGesture, GestureHandler } from './utils' -import { IntersectionObserverContext } from './context' +import { IntersectionObserverContext, ScrollViewContext } from './context' interface ScrollViewProps { children?: ReactNode; @@ -194,6 +194,12 @@ const _ScrollView = forwardRef, S gestureRef: scrollViewRef }) + const contextValue = useMemo(() => { + return { + gestureRef: scrollViewRef + } + }, []) + const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout }) if (scrollX && scrollY) { @@ -509,14 +515,17 @@ const _ScrollView = forwardRef, S }, (refresherDefaultStyle && refresherDefaultStyle !== 'none' ? { colors: refreshColor[refresherDefaultStyle] } : null))) : undefined }), - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } + createElement(ScrollViewContext.Provider, + { value: contextValue }, + wrapChildren( + props, + { + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps + } + ) ) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx index 9939bff0a1..7d8cdbc94b 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx @@ -12,9 +12,10 @@ import useAnimationHooks from './useAnimationHooks' import type { AnimationProp } from './useAnimationHooks' import { ExtendedViewStyle } from './types/common' import useNodesRef, { HandlerRef } from './useNodesRef' -import { parseUrl, PERCENT_REGEX, splitStyle, splitProps, useTransformStyle, wrapChildren, useLayout, renderImage, pickStyle, extendObject } from './utils' +import { parseUrl, PERCENT_REGEX, splitStyle, splitProps, useTransformStyle, wrapChildren, useLayout, renderImage, pickStyle, extendObject, useHover } from './utils' import { error } from '@mpxjs/utils' import LinearGradient from 'react-native-linear-gradient' +import { GestureDetector, PanGesture } from 'react-native-gesture-handler' export interface _ViewProps extends ViewProps { style?: ExtendedViewStyle @@ -642,7 +643,7 @@ function useWrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record @@ -684,8 +685,6 @@ const _View = forwardRef, _ViewProps>((viewProps, r animation } = props - const [isHover, setIsHover] = useState(false) - // 默认样式 const defaultStyle: ExtendedViewStyle = style.display === 'flex' ? { @@ -696,6 +695,9 @@ const _View = forwardRef, _ViewProps>((viewProps, r } : {} + const enableHover = !!hoverStyle + const { isHover, gesture } = useHover({ enableHover, hoverStartTime, hoverStayTime }) + const styleObj: ExtendedViewStyle = extendObject({}, defaultStyle, style, isHover ? hoverStyle as ExtendedViewStyle : {}) const { @@ -726,45 +728,6 @@ const _View = forwardRef, _ViewProps>((viewProps, r style: normalStyle }) - const dataRef = useRef<{ - startTimer?: ReturnType - stayTimer?: ReturnType - }>({}) - - useEffect(() => { - return () => { - dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer) - dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer) - } - }, []) - - const setStartTimer = () => { - dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer) - dataRef.current.startTimer = setTimeout(() => { - setIsHover(true) - }, +hoverStartTime) - } - - const setStayTimer = () => { - dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer) - dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer) - dataRef.current.stayTimer = setTimeout(() => { - setIsHover(false) - }, +hoverStayTime) - } - - function onTouchStart (e: NativeSyntheticEvent) { - const { bindtouchstart } = props - bindtouchstart && bindtouchstart(e) - setStartTimer() - } - - function onTouchEnd (e: NativeSyntheticEvent) { - const { bindtouchend } = props - bindtouchend && bindtouchend(e) - setStayTimer() - } - const { layoutRef, layoutStyle, @@ -773,33 +736,19 @@ const _View = forwardRef, _ViewProps>((viewProps, r const viewStyle = extendObject({}, innerStyle, layoutStyle) - enableAnimation = enableAnimation || !!animation - const enableAnimationRef = useRef(enableAnimation) - if (enableAnimationRef.current !== enableAnimation) { - error('[Mpx runtime error]: animation use should be stable in the component lifecycle, or you can set [enable-animation] with true.') - } + const { enableStyleAnimation, animationStyle } = useAnimationHooks({ + enableAnimation, + animation, + style: viewStyle + }) - const finalStyle = enableAnimationRef.current - ? [viewStyle, - // eslint-disable-next-line react-hooks/rules-of-hooks - useAnimationHooks({ - animation, - style: viewStyle - })] - : viewStyle const innerProps = useInnerProps( props, extendObject({ ref: nodeRef, - style: finalStyle + style: enableStyleAnimation ? [viewStyle, animationStyle] : viewStyle }, - layoutProps, - hoverStyle - ? { - bindtouchstart: onTouchStart, - bindtouchend: onTouchEnd - } - : {} + layoutProps ), [ 'hover-start-time', 'hover-stay-time', @@ -820,9 +769,13 @@ const _View = forwardRef, _ViewProps>((viewProps, r enableFastImage }) - return enableAnimation + const BaseComponent = enableStyleAnimation ? createElement(Animated.View, innerProps, childNode) : createElement(View, innerProps, childNode) + + return enableHover + ? createElement(GestureDetector, { gesture: gesture as PanGesture }, BaseComponent) + : BaseComponent }) _View.displayName = 'MpxView' diff --git a/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts b/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts index 71a41f5379..620dea764a 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts @@ -13,6 +13,7 @@ import { WithTimingConfig, AnimationCallback } from 'react-native-reanimated' +import { error } from '@mpxjs/utils' import { ExtendedViewStyle } from './types/common' import type { _ViewProps } from './mpx-view' @@ -166,20 +167,32 @@ const formatStyle = (style: ExtendedViewStyle): ExtendedViewStyle => { }) } -export default function useAnimationHooks (props: _ViewProps) { - const { style = {}, animation } = props +export default function useAnimationHooks (props: _ViewProps & { enableAnimation?: boolean }) { + const { style = {}, animation, enableAnimation } = props + + const enableStyleAnimation = enableAnimation || !!animation + const enableAnimationRef = useRef(enableStyleAnimation) + if (enableAnimationRef.current !== enableStyleAnimation) { + error('[Mpx runtime error]: animation use should be stable in the component lifecycle, or you can set [enable-animation] with true.') + } + + if (!enableAnimationRef.current) return { enableStyleAnimation: false } + const originalStyle = formatStyle(style) // id 标识 const id = animation?.id || -1 // 有动画样式的 style key + // eslint-disable-next-line react-hooks/rules-of-hooks const animatedStyleKeys = useSharedValue([] as (string|string[])[]) // 记录动画key的style样式值 没有的话设置为false + // eslint-disable-next-line react-hooks/rules-of-hooks const animatedKeys = useRef({} as {[propName: keyof ExtendedViewStyle]: boolean}) // const animatedKeys = useRef({} as {[propName: keyof ExtendedViewStyle]: boolean|number|string}) // ** 全量 style prop sharedValue // 不能做增量的原因: // 1 尝试用 useRef,但 useAnimatedStyle 访问后的 ref 不能在增加新的值,被冻结 // 2 尝试用 useSharedValue,因为实际触发的 style prop 需要是 sharedValue 才能驱动动画,若外层 shareValMap 也是 sharedValue,动画无法驱动。 + // eslint-disable-next-line react-hooks/rules-of-hooks const shareValMap = useMemo(() => { return Object.keys(InitialValue).reduce((valMap, key) => { const defaultVal = getInitialVal(key, isTransform(key)) @@ -188,6 +201,7 @@ export default function useAnimationHooks (props: _ViewProps) { }, {} as { [propName: keyof ExtendedViewStyle]: SharedValue }) }, []) // ** 获取动画样式prop & 驱动动画 + // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (id === -1) return // 更新动画样式 key map @@ -208,6 +222,7 @@ export default function useAnimationHooks (props: _ViewProps) { // }) // }, [style]) // ** 清空动画 + // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { return () => { Object.values(shareValMap).forEach((value) => { @@ -335,7 +350,8 @@ export default function useAnimationHooks (props: _ViewProps) { }, {} as { [propName: string]: string | number }) } // ** 生成动画样式 - return useAnimatedStyle(() => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const animationStyle = useAnimatedStyle(() => { // console.info(`useAnimatedStyle styles=`, originalStyle) return animatedStyleKeys.value.reduce((styles, key) => { // console.info('getAnimationStyles', key, shareValMap[key].value) @@ -353,4 +369,9 @@ export default function useAnimationHooks (props: _ViewProps) { return styles }, {} as ExtendedViewStyle) }) + + return { + enableStyleAnimation: enableAnimationRef.current, + animationStyle + } } diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index 08fac95572..f14a5e6ad8 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -1,12 +1,14 @@ import { useEffect, useCallback, useMemo, useRef, ReactNode, ReactElement, isValidElement, useContext, useState, Dispatch, SetStateAction, Children, cloneElement } from 'react' import { LayoutChangeEvent, TextStyle, ImageProps, Image } from 'react-native' import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn } from '@mpxjs/utils' -import { VarContext } from './context' +import { VarContext, ScrollViewContext } from './context' import { ExpressionParser, parseFunc, ReplaceSource } from './parser' import { initialWindowMetrics } from 'react-native-safe-area-context' import { useNavigation } from '@react-navigation/native' import FastImage, { FastImageProps } from '@d11/react-native-fast-image' -import type { AnyFunc, ExtendedFunctionComponent } from './types/common' +import type { AnyFunc, ExtendedFunctionComponent, ExtendedViewStyle } from './types/common' +import { runOnJS } from 'react-native-reanimated' +import { Gesture } from 'react-native-gesture-handler' export const TEXT_STYLE_REGEX = /color|font.*|text.*|letterSpacing|lineHeight|includeFontPadding|writingDirection/ export const PERCENT_REGEX = /^\s*-?\d+(\.\d+)?%\s*$/ @@ -642,3 +644,67 @@ export function pickStyle (styleObj: Record = {}, pickedKeys: Array return acc }, {}) } + +export function useHover ({ enableHover, hoverStartTime, hoverStayTime, disabled } : { enableHover: boolean, hoverStartTime: number, hoverStayTime: number, disabled?: boolean }) { + const enableHoverRef = useRef(enableHover) + if (enableHoverRef.current !== enableHover) { + error('[Mpx runtime error]: hover-class use should be stable in the component lifecycle.') + } + + if (!enableHoverRef.current) return { isHover: false } + // eslint-disable-next-line react-hooks/rules-of-hooks + const gestureRef = useContext(ScrollViewContext).gestureRef + // eslint-disable-next-line react-hooks/rules-of-hooks + const [isHover, setIsHover] = useState(false) + // eslint-disable-next-line react-hooks/rules-of-hooks + const dataRef = useRef<{ + startTimer?: ReturnType + stayTimer?: ReturnType + }>({}) + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + return () => { + dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer) + dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer) + } + }, []) + + const setStartTimer = () => { + if (disabled) return + dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer) + dataRef.current.startTimer = setTimeout(() => { + setIsHover(true) + }, +hoverStartTime) + } + + const setStayTimer = () => { + if (disabled) return + dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer) + dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer) + dataRef.current.stayTimer = setTimeout(() => { + setIsHover(false) + }, +hoverStayTime) + } + // eslint-disable-next-line react-hooks/rules-of-hooks + const gesture = useMemo(() => { + return Gesture.Pan() + .onTouchesDown(() => { + 'worklet' + runOnJS(setStartTimer)() + }) + .onTouchesUp(() => { + 'worklet' + runOnJS(setStayTimer)() + }) + }, []) + + if (gestureRef) { + gesture.simultaneousWithExternalGesture(gestureRef) + } + + return { + isHover, + gesture + } +}