diff --git a/packages/webpack-plugin/lib/runtime/components/react/context.ts b/packages/webpack-plugin/lib/runtime/components/react/context.ts index 5bb14fc15d..7953ddd9ae 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/context.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/context.ts @@ -51,4 +51,6 @@ export const IntersectionObserverContext = createContext(null) +export const SwiperContext = createContext({}) + export const KeyboardAvoidContext = createContext(null) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper-item.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper-item.tsx index 2c9771f001..161e3307ff 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper-item.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper-item.tsx @@ -1,8 +1,10 @@ -import { View, LayoutChangeEvent } from 'react-native' -import { ReactNode, forwardRef, useRef } from 'react' +import { View } from 'react-native' +import Animated, { useAnimatedStyle, interpolate, SharedValue } from 'react-native-reanimated' +import { ReactNode, forwardRef, useRef, useContext } from 'react' import useInnerProps from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' // 引入辅助函数 import { useTransformStyle, splitStyle, splitProps, wrapChildren, useLayout } from './utils' +import { SwiperContext } from './context' interface SwiperItemProps { 'item-id'?: string; @@ -14,15 +16,31 @@ interface SwiperItemProps { 'parent-height'?: number; children?: ReactNode; style?: Object; + customStyle: Object; + itemIndex: number; +} + +interface ContextType { + offset: SharedValue; + step: SharedValue; + scale: boolean; + dir: string; } const _SwiperItem = forwardRef, SwiperItemProps>((props: SwiperItemProps, ref) => { const { 'enable-var': enableVar, 'external-var-context': externalVarContext, - style + style, + customStyle, + itemIndex } = props + const contextValue = useContext(SwiperContext) as ContextType + const offset = contextValue.offset || 0 + const step = contextValue.step || 0 + const scale = contextValue.scale || false + const dir = contextValue.dir || 'x' const { textProps } = splitProps(props) const nodeRef = useRef(null) @@ -47,19 +65,35 @@ const _SwiperItem = forwardRef, SwiperItemProp } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: nodeRef }) const innerProps = useInnerProps(props, { - style: { ...innerStyle, ...layoutStyle }, ref: nodeRef, ...layoutProps }, [ 'children', - 'enable-offset' + 'enable-offset', + 'style' ], { layoutRef }) - + const itemAnimatedStyle = useAnimatedStyle(() => { + if (!step.value) return {} + const inputRange = [step.value, 0] + const outputRange = [0.7, 1] + // 实现元素的宽度跟随step从0到真实宽度,且不能触发重新渲染整个组件,通过AnimatedStyle的方式实现 + const outerLayoutStyle = dir === 'x' ? { width: step.value, height: '100%' } : { width: '100%', height: step.value } + const transformStyle = [] + if (scale) { + transformStyle.push({ + scale: interpolate(Math.abs(Math.abs(offset.value) - itemIndex * step.value), inputRange, outputRange) + }) + } + return Object.assign(outerLayoutStyle, { + transform: transformStyle + }) + }) return ( - - { + + { wrapChildren( props, { @@ -70,7 +104,7 @@ const _SwiperItem = forwardRef, SwiperItemProp } ) } - + ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx new file mode 100644 index 0000000000..bc1a45ebb4 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx @@ -0,0 +1,714 @@ +import { View, NativeSyntheticEvent, LayoutChangeEvent } from 'react-native' +import { GestureDetector, Gesture } from 'react-native-gesture-handler' +import Animated, { useAnimatedStyle, useSharedValue, withTiming, Easing, runOnJS, useAnimatedReaction, cancelAnimation } from 'react-native-reanimated' + +import React, { JSX, forwardRef, useRef, useEffect, ReactNode, ReactElement, useCallback, useMemo } from 'react' +import useInnerProps, { getCustomEvent } from './getInnerListeners' +import useNodesRef, { HandlerRef } from './useNodesRef' // 引入辅助函数 +import { useTransformStyle, splitStyle, splitProps, useLayout, wrapChildren } from './utils' +import { SwiperContext } from './context' +/** + * ✔ indicator-dots + * ✔ indicator-color + * ✔ indicator-active-color + * ✔ autoplay + * ✔ current + * ✔ interval + * ✔ duration + * ✔ circular + * ✔ vertical + * ✔ previous-margin + * ✔ next-margin + * ✔ easing-function ="easeOutCubic" + * ✘ display-multiple-items + * ✘ snap-to-edge + */ +type EaseType = 'default' | 'linear' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' +type StrAbsoType = 'absoluteX' | 'absoluteY' +type EventDataType = { + translation: number +} + +interface SwiperProps { + children?: ReactNode; + circular?: boolean; + current?: number; + interval?: number; + autoplay?: boolean; + // scrollView 只有安卓可以设 + duration?: number; + // 滑动过程中元素是否scale变化 + scale?: boolean; + 'indicator-dots'?: boolean; + 'indicator-color'?: string; + 'indicator-active-color'?: string; + vertical?: boolean; + style: { + [key: string]: any + }; + 'easing-function'?: EaseType; + 'previous-margin'?: string; + 'next-margin'?: string; + 'enable-offset'?: boolean; + 'enable-var': boolean; + 'parent-font-size'?: number; + 'parent-width'?: number; + 'parent-height'?: number; + 'external-var-context'?: Record; + bindchange?: (event: NativeSyntheticEvent | unknown) => void; +} + +/** + * 默认的Style类型 + */ +const styles: { [key: string]: Object } = { + pagination_x: { + position: 'absolute', + bottom: 25, + left: 0, + right: 0, + flexDirection: 'row', + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + pagination_y: { + position: 'absolute', + right: 15, + top: 0, + bottom: 0, + flexDirection: 'column', + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + pagerWrapperx: { + position: 'absolute', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center' + }, + pagerWrappery: { + position: 'absolute', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center' + }, + swiper: { + overflow: 'scroll', + display: 'flex', + justifyContent: 'flex-start' + } +} + +const dotCommonStyle = { + width: 8, + height: 8, + borderRadius: 4, + marginLeft: 3, + marginRight: 3, + marginTop: 3, + marginBottom: 3, + zIndex: 98 +} +const activeDotStyle = { + zIndex: 99 +} +const longPressRatio = 100 + +const easeMap = { + default: Easing.linear, + linear: Easing.linear, + easeInCubic: Easing.in(Easing.cubic), + easeOutCubic: Easing.out(Easing.cubic), + easeInOutCubic: Easing.inOut(Easing.cubic) +} + +const SwiperWrapper = forwardRef, SwiperProps>((props: SwiperProps, ref): JSX.Element => { + const { + 'indicator-dots': showsPagination, + 'indicator-color': dotColor = 'rgba(0, 0, 0, .3)', + 'indicator-active-color': activeDotColor = '#000000', + 'enable-var': enableVar = false, + 'parent-font-size': parentFontSize, + 'parent-width': parentWidth, + 'parent-height': parentHeight, + 'external-var-context': externalVarContext, + style = {} + } = props + const easeingFunc = props['easing-function'] || 'default' + const easeDuration = props.duration || 500 + const horizontal = props.vertical !== undefined ? !props.vertical : true + const nodeRef = useRef(null) + useNodesRef(props, ref, nodeRef, {}) + // 计算transfrom之类的 + const { + normalStyle, + hasVarDec, + varContextRef, + hasSelfPercent, + setWidth, + setHeight + } = useTransformStyle(style, { + enableVar, + externalVarContext, + parentFontSize, + parentWidth, + parentHeight + }) + const { textStyle } = splitStyle(normalStyle) + const { textProps } = splitProps(props) + const preMargin = props['previous-margin'] ? parseInt(props['previous-margin']) : 0 + const nextMargin = props['next-margin'] ? parseInt(props['next-margin']) : 0 + const previousMargin = useSharedValue(preMargin) + const nextMarginShared = useSharedValue(nextMargin) + // 默认前后补位的元素个数 + const patchEleNum = props.circular ? (preMargin ? 2 : 1) : 0 + const patchElementNum = useSharedValue(patchEleNum) + const circular = useSharedValue(props.circular || false) + const children = Array.isArray(props.children) ? props.children.filter(child => child) : (props.children ? [props.children] : []) + // 对有变化的变量,在worklet中只能使用sharedValue变量,useRef不能更新 + const childrenLength = useSharedValue(children.length) + const initWidth = typeof normalStyle?.width === 'number' ? normalStyle.width - preMargin - nextMargin : normalStyle.width + const initHeight = typeof normalStyle?.height === 'number' ? normalStyle.height - preMargin - nextMargin : normalStyle.height + const dir = horizontal === false ? 'y' : 'x' + const pstep = dir === 'x' ? initWidth : initHeight + const initStep: number = isNaN(pstep) ? 0 : pstep + // 每个元素的宽度 or 高度,有固定值直接初始化无则0 + const step = useSharedValue(initStep) + // 记录选中元素的索引值 + const currentIndex = useSharedValue(props.current || 0) + // const initOffset = getOffset(props.current || 0, initStep) + // 记录元素的偏移量 + const offset = useSharedValue(getOffset(props.current || 0, initStep)) + const strAbso = 'absolute' + dir.toUpperCase() as StrAbsoType + // 标识手指触摸和抬起, 起点在onBegin + const touchfinish = useSharedValue(true) + // 记录上一帧的绝对定位坐标 + const preAbsolutePos = useSharedValue(0) + // 记录从onBegin 到 onTouchesUp 时移动的距离 + const moveTranstion = useSharedValue(0) + // 记录从onBegin 到 onTouchesUp 的时间 + const moveTime = useSharedValue(0) + const timerId = useRef(0 as number | ReturnType) + const intervalTimer = props.interval || 500 + const { + // 存储layout布局信息 + layoutRef, + layoutProps, + layoutStyle + } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef, onLayout: onWrapperLayout }) + const innerProps = useInnerProps(props, { + ref: nodeRef + }, [ + 'style', + 'indicator-dots', + 'indicator-color', + 'indicator-active-color', + 'previous-margin', + 'vertical', + 'previous-margin', + 'next-margin', + 'easing-function', + 'autoplay', + 'circular', + 'interval', + 'easing-function' + ], { layoutRef: layoutRef }) + + function onWrapperLayout (e: LayoutChangeEvent) { + const { width, height } = e.nativeEvent.layout + const realWidth = dir === 'x' ? width - preMargin - nextMargin : width + const realHeight = dir === 'y' ? height - preMargin - nextMargin : height + const iStep = dir === 'x' ? realWidth : realHeight + step.value = iStep + if (touchfinish.value) { + offset.value = getOffset(currentIndex.value, iStep) + // useEffect中没拿到step.value之前不会开启loop,有step直接开启layout后取消再开启,无step依赖layout结束后开启 + pauseLoop() + resumeLoop() + } + } + + const dotAnimatedStyle = useAnimatedStyle(() => { + if (!step.value) return {} + const dotStep = dotCommonStyle.width + dotCommonStyle.marginRight + dotCommonStyle.marginLeft + if (dir === 'x') { + return { transform: [{ translateX: currentIndex.value * dotStep }] } + } else { + return { transform: [{ translateY: currentIndex.value * dotStep }] } + } + }) + + const renderPagination = useCallback(() => { + if (children.length <= 1) return null + const activeColor = activeDotColor || '#007aff' + const unActionColor = dotColor || 'rgba(0,0,0,.2)' + // 正常渲染所有dots + const dots: Array = [] + for (let i = 0; i < children.length; i++) { + dots.push() + } + return ( + + + + {dots} + + ) + }, [activeDotColor, dotColor, children.length]) + + function renderItems () { + const intLen = children.length + let renderChild = children.slice() + if (props.circular && intLen > 1) { + // 最前面加最后一个元素 + const lastChild = React.cloneElement(children[intLen - 1] as ReactElement, { key: 'clone0' }) + // 最后面加第一个元素 + const firstChild = React.cloneElement(children[0] as ReactElement, { key: 'clone1' }) + if (preMargin) { + const lastChild1 = React.cloneElement(children[intLen - 2] as ReactElement, { key: 'clone2' }) + const firstChild1 = React.cloneElement(children[1] as ReactElement, { key: 'clone3' }) + renderChild = [lastChild1, lastChild].concat(renderChild).concat([firstChild, firstChild1]) + } else { + renderChild = [lastChild].concat(renderChild).concat([firstChild]) + } + } + const arrChildren = renderChild.map((child, index) => { + const extraStyle = {} as { [key: string]: any } + if (index === 0 && !props.circular) { + preMargin && dir === 'x' && (extraStyle.marginLeft = preMargin) + preMargin && dir === 'y' && (extraStyle.marginTop = preMargin) + } + if (index === intLen - 1 && !props.circular) { + nextMargin && dir === 'x' && (extraStyle.marginRight = nextMargin) + nextMargin && dir === 'y' && (extraStyle.marginBottom = nextMargin) + } + // 业务swiper-item自己生成key,内部添加的元素自定义key + const newChild = React.cloneElement(child, { + itemIndex: index, + customStyle: extraStyle + }) + return newChild + }) + const contextValue = { + offset, + step, + scale: props.scale, + dir + } + return ({arrChildren}) + } + + const createAutoPlay = useCallback(() => { + let targetOffset = 0 + let nextIndex = currentIndex.value + if (!circular.value) { + // 获取下一个位置的坐标, 循环到最后一个元素,直接停止, 取消定时器 + if (currentIndex.value === childrenLength.value - 1) { + pauseLoop() + return + } + nextIndex += 1 + targetOffset = -nextIndex * step.value - previousMargin.value + offset.value = withTiming(targetOffset, { + duration: easeDuration, + easing: easeMap[easeingFunc] + }, () => { + currentIndex.value = nextIndex + runOnJS(loop)() + }) + } else { + // 默认向右, 向下 + if (nextIndex === childrenLength.value - 1) { + nextIndex = 0 + targetOffset = -(childrenLength.value + patchElementNum.value) * step.value + previousMargin.value + // 执行动画到下一帧 + offset.value = withTiming(targetOffset, { + duration: easeDuration + }, () => { + const initOffset = -step.value * patchElementNum.value + previousMargin.value + // 将开始位置设置为真正的位置 + offset.value = initOffset + currentIndex.value = nextIndex + runOnJS(loop)() + }) + } else { + nextIndex = currentIndex.value + 1 + targetOffset = -(nextIndex + patchElementNum.value) * step.value + previousMargin.value + // 执行动画到下一帧 + offset.value = withTiming(targetOffset, { + duration: easeDuration, + easing: easeMap[easeingFunc] + }, () => { + currentIndex.value = nextIndex + runOnJS(loop)() + }) + } + } + }, []) + + function handleSwiperChange (current: number) { + if (props.current !== currentIndex.value) { + const eventData = getCustomEvent('change', {}, { detail: { current, source: 'touch' }, layoutRef: layoutRef }) + props.bindchange && props.bindchange(eventData) + } + } + + function getOffset (index:number, stepValue: number) { + if (!stepValue) return 0 + let targetOffset = 0 + if (props.circular && children.length > 1) { + const targetIndex = index + patchEleNum + targetOffset = -(stepValue * targetIndex - preMargin) + } else { + targetOffset = -index * stepValue + } + return targetOffset + } + + function loop () { + timerId.current = setTimeout(createAutoPlay, intervalTimer) + } + + function pauseLoop () { + timerId.current && clearTimeout(timerId.current) + } + + function resumeLoop () { + if (props.autoplay) { + loop() + } + } + // 1. 用户在当前页切换选中项,动画;用户携带选中index打开到swiper页直接选中不走动画 + useAnimatedReaction(() => currentIndex.value, (newIndex: number, preIndex: number) => { + // 这里必须传递函数名, 直接写()=> {}形式会报 访问了未sharedValue信息 + if (newIndex !== preIndex && props.bindchange) { + runOnJS(handleSwiperChange)(newIndex) + } + }) + + useEffect(() => { + let patchStep = 0 + if (preMargin !== previousMargin.value) { + patchStep = preMargin - previousMargin.value + } + if (nextMargin !== nextMarginShared.value) { + patchStep += nextMargin - nextMarginShared.value + } + previousMargin.value = preMargin + nextMarginShared.value = nextMargin + const newStep = step.value - patchStep + step.value = newStep + offset.value = getOffset(currentIndex.value, newStep) + }, [preMargin, nextMargin]) + + useEffect(() => { + childrenLength.value = children.length + if (children.length - 1 < currentIndex.value) { + pauseLoop() + currentIndex.value = 0 + offset.value = getOffset(0, step.value) + resumeLoop() + } + }, [children.length]) + + useEffect(() => { + if (!step.value) { + return + } + const targetOffset = getOffset(props.current || 0, step.value) + if (props.current !== undefined && props.current !== currentIndex.value) { + offset.value = withTiming(targetOffset, { + duration: easeDuration, + easing: easeMap[easeingFunc] + }, () => { + currentIndex.value = props.current || 0 + }) + } + }, [props.current]) + + useEffect(() => { + if (!step.value) { + return + } + if (props.autoplay) { + resumeLoop() + } else { + pauseLoop() + } + return () => { + if (props.autoplay) { + pauseLoop() + } + } + }, [props.autoplay]) + + const getTargetPosition = useCallback((eventData: EventDataType) => { + 'worklet' + // 移动的距离 + const { translation } = eventData + let resetOffsetPos = 0 + let selectedIndex = currentIndex.value + // 是否临界点 + let isCriticalItem = false + // 真实滚动到的偏移量坐标 + let moveToTargetPos = 0 + const currentOffset = translation < 0 ? offset.value - previousMargin.value : offset.value + previousMargin.value + const computedIndex = Math.abs(currentOffset) / step.value + const moveToIndex = translation < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex) + // 实际应该定位的索引值 + if (!circular.value) { + selectedIndex = moveToIndex + moveToTargetPos = selectedIndex * step.value + } else { + if (moveToIndex >= childrenLength.value + patchElementNum.value) { + selectedIndex = moveToIndex - (childrenLength.value + patchElementNum.value) + resetOffsetPos = (selectedIndex + patchElementNum.value) * step.value - previousMargin.value + moveToTargetPos = moveToIndex * step.value - previousMargin.value + isCriticalItem = true + } else if (moveToIndex <= patchElementNum.value - 1) { + selectedIndex = moveToIndex === 0 ? childrenLength.value - patchElementNum.value : childrenLength.value - 1 + resetOffsetPos = (selectedIndex + patchElementNum.value) * step.value - previousMargin.value + moveToTargetPos = moveToIndex * step.value - previousMargin.value + isCriticalItem = true + } else { + selectedIndex = moveToIndex - patchElementNum.value + moveToTargetPos = moveToIndex * step.value - previousMargin.value + } + } + return { + selectedIndex, + isCriticalItem, + resetOffset: -resetOffsetPos, + targetOffset: -moveToTargetPos + } + }, []) + + const canMove = useCallback((eventData: EventDataType) => { + 'worklet' + const { translation } = eventData + const currentOffset = Math.abs(offset.value) + if (!circular.value) { + if (translation < 0) { + return currentOffset < step.value * (childrenLength.value - 1) + } else { + return currentOffset > 0 + } + } else { + return true + } + }, []) + + const handleEnd = useCallback((eventData: EventDataType) => { + 'worklet' + const { isCriticalItem, targetOffset, resetOffset, selectedIndex } = getTargetPosition(eventData) + if (isCriticalItem) { + offset.value = withTiming(targetOffset, { + duration: easeDuration, + easing: easeMap[easeingFunc] + }, () => { + if (touchfinish.value !== false) { + currentIndex.value = selectedIndex + offset.value = resetOffset + runOnJS(resumeLoop)() + } + }) + } else { + offset.value = withTiming(targetOffset, { + duration: easeDuration, + easing: easeMap[easeingFunc] + }, () => { + if (touchfinish.value !== false) { + currentIndex.value = selectedIndex + runOnJS(resumeLoop)() + } + }) + } + }, []) + + const handleBack = useCallback((eventData: EventDataType) => { + 'worklet' + const { translation } = eventData + // 向右滑动的back:trans < 0, 向左滑动的back: trans < 0 + let currentOffset = Math.abs(offset.value) + if (circular.value) { + currentOffset += translation < 0 ? previousMargin.value : -previousMargin.value + } + const curIndex = currentOffset / step.value + const moveToIndex = (translation < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElementNum.value + const targetOffset = -(moveToIndex + patchElementNum.value) * step.value + (circular.value ? previousMargin.value : 0) + offset.value = withTiming(targetOffset, { + duration: easeDuration, + easing: easeMap[easeingFunc] + }, () => { + if (touchfinish.value !== false) { + currentIndex.value = moveToIndex + runOnJS(resumeLoop)() + } + }) + }, []) + + // 按照用户手指抬起的位置计算 + const handleLongPress = useCallback(() => { + 'worklet' + const currentOffset = Math.abs(offset.value) + let preOffset = (currentIndex.value + patchElementNum.value) * step.value + if (circular.value) { + preOffset -= previousMargin.value + } + // 正常事件中拿到的transition值(正向滑动<0,倒着滑>0) + const diffOffset = preOffset - currentOffset + const half = Math.abs(diffOffset) > step.value / 2 + if (+diffOffset === 0) { + runOnJS(resumeLoop)() + } else if (half) { + handleEnd({ translation: diffOffset }) + } else { + handleBack({ translation: diffOffset }) + } + }, []) + + const reachBoundary = useCallback((eventData: EventDataType) => { + 'worklet' + // 移动的距离 + const { translation } = eventData + const elementsLength = step.value * childrenLength.value + + let isBoundary = false + let resetOffset = 0 + // Y轴向下滚动, transDistance > 0, 向上滚动 < 0 X轴向左滚动, transDistance > 0 + const currentOffset = offset.value + const moveStep = Math.ceil(translation / elementsLength) + if (translation < 0) { + const posEnd = (childrenLength.value + patchElementNum.value + 1) * step.value + const posReverseEnd = (patchElementNum.value - 1) * step.value + if (currentOffset < -posEnd + step.value) { + isBoundary = true + resetOffset = Math.abs(moveStep) === 0 ? patchElementNum.value * step.value + translation : moveStep * elementsLength + } + if (currentOffset > -posReverseEnd) { + isBoundary = true + resetOffset = moveStep * elementsLength + } + } else if (translation > 0) { + const posEnd = (patchElementNum.value - 1) * step.value + const posReverseEnd = (patchElementNum.value + childrenLength.value) * step.value + if (currentOffset > -posEnd) { + isBoundary = true + resetOffset = moveStep * elementsLength + step.value + (moveStep === 1 ? translation : 0) + } + if (currentOffset < -posReverseEnd) { + isBoundary = true + resetOffset = moveStep * elementsLength + patchElementNum.value * step.value + } + } + return { + isBoundary, + resetOffset: -resetOffset + } + }, []) + + const gestureHandler = useMemo(() => { + const gesturePan = Gesture.Pan() + .onBegin((e) => { + 'worklet' + touchfinish.value = false + cancelAnimation(offset) + runOnJS(pauseLoop)() + preAbsolutePos.value = e[strAbso] + moveTranstion.value = e[strAbso] + moveTime.value = new Date().getTime() + }) + .onTouchesMove((e) => { + 'worklet' + const touchEventData = e.changedTouches[0] + const moveDistance = touchEventData[strAbso] - preAbsolutePos.value + const eventData = { + translation: moveDistance + } + // 处理用户一直拖拽到临界点的场景, 不会执行onEnd + if (!circular.value && !canMove(eventData)) { + return + } + const { isBoundary, resetOffset } = reachBoundary(eventData) + if (isBoundary && circular.value && !touchfinish.value) { + offset.value = resetOffset + } else { + offset.value = moveDistance + offset.value + } + preAbsolutePos.value = touchEventData[strAbso] + }) + .onTouchesUp((e) => { + 'worklet' + const touchEventData = e.changedTouches[0] + const moveDistance = touchEventData[strAbso] - moveTranstion.value + touchfinish.value = true + const eventData = { + translation: moveDistance + } + // 用户手指按下起来, 需要计算正确的位置, 比如在滑动过程中突然按下然后起来,需要计算到正确的位置 + if (!circular.value && !canMove(eventData)) { + return + } + const strVelocity = moveDistance / (new Date().getTime() - moveTime.value) * 1000 + if (Math.abs(strVelocity) < longPressRatio) { + handleLongPress() + } else { + handleEnd(eventData) + } + }) + return gesturePan + }, []) + + const animatedStyles = useAnimatedStyle(() => { + if (dir === 'x') { + return { transform: [{ translateX: offset.value }], opacity: step.value > 0 ? 1 : 0 } + } else { + return { transform: [{ translateY: offset.value }], opacity: step.value > 0 ? 1 : 0 } + } + }) + + function renderSwiper () { + const arrPages: Array | ReactNode = renderItems() + return ( + + {wrapChildren({ + children: arrPages + }, { + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps + })} + + {showsPagination && renderPagination()} + ) + } + + if (children.length === 1) { + return renderSwiper() + } else { + return ( + {renderSwiper()} + ) + } +}) +SwiperWrapper.displayName = 'MpxSwiperWrapper' + +export default SwiperWrapper diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/carouse.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/carouse.tsx deleted file mode 100644 index 6d00d236b6..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/carouse.tsx +++ /dev/null @@ -1,527 +0,0 @@ -/** - * swiper 实现 - */ -import { Animated, View, ScrollView, Dimensions, NativeSyntheticEvent, NativeScrollEvent, NativeScrollPoint, Platform, LayoutChangeEvent } from 'react-native' -import { JSX, forwardRef, useState, useRef, useEffect, ReactNode } from 'react' -import { CarouseProps, CarouseState } from './type' -import { getCustomEvent } from '../getInnerListeners' -import useNodesRef, { HandlerRef } from '../useNodesRef' // 引入辅助函数 -import { useTransformStyle, splitStyle, splitProps, useLayout, wrapChildren } from '../utils' - -/** - * 默认的Style类型 - */ -const styles: { [key: string]: Object } = { - slide: { - backgroundColor: 'transparent' - }, - container_x: { - position: 'relative' - }, - container_y: { - position: 'relative' - }, - pagination_x: { - position: 'absolute', - bottom: 25, - left: 0, - right: 0, - flexDirection: 'row', - flex: 1, - justifyContent: 'center', - alignItems: 'center' - }, - - pagination_y: { - position: 'absolute', - right: 15, - top: 0, - bottom: 0, - flexDirection: 'column', - flex: 1, - justifyContent: 'center', - alignItems: 'center' - } -} - -const _Carouse = forwardRef, CarouseProps>((props, ref): JSX.Element => { - // 默认取水平方向的width - const { width } = Dimensions.get('window') - const { - style, - previousMargin = 0, - nextMargin = 0, - enableVar, - externalVarContext, - parentFontSize, - parentWidth, - parentHeight - } = props - // 计算transform之类的 - const { - normalStyle, - hasVarDec, - varContextRef, - hasSelfPercent, - setWidth, - setHeight - } = useTransformStyle(style, { - enableVar, - externalVarContext, - parentFontSize, - parentWidth, - parentHeight - }) - const { textStyle, innerStyle } = splitStyle(normalStyle) - const { textProps } = splitProps(props) - const newChild = Array.isArray(props.children) ? props.children.filter(child => child) : props.children - const totalElements = Array.isArray(newChild) ? newChild.length : (newChild ? 1 : 0) - const defaultHeight = (normalStyle?.height || 150) - const defaultWidth = (normalStyle?.width || width || 375) - const dir = props.horizontal === false ? 'y' : 'x' - // state的offset默认值 - // const initIndex = props.circular ? props.current + 1: (props.current || 0) - // 记录真正的下标索引, 不包括循环前后加入的索引, 游标 - const initIndex = props.current || 0 - // 这里要排除超过元素个数的设置 - const initOffsetIndex = initIndex + (props.circular && totalElements > 1 ? 1 : 0) - const defaultX = (defaultWidth * initOffsetIndex) || 0 - const defaultY = (defaultHeight * initOffsetIndex) || 0 - // 主动scorllTo时是否要出发onScrollEnd - const needTriggerScrollEnd = useRef(true) - // 内部存储上一次的offset值 - const autoplayTimerRef = useRef | null>(null) - const scrollViewRef = useRef(null) - useNodesRef(props, ref, scrollViewRef, { - style: normalStyle - }) - const { - // 存储layout布局信息 - layoutRef, - layoutProps, - layoutStyle - } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout: onWrapperLayout }) - // 内部存储上一次的偏移量 - const internalsRef = useRef({ - offset: { - x: 0, - y: 0 - }, - isScrolling: false - }) - const isDragRef = useRef(false) - const [state, setState] = useState({ - width: dir === 'x' && typeof defaultWidth === 'number' ? defaultWidth - previousMargin - nextMargin : defaultWidth, - height: dir === 'y' && typeof defaultHeight === 'number' ? defaultHeight - previousMargin - nextMargin : defaultHeight, - // 真正的游标索引, 从0开始 - index: initIndex, - total: totalElements, - offset: { - x: 0, - y: 0 - }, - dir - } as CarouseState) - /** - * @desc: 开启下一次自动轮播 - */ - function createAutoPlay () { - autoplayTimerRef.current && clearTimeout(autoplayTimerRef.current) - autoplayTimerRef.current = setTimeout(() => { - startAutoPlay() - }, props.interval || 500) - } - - useEffect(() => { - // 确认这个是变化的props变化的时候才执行,还是初始化的时候就执行 - if (props.autoplay) { - createAutoPlay() - } - }, [props.autoplay, props.current, state.index, state.width, state.height]) - - useEffect(() => { - // 确认这个是变化的props变化的时候才执行,还是初始化的时候就执行 - if (!props.autoplay && props.current !== undefined && props.current !== state.index) { - const initIndex = props.current || 0 - // 这里要排除超过元素个数的设置 - const { nextIndex, nextOffset } = getMultiNextConfig(props.current) - // 1. 安卓需要主动更新下内部状态, 2. IOS不能触发完wcrollTo之后立即updateState, 会造成滑动两次 - // 2. setTimeout 是fix 当再渲染过程中触发scrollTo失败的问题 - if (Platform.OS === 'ios') { - needTriggerScrollEnd.current = false - setTimeout(() => { - scrollViewRef.current?.scrollTo({ - ...nextOffset, - animated: true - }) - }, 50) - } else { - updateState(nextIndex, nextOffset) - } - } - }, [props.current, state.width, state.height]) - - function getMultiNextConfig (target: number) { - const step = state.dir === 'x' ? state.width : state.height - const targetPos = step * props.current - const targetOffset = { - x: dir === 'x' ? targetPos : 0, - y: dir === 'y' ? targetPos : 0 - } - return { - nextIndex: target, - nextOffset: targetOffset - } - } - /** - * @desc: 更新状态: index和offset, 并响应索引变化的事件 - * scrollViewOffset: 移动到的目标位置 - */ - function updateIndex (scrollViewOffset: NativeScrollPoint, useIndex = false) { - const { nextIndex, nextOffset } = getNextConfig(scrollViewOffset) - updateState(nextIndex, nextOffset) - // 更新完状态之后, 开启新的loop - } - - /** - * 更新索引状态 - */ - function updateState (index: number, offset: { x: number, y: number}) { - internalsRef.current.offset = offset - setState((preState) => { - const newState = { - ...preState, - index: index, - // offset用来指示当前scrollView的偏移量 - offset: offset - } - return newState - }) - internalsRef.current.isScrolling = false - // getCustomEvent - const eventData = getCustomEvent('change', {}, { detail: { current: index, source: 'touch' }, layoutRef: layoutRef }) - props.bindchange && props.bindchange(eventData) - } - - /** - * @desc: 获取下一个位置的索引、scrollView的contentOffset、scrollTo到的offset - * @desc: 包括正循环、反向循环、不循环 - * 其中循环模式为了实现无缝链接, 会将结合contentOffset, 和 scrollTo的offset, - * 先scrollTo一个位置的坐标, 然后通过updateIndex设置真正的index和内容的offset,视觉上是无缝 - */ - function getNextConfig (scrollViewOffset: NativeScrollPoint) { - const step = state.dir === 'x' ? state.width : state.height - const currentOffset = state.offset - let nextIndex = state.index + 1 - let nextOffset = currentOffset - // autoMoveOffset scrollView 滚动到前后增加的位置 - let autoMoveOffset = currentOffset - let isBack = false - let isAutoEnd = false - // 如果是循环反向的 - if (scrollViewOffset?.[state.dir] < currentOffset[state.dir]) { - isBack = true - nextIndex = isBack ? nextIndex - 2 : nextIndex - } - if (!props.circular) { - nextOffset = Object.assign({}, currentOffset, { [state.dir]: step * nextIndex }) - } else { - if (isBack) { - if (nextIndex < 0) { - // 反向: scollView 滚动到虚拟的位置 - autoMoveOffset = Object.assign({}, currentOffset, { [state.dir]: 0 }) - nextIndex = state.total - 1 - // 反向: 数组最后一个index - nextOffset = Object.assign({}, currentOffset, { [state.dir]: step * state.total }) - isAutoEnd = true - } else { - // 反向非最后一个 - nextOffset = Object.assign({}, currentOffset, { [state.dir]: (nextIndex + 1) * step }) - } - } else { - if (nextIndex > state.total - 1) { - autoMoveOffset = Object.assign({}, currentOffset, { [state.dir]: step * (nextIndex + 1) }) - nextIndex = 0 - nextOffset = Object.assign({}, currentOffset, { [state.dir]: step }) - isAutoEnd = true - } else { - // nextIndex = nextIndex, - nextOffset = Object.assign({}, currentOffset, { [state.dir]: (nextIndex + 1) * step }) - } - } - } - return { - // 下一个要滚动到的实际元素的索引 - nextIndex, - // 下一个要滚动到实际元素的offset - nextOffset, - // scrollTo一个位置的坐标, 虚拟元素的位置 - autoMoveOffset, - isAutoEnd - } - } - - /** - * @desc: 开启自动轮播 - */ - function startAutoPlay () { - if (state.width && isNaN(+state.width)) { - createAutoPlay() - return - } - if (!Array.isArray(props.children)) { - return - } - const step = state.dir === 'x' ? state.width : state.height - const { nextOffset, autoMoveOffset, isAutoEnd } = getNextConfig(state.offset) - // 这里可以scroll到下一个元素, 但是把scrollView的偏移量在设置为content,视觉效果就没了吧 - if (Platform.OS === 'ios') { - if (!isAutoEnd) { - scrollViewRef.current?.scrollTo({ x: nextOffset.x, y: nextOffset.y, animated: true }) - } else { - if (state.dir === 'x') { - scrollViewRef.current?.scrollTo({ x: autoMoveOffset.x, y: autoMoveOffset.x, animated: true }) - } else { - scrollViewRef.current?.scrollTo({ x: autoMoveOffset.y, y: autoMoveOffset.y, animated: true }) - } - } - } else { - if (!isAutoEnd) { - scrollViewRef.current?.scrollTo({ x: nextOffset.x, y: nextOffset.y, animated: true }) - onScrollEnd({ - nativeEvent: { - contentOffset: { - x: +nextOffset.x, - y: +nextOffset.y - } - } - } as NativeSyntheticEvent) - } else { - // 安卓无法实现视觉的无缝连接, 只能回到真正的位置, 且安卓调用scrollTo不能触发onMomentumScrollEnd,还未找到为啥 - if (state.dir === 'x') { - scrollViewRef.current?.scrollTo({ x: step, y: step, animated: true }) - } else { - scrollViewRef.current?.scrollTo({ x: autoMoveOffset.x, y: step, animated: true }) - } - updateState(0, nextOffset) - } - } - } - - /** - * 当用户开始拖动此视图时调用此函数, 更新当前在滚动态 - */ - function onScrollBegin () { - internalsRef.current.isScrolling = true - } - - /** - * 当用户开始拖动结束 - * 注意: 当手动调用scrollTo的时候, 安卓不会触发onMomentumScrollEnd, IOS会触发onMomentumScrollEnd - */ - function onScrollEnd (event: NativeSyntheticEvent) { - if (Platform.OS === 'ios' && !needTriggerScrollEnd.current) { - const { nextIndex, nextOffset } = getMultiNextConfig(props.current) - updateState(nextIndex, nextOffset) - needTriggerScrollEnd.current = true - return - } - if (totalElements === 1) { - return - } - internalsRef.current.isScrolling = false - // 用户手动滑动更新索引后,如果开启了自动轮播等重新开始 - updateIndex(event.nativeEvent.contentOffset, true) - } - - /** - * 当拖拽结束时,检测是否可滚动 - */ - function onScrollEndDrag (event: NativeSyntheticEvent) { - const { contentOffset } = event.nativeEvent - const { index, total } = state - isDragRef.current = true - const internalOffset = internalsRef.current.offset - const previousOffset = props.horizontal ? internalOffset.x : internalOffset.y - const moveOffset = props.horizontal ? contentOffset.x : contentOffset.y - if (previousOffset === moveOffset && (index === 0 || index === total - 1)) { - internalsRef.current.isScrolling = false - } - } - - /** - * @desc: 水平方向时,获取元素的布局,更新, 其中如果传递100%时需要依赖measure计算元算的宽高 - */ - function onWrapperLayout (e: LayoutChangeEvent) { - scrollViewRef.current?.measure((x: number, y: number, width: number, height: number, offsetLeft: number, offsetTop: number) => { - layoutRef.current = { x, y, width, height, offsetLeft, offsetTop } - const isWDiff = state.width !== width - const isHDiff = state.height !== height - if (isWDiff || isHDiff) { - const changeState = { - width: isWDiff ? width : state.width, - height: isHDiff ? height : state.height - } - const attr = state.dir === 'x' ? 'width' : 'height' - changeState[attr] = changeState[attr] - previousMargin - nextMargin - const correctOffset = Object.assign({}, state.offset, { - [state.dir]: initOffsetIndex * (state.dir === 'x' ? changeState.width : changeState.height) - }) - state.width = changeState.width - state.height = changeState.height - // 这里setState之后,会再触发重新渲染, renderScrollView会再次触发onScrollEnd, - setState((preState) => { - return { - ...preState, - width: changeState.width, - height: changeState.height - } - }) - } - props.getInnerLayout && props.getInnerLayout(layoutRef) - }) - } - - function getOffset (): Array { - const step = state.dir === 'x' ? state.width : state.height - if (!step || Number.isNaN(+step)) return [] - const offsetArray = [] - for (let i = 0; i < totalElements; i++) { - offsetArray.push(i * step) - } - return offsetArray - } - - function renderScrollView (pages: ReactNode) { - const offsetsArray = getOffset() - const scrollElementProps = { - ref: scrollViewRef, - horizontal: props.horizontal, - pagingEnabled: true, - snapToOffsets: offsetsArray, - decelerationRate: 0.99, // 'fast' - showsHorizontalScrollIndicator: false, - showsVerticalScrollIndicator: false, - bounces: false, - scrollsToTop: false, - removeClippedSubviews: true, - automaticallyAdjustContentInsets: false - } - const layoutStyle = dir === 'x' ? { width: defaultWidth, height: defaultHeight } : { width: defaultWidth } - return ( - - {pages} - - ) - } - - function renderPagination () { - if (state.total <= 1) return null - const dots: Array = [] - const activeDotStyle = [{ - backgroundColor: props.activeDotColor || '#007aff', - width: 8, - height: 8, - borderRadius: 4, - marginLeft: 3, - marginRight: 3, - marginTop: 3, - marginBottom: 3 - }] - const dotStyle = [{ - backgroundColor: props.dotColor || 'rgba(0,0,0,.2)', - width: 8, - height: 8, - borderRadius: 4, - marginLeft: 3, - marginRight: 3, - marginTop: 3, - marginBottom: 3 - }] - for (let i = 0; i < state.total; i++) { - if (i === state.index) { - dots.push() - } else { - dots.push() - } - } - return ( - - {dots} - - ) - } - - function renderPages () { - const { width, height } = state - const { children } = props - const { circular } = props - const pageStyle = { width: width, height: height } - // 设置了previousMargin或者nextMargin, - // 1. 元素的宽度是减去这两个数目之和 - // 2. previousMargin设置marginLeft正值, nextmargin设置marginRight负值 - // 3. 第一个元素设置previousMargin 和 nextMargin, 最后一个元素 - if (totalElements > 1 && Array.isArray(children)) { - let arrElements: (Array) = [] - // pages = ["2", "0", "1", "2", "0"] - const pages = Array.isArray(children) ? Object.keys(children) : [] - /* 无限循环的时候 */ - if (circular) { - pages.unshift(totalElements - 1 + '') - pages.push('0') - } - arrElements = pages.map((page, i) => { - const extraStyle = {} as { - [key: string]: any - } - if (i === 0 && dir === 'x' && typeof width === 'number') { - previousMargin && (extraStyle.marginLeft = previousMargin) - } else if (i === pages.length - 1 && typeof width === 'number') { - nextMargin && (extraStyle.marginRight = nextMargin) - } - return ( - {wrapChildren( - { - children: children[+page] - }, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - )} - ) - }) - return arrElements - } else { - const realElement = ( - - {children} - - ) - return realElement - } - } - - const pages: Array | ReactNode = renderPages() - return ( - {renderScrollView(pages)} - {props.showsPagination && renderPagination()} - ) -}) - -_Carouse.displayName = 'MpxCarouse' - -export default _Carouse diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/index.tsx deleted file mode 100644 index 46f6d8daeb..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { ScrollView } from 'react-native' -import { JSX, MutableRefObject, forwardRef, useRef } from 'react' -import Carouse from './carouse' -import { SwiperProps } from './type' -import useInnerProps from '../getInnerListeners' -import useNodesRef, { HandlerRef } from '../useNodesRef' // 引入辅助函数 -/** - * ✔ indicator-dots - * ✔ indicator-color - * ✔ indicator-active-color - * ✔ autoplay - * ✔ current - * ✔ interval - * ✔ duration - * ✔ circular - * ✔ vertical - * ✘ display-multiple-items - * ✘ previous-margin - * ✘ next-margin - * ✔ easing-function ="easeOutCubic" - * ✘ snap-to-edge - */ -const _SwiperWrapper = forwardRef, SwiperProps>((props: SwiperProps, ref): JSX.Element => { - const { children } = props - const innerLayout = useRef({}) - const swiperProp = { - circular: props.circular || false, - current: props.current || 0, - autoplay: props.autoplay || false, - duration: props.duration || 500, - interval: props.interval || 5000, - showsPagination: props['indicator-dots'], - dotColor: props['indicator-color'] || 'rgba(0, 0, 0, .3)', - activeDotColor: props['indicator-active-color'] || '#000000', - horizontal: props.vertical !== undefined ? !props.vertical : true, - previousMargin: props['previous-margin'] ? parseInt(props['previous-margin']) : 0, - nextMargin: props['next-margin'] ? parseInt(props['next-margin']) : 0, - enableOffset: props['enable-offset'] || true, - enableVar: props['enable-var'] || false, - parentFontSize: props['parent-font-size'], - parentWidth: props['parent-width'], - parentHeight: props['parent-height'], - style: props.style || {}, - externalVarContext: props['external-var-context'], - bindchange: props.bindchange, - easingFunction: props['easing-function'] || 'default' - } - - const nodeRef = useRef(null) - useNodesRef(props, ref, nodeRef, {}) - - const innerProps = useInnerProps(props, { - ref: nodeRef - }, [ - 'indicator-dots', - 'indicator-color', - 'indicator-active-color', - 'previous-margin', - 'vertical', - 'previous-margin', - 'next-margin', - 'easing-function' - ], { layoutRef: innerLayout }) - - const getInnerLayout = (layout: MutableRefObject<{}>) => { - innerLayout.current = layout.current - } - - return - {children} - -}) - -_SwiperWrapper.displayName = 'MpxSwiper' - -export default _SwiperWrapper diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/type.ts b/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/type.ts deleted file mode 100644 index 2815f9acc7..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-swiper/type.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ReactNode } from 'react' -import { NativeSyntheticEvent } from 'react-native' - -export type EaseType = 'default' | 'linear' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' - -export interface SwiperProps { - children?: ReactNode; - circular?: boolean; - current?: number; - interval?: number; - autoplay?: boolean; - duration?: number; - 'indicator-dots'?: boolean; - 'indicator-color'?: string; - 'indicator-active-color'?: string; - vertical?: boolean; - style: { - [key: string]: any - }; - 'easing-function'?: EaseType; - 'previous-margin'?: string; - 'next-margin'?: string; - 'enable-offset'?: boolean; - 'enable-var': boolean; - 'parent-font-size'?: number; - 'parent-width'?: number; - 'parent-height'?: number; - 'external-var-context'?: Record; - bindchange?: (event: NativeSyntheticEvent | unknown) => void; -} - -export interface CarouseProps { - children?: ReactNode; - circular?: boolean; - current: number; - autoplay?: boolean; - duration?: number; - interval?: number; - showsPagination?: boolean; - dotColor?: string; - activeDotColor?: string; - horizontal?: boolean; - easingFunction?: EaseType; - previousMargin?: number; - nextMargin?: number; - enableOffset?: boolean; - parentFontSize?: number; - parentWidth?: number; - parentHeight?: number; - bindchange?: (event: NativeSyntheticEvent | unknown) => void; - getInnerLayout: Function; - innerProps: Object; - style: { - [key: string]: any - }; - enableVar: boolean; - externalVarContext?: Record; -} - -export interface CarouseState { - children: Array | ReactNode, - width: number; - height: number; - index: number; - total: number; - // 设置scrollView初始的滚动坐标,contentOffset - offset: { - x: number, - y: number - }; - // 是否结束自动轮播,手动设置滚动到具体位置时结束 - autoplayEnd: boolean; - loopJump: boolean; - dir: 'x' | 'y'; - isScrollEnd: boolean -} - -export interface ScrollElementProps { - pagingEnabled: boolean, - showsHorizontalScrollIndicator: boolean, - showsVerticalScrollIndicator: boolean, - bounces: boolean, - scrollsToTop: boolean, - removeClippedSubviews: boolean, - automaticallyAdjustContentInsets: boolean, - horizontal: boolean -}