diff --git a/packages/api-proxy/src/platform/api/system/index.web.js b/packages/api-proxy/src/platform/api/system/index.web.js index 2614aa2562..0e756b73d9 100644 --- a/packages/api-proxy/src/platform/api/system/index.web.js +++ b/packages/api-proxy/src/platform/api/system/index.web.js @@ -139,7 +139,7 @@ const getLaunchOptionsSync = function () { throwSSRWarning('getLaunchOptionsSync API is running in non browser environments') return } - return global.__mpxEnterOptions || {} + return global.__mpxLaunchOptions || {} } export { diff --git a/packages/api-proxy/src/platform/api/system/rnSystem.js b/packages/api-proxy/src/platform/api/system/rnSystem.js index acf3965ed9..abc0501466 100644 --- a/packages/api-proxy/src/platform/api/system/rnSystem.js +++ b/packages/api-proxy/src/platform/api/system/rnSystem.js @@ -41,18 +41,11 @@ const getWindowInfo = function () { } const getLaunchOptionsSync = function () { - const options = global.__mpxEnterOptions || {} - const { path, scene, query } = options - return { - path, - scene, - query - } + return global.__mpxLaunchOptions || {} } const getEnterOptionsSync = function () { - const result = getLaunchOptionsSync() - return result + return global.__mpxEnterOptions || {} } export { diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index b3ef722394..64123508ec 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -1,13 +1,12 @@ import transferOptions from '../core/transferOptions' import builtInKeysMap from './patch/builtInKeysMap' -import { makeMap, spreadProp, getFocusedNavigation, hasOwn, extend } from '@mpxjs/utils' +import { makeMap, spreadProp, getFocusedNavigation, hasOwn } from '@mpxjs/utils' import { mergeLifecycle } from '../convertor/mergeLifecycle' import { LIFECYCLE } from '../platform/patch/lifecycle/index' import Mpx from '../index' import { createElement, memo, useRef, useEffect } from 'react' import * as ReactNative from 'react-native' import { Image } from 'react-native' -import { ref } from '../observer/ref' const appHooksMap = makeMap(mergeLifecycle(LIFECYCLE).app) @@ -30,22 +29,24 @@ function filterOptions (options, appData) { return newOptions } -function createAppInstance (appData) { - return extend({}, Mpx.prototype, appData) -} - -export default function createApp (option, config = {}) { +export default function createApp (options) { const appData = {} const { NavigationContainer, createStackNavigator, SafeAreaProvider } = global.__navigationHelper // app选项目前不需要进行转换 - const { rawOptions, currentInject } = transferOptions(option, 'app', false) + const { rawOptions, currentInject } = transferOptions(options, 'app', false) const defaultOptions = filterOptions(spreadProp(rawOptions, 'methods'), appData) - defaultOptions.onAppInit && defaultOptions.onAppInit() // 在页面script执行前填充getApp() global.getApp = function () { return appData } + + defaultOptions.onShow && global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(appData)) + defaultOptions.onHide && global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(appData)) + defaultOptions.onError && global.__mpxAppCbs.error.push(defaultOptions.onError.bind(appData)) + defaultOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(appData)) + defaultOptions.onAppInit && defaultOptions.onAppInit() + const pages = currentInject.getPages() || {} const firstPage = currentInject.firstPage const Stack = createStackNavigator() @@ -82,55 +83,43 @@ export default function createApp (option, config = {}) { } global.__mpxAppLaunched = false - - global.__mpxAppFocusedState = ref('show') + global.__mpxAppHotLaunched = false global.__mpxOptionsMap[currentInject.moduleId] = memo((props) => { - const instanceRef = useRef(null) - if (!instanceRef.current) { - instanceRef.current = createAppInstance(appData) - } - const instance = instanceRef.current const initialRouteRef = useRef({ initialRouteName: firstPage, initialParams: {} }) - if (!global.__mpxAppLaunched) { + if (!global.__mpxAppHotLaunched) { const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams global.__mpxAppOnLaunch = (navigation) => { - global.__mpxAppLaunched = true const state = navigation.getState() Mpx.config.rnConfig.onStateChange?.(state) const current = state.routes[state.index] - global.__mpxEnterOptions = { + const options = { path: current.name, query: current.params, scene: 0, shareTicket: '', referrerInfo: {} } - defaultOptions.onLaunch && defaultOptions.onLaunch.call(instance, global.__mpxEnterOptions) - defaultOptions.onShow && defaultOptions.onShow.call(instance, global.__mpxEnterOptions) + global.__mpxEnterOptions = options + if (!global.__mpxAppLaunched) { + global.__mpxLaunchOptions = options + defaultOptions.onLaunch && defaultOptions.onLaunch.call(appData, options) + } + global.__mpxAppCbs.show.forEach((cb) => { + cb(options) + }) + global.__mpxAppLaunched = true + global.__mpxAppHotLaunched = true } } useEffect(() => { - if (defaultOptions.onShow) { - global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(instance)) - } - if (defaultOptions.onHide) { - global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(instance)) - } - if (defaultOptions.onError) { - global.__mpxAppCbs.error.push(defaultOptions.onError.bind(instance)) - } - if (defaultOptions.onUnhandledRejection) { - global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(instance)) - } - const changeSubscription = ReactNative.AppState.addEventListener('change', (currentState) => { if (currentState === 'active') { let options = global.__mpxEnterOptions @@ -177,6 +166,8 @@ export default function createApp (option, config = {}) { return () => { changeSubscription && changeSubscription.remove() resizeSubScription && resizeSubScription.remove() + // 热启动情况下,app会被销毁重建,将__mpxAppHotLaunched重置保障路由等初始化逻辑正确执行 + global.__mpxAppHotLaunched = false } }, []) diff --git a/packages/core/src/platform/createApp.js b/packages/core/src/platform/createApp.js index 0f35cf97e5..f3e847cb61 100644 --- a/packages/core/src/platform/createApp.js +++ b/packages/core/src/platform/createApp.js @@ -24,19 +24,23 @@ function filterOptions (options, appData) { return newOptions } -export default function createApp (option, config = {}) { - // 在App中挂载mpx对象供周边工具访问,如e2e测试 +export default function createApp (options, config = {}) { + const appData = {} + // app选项目前不需要进行转换 + const { rawOptions, currentInject } = transferOptions(options, 'app', false) const builtInMixins = [{ + // 在App中挂载mpx对象供周边工具访问,如e2e测试 getMpx () { return Mpx } }] - const appData = {} if (__mpx_mode__ === 'web') { builtInMixins.push({ - created () { - Object.assign(this, Mpx.prototype) + beforeCreate () { + // for vue provide vm access Object.assign(this, appData) + }, + created () { const current = this.$root.$options?.router?.currentRoute || {} const options = { path: current.path && current.path.replace(/^\//, ''), @@ -45,48 +49,41 @@ export default function createApp (option, config = {}) { shareTicket: '', referrerInfo: {} } + // web不分冷启动和热启动 global.__mpxEnterOptions = options - this.$options.onLaunch && this.$options.onLaunch.call(this, options) - if (isBrowser) { - if (this.$options.onShow) { - this.$options.onShow.call(this, options) - global.__mpxAppCbs.show.push(this.$options.onShow.bind(this)) - } - if (this.$options.onHide) { - global.__mpxAppCbs.hide.push(this.$options.onHide.bind(this)) - } - if (this.$options.onError) { - global.__mpxAppCbs.error.push(this.$options.onError.bind(this)) - } - if (this.$options.onUnhandledRejection) { - global.__mpxAppCbs.rejection.push(this.$options.onUnhandledRejection.bind(this)) - } - } + global.__mpxLaunchOptions = options + rawOptions.onLaunch && rawOptions.onLaunch.call(appData, options) + global.__mpxAppCbs.show.forEach((cb) => { + cb(options) + }) } }) } else { builtInMixins.push({ onLaunch () { - Object.assign(this, Mpx.prototype) + initAppProvides(rawOptions.provide, this) } }) } - // app选项目前不需要进行转换 - const { rawOptions, currentInject } = transferOptions(option, 'app', false) rawOptions.mixins = builtInMixins const defaultOptions = filterOptions(spreadProp(mergeOptions(rawOptions, 'app', false), 'methods'), appData) if (__mpx_mode__ === 'web') { - global.__mpxOptionsMap = global.__mpxOptionsMap || {} - global.__mpxOptionsMap[currentInject.moduleId] = defaultOptions global.getApp = function () { if (!isBrowser) { console.error('[Mpx runtime error]: Dangerous API! global.getApp method is running in non browser environments') } return appData } + if (isBrowser) { + defaultOptions.onShow && global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(appData)) + defaultOptions.onHide && global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(appData)) + defaultOptions.onError && global.__mpxAppCbs.error.push(defaultOptions.onError.bind(appData)) + defaultOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(appData)) + } + global.__mpxOptionsMap = global.__mpxOptionsMap || {} + global.__mpxOptionsMap[currentInject.moduleId] = defaultOptions } else { - initAppProvides(rawOptions) defaultOptions.onAppInit && defaultOptions.onAppInit() const ctor = config.customCtor || global.currentCtor || App ctor(defaultOptions) diff --git a/packages/core/src/platform/export/inject.js b/packages/core/src/platform/export/inject.js index ea73092074..bc387994d0 100644 --- a/packages/core/src/platform/export/inject.js +++ b/packages/core/src/platform/export/inject.js @@ -11,11 +11,10 @@ const providesMap = { global.__mpxProvidesMap = providesMap /** @internal createApp() 初始化应用层 scope provide */ -export function initAppProvides (appOptions) { - const provideOpt = appOptions.provide +export function initAppProvides (provideOpt, instance) { if (provideOpt) { const provided = isFunction(provideOpt) - ? callWithErrorHandling(provideOpt.bind(appOptions), appOptions, 'createApp provide function') + ? callWithErrorHandling(provideOpt.bind(instance), instance, 'createApp provide function') : provideOpt if (isObject(provided)) { providesMap.__app = provided diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index ac4c5c7561..0189823b53 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -365,14 +365,10 @@ function usePageStatus (navigation, pageId) { const blurSubscription = navigation.addListener('blur', () => { pageStatusMap[pageId] = 'hide' }) - const unWatchAppFocusedState = watch(global.__mpxAppFocusedState, (value) => { - pageStatusMap[pageId] = value - }) return () => { focusSubscription() blurSubscription() - unWatchAppFocusedState() del(pageStatusMap, pageId) } }, [navigation]) @@ -442,7 +438,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { useEffect(() => { if (type === 'page') { - if (!global.__mpxAppLaunched && global.__mpxAppOnLaunch) { + if (!global.__mpxAppHotLaunched && global.__mpxAppOnLaunch) { global.__mpxAppOnLaunch(props.navigation) } proxy.callHook(ONLOAD, [props.route.params || {}]) diff --git a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts index 3bc80cda97..6d00a5ceba 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts @@ -1,5 +1,6 @@ import { useRef, useMemo, RefObject } from 'react' import { hasOwn, collectDataset } from '@mpxjs/utils' +import { useNavigation } from '@react-navigation/native' import { omit, extendObject } from './utils' import eventConfigMap from './event.config' import { @@ -10,15 +11,22 @@ import { InnerRef, SetTimeoutReturnType, LayoutRef, - NativeTouchEvent + NativeTouchEvent, + Navigation } from './types/getInnerListeners' +const globalEventState = { + needPress: true +} + const getTouchEvent = ( type: string, event: NativeTouchEvent, props: Props, - config: UseInnerPropsConfig + config: UseInnerPropsConfig, + navigation: Navigation ) => { + const { y: navigationY = 0 } = navigation?.layout || {} const nativeEvent = event.nativeEvent const { timestamp, pageX, pageY, touches, changedTouches } = nativeEvent const { id } = props @@ -49,24 +57,24 @@ const getTouchEvent = ( target, detail: { x: pageX, - y: pageY + y: pageY - navigationY }, touches: touches.map((item) => { return { identifier: item.identifier, pageX: item.pageX, - pageY: item.pageY, - clientX: item.locationX, - clientY: item.locationY + pageY: item.pageY - navigationY, + clientX: item.pageX, + clientY: item.pageY - navigationY } }), changedTouches: changedTouches.map((item) => { return { identifier: item.identifier, pageX: item.pageX, - pageY: item.pageY, - clientX: item.locationX, - clientY: item.locationY + pageY: item.pageY - navigationY, + clientX: item.pageX, + clientY: item.pageY - navigationY } }), persist: event.persist, @@ -105,7 +113,8 @@ function handleEmitEvent ( type: string, oe: NativeTouchEvent, propsRef: Record, - config: UseInnerPropsConfig + config: UseInnerPropsConfig, + navigation: Navigation ) { events.forEach((event) => { if (propsRef.current[event]) { @@ -114,7 +123,7 @@ function handleEmitEvent ( oe.stopPropagation() } propsRef.current[event]( - getTouchEvent(type, oe, propsRef.current, config) + getTouchEvent(type, oe, propsRef.current, config, navigation) ) } }) @@ -129,14 +138,14 @@ function checkIsNeedPress (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: Math.abs(currentPageX - tapDetailInfo.x) > 3 || Math.abs(currentPageY - tapDetailInfo.y) > 3 ) { - ref.current!.needPress[type] = false + globalEventState.needPress = false ref.current!.startTimer[type] && clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) ref.current!.startTimer[type] = null } } -function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { +function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) { e.persist() const bubbleTouchEvent = ['catchtouchstart', 'bindtouchstart'] const bubblePressEvent = ['catchlongpress', 'bindlongpress'] @@ -149,7 +158,7 @@ function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: 'capture-bindlongpress' ] ref.current!.startTimer[type] = null - ref.current!.needPress[type] = true + globalEventState.needPress = true const nativeEvent = e.nativeEvent ref.current!.mpxPressInfo.detail = { x: nativeEvent.changedTouches[0].pageX, @@ -159,7 +168,7 @@ function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: type === 'bubble' ? bubbleTouchEvent : captureTouchEvent const currentPressEvent = type === 'bubble' ? bubblePressEvent : capturePressEvent - handleEmitEvent(currentTouchEvent, 'touchstart', e, propsRef, config) + handleEmitEvent(currentTouchEvent, 'touchstart', e, propsRef, config, navigation) const { catchlongpress, bindlongpress, @@ -173,13 +182,14 @@ function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: captureBindlongpress ) { ref.current!.startTimer[type] = setTimeout(() => { - ref.current!.needPress[type] = false - handleEmitEvent(currentPressEvent, 'longpress', e, propsRef, config) + // 只要触发过longpress, 全局就不再触发tap + globalEventState.needPress = false + handleEmitEvent(currentPressEvent, 'longpress', e, propsRef, config, navigation) }, 350) } } -function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { +function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) { const bubbleTouchEvent = ['catchtouchmove', 'bindtouchmove'] const captureTouchEvent = [ 'capture-catchtouchmove', @@ -187,11 +197,11 @@ function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: ] const currentTouchEvent = type === 'bubble' ? bubbleTouchEvent : captureTouchEvent - handleEmitEvent(currentTouchEvent, 'touchmove', e, propsRef, config) + handleEmitEvent(currentTouchEvent, 'touchmove', e, propsRef, config, navigation) checkIsNeedPress(e, type, ref) } -function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { +function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) { // move event may not be triggered checkIsNeedPress(e, type, ref) const bubbleTouchEvent = ['catchtouchend', 'bindtouchend'] @@ -208,19 +218,22 @@ function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: R ref.current!.startTimer[type] && clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) ref.current!.startTimer[type] = null - handleEmitEvent(currentTouchEvent, 'touchend', e, propsRef, config) - if (ref.current!.needPress[type]) { + handleEmitEvent(currentTouchEvent, 'touchend', e, propsRef, config, navigation) + if (globalEventState.needPress) { if (type === 'bubble' && config.disableTap) { return } - handleEmitEvent(currentTapEvent, 'tap', e, propsRef, config) + handleEmitEvent(currentTapEvent, 'tap', e, propsRef, config, navigation) } } function handleTouchcancel ( e: NativeTouchEvent, type: 'bubble' | 'capture', - ref: RefObject, propsRef: Record, config: UseInnerPropsConfig + ref: RefObject, + propsRef: Record, + config: UseInnerPropsConfig, + navigation: Navigation ) { const bubbleTouchEvent = ['catchtouchcancel', 'bindtouchcancel'] const captureTouchEvent = [ @@ -232,11 +245,11 @@ function handleTouchcancel ( ref.current!.startTimer[type] && clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) ref.current!.startTimer[type] = null - handleEmitEvent(currentTouchEvent, 'touchcancel', e, propsRef, config) + handleEmitEvent(currentTouchEvent, 'touchcancel', e, propsRef, config, navigation) } function createTouchEventHandler (eventName: 'onTouchStart'|'onTouchMove'|'onTouchEnd'|'onTouchCancel', type: 'bubble' | 'capture') { - return (e: NativeTouchEvent, ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) => { + return (e: NativeTouchEvent, ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) => { const handlerMap = { onTouchStart: handleTouchstart, onTouchMove: handleTouchmove, @@ -246,7 +259,7 @@ function createTouchEventHandler (eventName: 'onTouchStart'|'onTouchMove'|'onTou const handler = handlerMap[eventName] if (handler) { - handler(e, type, ref, propsRef, config) + handler(e, type, ref, propsRef, config, navigation) } } } @@ -273,10 +286,6 @@ const useInnerProps = ( bubble: null, capture: null }, - needPress: { - bubble: false, - capture: false - }, mpxPressInfo: { detail: { x: 0, @@ -291,6 +300,8 @@ const useInnerProps = ( layoutRef: { current: {} }, disableTap: false } + const navigation = useNavigation() + const removeProps = [ 'children', 'enable-background', @@ -332,7 +343,7 @@ const useInnerProps = ( touchEventList.forEach((item) => { if (finalEventKeys.includes(item.eventName)) { events[item.eventName] = (e: NativeTouchEvent) => - item.handler(e, ref, propsRef, config) + item.handler(e, ref, propsRef, config, navigation) } }) 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 3a0a330251..b6410e9034 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 @@ -19,10 +19,10 @@ */ import { useEffect, forwardRef, ReactNode, useContext, useCallback, useRef, useMemo, createElement } from 'react' import { StyleSheet, NativeSyntheticEvent, View, LayoutChangeEvent } from 'react-native' -import { getCustomEvent } from './getInnerListeners' +import useInnerProps, { getCustomEvent } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { MovableAreaContext } from './context' -import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, GestureHandler, flatGesture, extendObject } from './utils' +import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, GestureHandler, flatGesture, extendObject, omit } from './utils' import { GestureDetector, Gesture, GestureTouchEvent, GestureStateChangeEvent, PanGestureHandlerEventPayload, PanGesture } from 'react-native-gesture-handler' import Animated, { useSharedValue, @@ -33,6 +33,7 @@ import Animated, { useAnimatedReaction, withSpring } from 'react-native-reanimated' +import { collectDataset, noop } from '@mpxjs/utils' interface MovableViewProps { children: ReactNode; @@ -42,17 +43,22 @@ interface MovableViewProps { y?: number; disabled?: boolean; animation?: boolean; + id?: string; bindchange?: (event: unknown) => void; - bindtouchstart?: (event: NativeSyntheticEvent) => void; - catchtouchstart?: (event: NativeSyntheticEvent) => void; - bindtouchmove?: (event: NativeSyntheticEvent) => void; - catchtouchmove?: (event: NativeSyntheticEvent) => void; - catchtouchend?: (event: NativeSyntheticEvent) => void; - bindtouchend?: (event: NativeSyntheticEvent) => void; - bindhtouchmove?: (event: NativeSyntheticEvent) => void; - bindvtouchmove?: (event: NativeSyntheticEvent) => void; - catchhtouchmove?: (event: NativeSyntheticEvent) => void; - catchvtouchmove?: (event: NativeSyntheticEvent) => void; + bindtouchstart?: (event: GestureTouchEvent) => void; + catchtouchstart?: (event: GestureTouchEvent) => void; + bindtouchmove?: (event: GestureTouchEvent) => void; + catchtouchmove?: (event: GestureTouchEvent) => void; + catchtouchend?: (event: GestureTouchEvent) => void; + bindtouchend?: (event: GestureTouchEvent) => void; + bindhtouchmove?: (event: GestureTouchEvent) => void; + bindvtouchmove?: (event: GestureTouchEvent) => void; + catchhtouchmove?: (event: GestureTouchEvent) => void; + catchvtouchmove?: (event: GestureTouchEvent) => void; + bindlongpress?: (event: GestureTouchEvent) => void; + catchlongpress?: (event: GestureTouchEvent) => void; + bindtap?: (event: GestureTouchEvent) => void; + catchtap?: (event: GestureTouchEvent) => void; onLayout?: (event: LayoutChangeEvent) => void; 'out-of-bounds'?: boolean; 'wait-for'?: Array; @@ -153,10 +159,10 @@ const _MovableView = forwardRef, MovableViewP }) const hasSimultaneousHandlersChanged = prevSimultaneousHandlersRef.current.length !== (originSimultaneousHandlers?.length || 0) || - (originSimultaneousHandlers || []).some((handler, index) => handler !== prevSimultaneousHandlersRef.current[index]) + (originSimultaneousHandlers || []).some((handler, index) => handler !== prevSimultaneousHandlersRef.current[index]) const hasWaitForHandlersChanged = prevWaitForHandlersRef.current.length !== (waitFor?.length || 0) || - (waitFor || []).some((handler, index) => handler !== prevWaitForHandlersRef.current[index]) + (waitFor || []).some((handler, index) => handler !== prevWaitForHandlersRef.current[index]) if (hasSimultaneousHandlersChanged || hasWaitForHandlersChanged) { gestureSwitch.current = !gestureSwitch.current @@ -333,8 +339,7 @@ const _MovableView = forwardRef, MovableViewP props.onLayout && props.onLayout(e) } - const extendEvent = useCallback((e: any) => { - 'worklet' + const extendEvent = useCallback((e: any, obj?: Record) => { const touchArr = [e.changedTouches, e.allTouches] touchArr.forEach(touches => { touches && touches.forEach((item: { absoluteX: number; absoluteY: number; pageX: number; pageY: number }) => { @@ -342,48 +347,69 @@ const _MovableView = forwardRef, MovableViewP item.pageY = item.absoluteY }) }) - e.touches = e.allTouches + Object.assign(e, { + touches: e.allTouches, + detail: { + x: e.changedTouches[0].absoluteX, + y: e.changedTouches[0].absoluteY + }, + currentTarget: { + id: props.id || '', + dataset: collectDataset(props), + offsetLeft: 0, + offsetTop: 0 + } + }, obj) }, []) - const gesture = useMemo(() => { - const handleTriggerStart = (e: any) => { - 'worklet' - extendEvent(e) - bindtouchstart && runOnJS(bindtouchstart)(e) - catchtouchstart && runOnJS(catchtouchstart)(e) - } - - const handleTriggerMove = (e: any) => { - 'worklet' - extendEvent(e) - const hasTouchmove = !!bindhtouchmove || !!bindvtouchmove || !!bindtouchmove - const hasCatchTouchmove = !!catchhtouchmove || !!catchvtouchmove || !!catchtouchmove + const triggerStartOnJS = ({ e }: { e: GestureTouchEvent }) => { + extendEvent(e) + bindtouchstart && bindtouchstart(e) + catchtouchstart && catchtouchstart(e) + } - if (hasTouchmove) { - if (touchEvent.value === 'htouchmove') { - bindhtouchmove && runOnJS(bindhtouchmove)(e) - } else if (touchEvent.value === 'vtouchmove') { - bindvtouchmove && runOnJS(bindvtouchmove)(e) - } - bindtouchmove && runOnJS(bindtouchmove)(e) + const triggerMoveOnJS = ({ e, hasTouchmove, hasCatchTouchmove, touchEvent }: { e: GestureTouchEvent; hasTouchmove: boolean; hasCatchTouchmove: boolean; touchEvent: string }) => { + extendEvent(e) + if (hasTouchmove) { + if (touchEvent === 'htouchmove') { + bindhtouchmove && bindhtouchmove(e) + } else if (touchEvent === 'vtouchmove') { + bindvtouchmove && bindvtouchmove(e) } + bindtouchmove && bindtouchmove(e) + } - if (hasCatchTouchmove) { - if (touchEvent.value === 'htouchmove') { - catchhtouchmove && runOnJS(catchhtouchmove)(e) - } else if (touchEvent.value === 'vtouchmove') { - catchvtouchmove && runOnJS(catchvtouchmove)(e) - } - catchtouchmove && runOnJS(catchtouchmove)(e) + if (hasCatchTouchmove) { + if (touchEvent === 'htouchmove') { + catchhtouchmove && catchhtouchmove(e) + } else if (touchEvent === 'vtouchmove') { + catchvtouchmove && catchvtouchmove(e) } + catchtouchmove && catchtouchmove(e) } + } + + const triggerEndOnJS = ({ e }: { e: GestureTouchEvent }) => { + extendEvent(e) + bindtouchend && bindtouchend(e) + catchtouchend && catchtouchend(e) + } - const handleTriggerEnd = (e: any) => { + const gesture = useMemo(() => { + const handleTriggerMove = (e: GestureTouchEvent) => { 'worklet' - extendEvent(e) - bindtouchend && runOnJS(bindtouchend)(e) - catchtouchend && runOnJS(catchtouchend)(e) + const hasTouchmove = !!bindhtouchmove || !!bindvtouchmove || !!bindtouchmove + const hasCatchTouchmove = !!catchhtouchmove || !!catchvtouchmove || !!catchtouchmove + if (hasTouchmove || hasCatchTouchmove) { + runOnJS(triggerMoveOnJS)({ + e, + touchEvent: touchEvent.value, + hasTouchmove, + hasCatchTouchmove + }) + } } + const gesturePan = Gesture.Pan() .onTouchesDown((e: GestureTouchEvent) => { 'worklet' @@ -393,12 +419,14 @@ const _MovableView = forwardRef, MovableViewP x: changedTouches.x, y: changedTouches.y } - handleTriggerStart(e) + if (bindtouchstart || catchtouchstart) { + runOnJS(triggerStartOnJS)({ e }) + } }) .onTouchesMove((e: GestureTouchEvent) => { 'worklet' - isMoving.value = true const changedTouches = e.changedTouches[0] || { x: 0, y: 0 } + isMoving.value = true if (isFirstTouch.value) { touchEvent.value = Math.abs(changedTouches.x - startPosition.value.x) > Math.abs(changedTouches.y - startPosition.value.y) ? 'htouchmove' : 'vtouchmove' isFirstTouch.value = false @@ -430,7 +458,9 @@ const _MovableView = forwardRef, MovableViewP 'worklet' isFirstTouch.value = true isMoving.value = false - handleTriggerEnd(e) + if (bindtouchend || catchtouchend) { + runOnJS(triggerEndOnJS)({ e }) + } if (disabled) return if (!inertia) { const { x, y } = checkBoundaryPosition({ positionX: offsetX.value, positionY: offsetY.value }) @@ -454,8 +484,8 @@ const _MovableView = forwardRef, MovableViewP }) .onFinalize((e: GestureStateChangeEvent) => { 'worklet' - if (!inertia || disabled || !animation) return isMoving.value = false + if (!inertia || disabled || !animation) return if (direction === 'horizontal' || direction === 'all') { xInertialMotion.value = true offsetX.value = withDecay({ @@ -497,35 +527,50 @@ const _MovableView = forwardRef, MovableViewP } }) - const injectCatchEvent = (props: Record) => { - const eventHandlers: Record = {} - const catchEventList = [ - { name: 'onTouchStart', value: ['catchtouchstart'] }, - { name: 'onTouchMove', value: ['catchtouchmove', 'catchvtouchmove', 'catchhtouchmove'] }, - { name: 'onTouchEnd', value: ['catchtouchend'] } + const rewriteCatchEvent = () => { + const handlers: Record = {} + + const events = [ + { type: 'touchstart' }, + { type: 'touchmove', alias: ['vtouchmove', 'htouchmove'] }, + { type: 'touchend' } ] - catchEventList.forEach(event => { - event.value.forEach(name => { - if (props[name] && !eventHandlers[event.name]) { - eventHandlers[event.name] = (e: NativeSyntheticEvent) => { - e.stopPropagation() - } - } - }) + events.forEach(({ type, alias = [] }) => { + const hasCatchEvent = + props[`catch${type}` as keyof typeof props] || + alias.some(name => props[`catch${name}` as keyof typeof props]) + if (hasCatchEvent) handlers[`catch${type}`] = noop }) - return eventHandlers + + return handlers } - const catchEventHandlers = injectCatchEvent(props) const layoutStyle = !hasLayoutRef.current && hasSelfPercent ? HIDDEN_STYLE : {} + // bind 相关 touch 事件直接由 gesture 触发,无须重复挂载 + // catch 相关 touch 事件需要重写并通过 useInnerProps 注入阻止冒泡逻辑 + const filterProps = omit(props, [ + 'bindtouchstart', + 'bindtouchmove', + 'bindvtouchmove', + 'bindhtouchmove', + 'bindtouchend', + 'catchtouchstart', + 'catchtouchmove', + 'catchvtouchmove', + 'catchhtouchmove', + 'catchtouchend' + ]) + + const innerProps = useInnerProps(filterProps, extendObject({ + ref: nodeRef, + onLayout: onLayout, + style: [innerStyle, animatedStyles, layoutStyle] + }, rewriteCatchEvent())) + return createElement(GestureDetector, { gesture: gesture }, createElement( Animated.View, - extendObject({ - ref: nodeRef, - onLayout: onLayout, - style: [innerStyle, animatedStyles, layoutStyle] - }, catchEventHandlers), + innerProps, wrapChildren( props, { diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/common.ts b/packages/webpack-plugin/lib/runtime/components/react/types/common.d.ts similarity index 100% rename from packages/webpack-plugin/lib/runtime/components/react/types/common.ts rename to packages/webpack-plugin/lib/runtime/components/react/types/common.d.ts diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.d.ts similarity index 93% rename from packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.ts rename to packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.d.ts index 78df219009..317d483d62 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.d.ts @@ -13,6 +13,8 @@ type RemoveProps = string[]; type NativeTouchEvent = NativeSyntheticEvent +type Navigation = Record + interface NativeEvent { timestamp: number; pageX: number; @@ -36,10 +38,6 @@ interface InnerRef { bubble: null | ReturnType; capture: null | ReturnType; }; - needPress: { - bubble: boolean; - capture: boolean; - }; mpxPressInfo: { detail: { x: number; @@ -65,5 +63,6 @@ export { InnerRef, LayoutRef, SetTimeoutReturnType, - DataSetType + DataSetType, + Navigation } diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts b/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts index 052fc1ac56..82d5a57476 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts @@ -20,6 +20,12 @@ declare module '@mpxjs/utils' { left: number right: number }, + layout: { + x: number + y: number + width: number + height: number + }, setOptions: (params: Record) => void } | undefined } @@ -27,3 +33,7 @@ declare module '@mpxjs/utils' { declare let global: { __formatValue (value: string): string | number } & Record + +declare module '@react-navigation/native' { + export function useNavigation (): Record +} diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index 5c933aea4f..08fac95572 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -1,9 +1,10 @@ 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, getFocusedNavigation } from '@mpxjs/utils' +import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn } from '@mpxjs/utils' import { VarContext } 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' @@ -32,8 +33,7 @@ const safeAreaInsetMap: Record = { 'safe-area-inset-left': 'left' } -function getSafeAreaInset (name: string) { - const navigation = getFocusedNavigation() +function getSafeAreaInset (name: string, navigation: Record) { const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets) return insets[safeAreaInsetMap[name]] } @@ -232,7 +232,7 @@ function transformVar (styleObj: Record, varKeyPaths: Array, envKeyPaths: Array>) { +function transformEnv (styleObj: Record, envKeyPaths: Array>, navigation: Record) { envKeyPaths.forEach((envKeyPath) => { setStyle(styleObj, envKeyPath, ({ target, key, value }) => { const parsed = parseFunc(value, 'env') @@ -240,7 +240,7 @@ function transformEnv (styleObj: Record, envKeyPaths: Array { const name = args[0] const fallback = args[1] || '' - const value = '' + (getSafeAreaInset(name) ?? global.__formatValue(fallback)) + const value = '' + (getSafeAreaInset(name, navigation) ?? global.__formatValue(fallback)) replaced.replace(start, end - 1, value) }) target[key] = global.__formatValue(replaced.source()) @@ -302,6 +302,7 @@ export function useTransformStyle (styleObj: Record = {}, { enableV const envKeyPaths: Array> = [] const [width, setWidth] = useState(0) const [height, setHeight] = useState(0) + const navigation = useNavigation() function varVisitor ({ key, value, keyPath }: VisitorArg) { if (keyPath.length === 1) { @@ -396,7 +397,7 @@ export function useTransformStyle (styleObj: Record = {}, { enableV } // apply env - transformEnv(normalStyle, envKeyPaths) + transformEnv(normalStyle, envKeyPaths, navigation) // apply percent transformPercent(normalStyle, percentKeyPaths, percentConfig) // apply calc @@ -595,7 +596,7 @@ export const useStableCallback = ( ) } -export const usePrevious = (value: T): T | undefined => { +export function usePrevious (value: T): T | undefined { const ref = useRef() const prev = ref.current ref.current = value