Skip to content

Commit 7c08485

Browse files
authored
Merge pull request #2114 from didi/fix-swiper-nocircular
1. 支持一个元素和非循环边界元素阻力滑动效果, 2. 支持滑动过程中超过一半更新索引能力,3. 修复循环边界offset计算逻辑
2 parents 4da7979 + 819ef14 commit 7c08485

File tree

1 file changed

+155
-75
lines changed

1 file changed

+155
-75
lines changed

packages/webpack-plugin/lib/runtime/components/react/mpx-swiper.tsx

Lines changed: 155 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { View, NativeSyntheticEvent, LayoutChangeEvent } from 'react-native'
2-
import { GestureDetector, Gesture, PanGesture } from 'react-native-gesture-handler'
2+
import { GestureDetector, Gesture, PanGesture, GestureStateChangeEvent, PanGestureHandlerEventPayload } from 'react-native-gesture-handler'
33
import Animated, { useAnimatedStyle, useSharedValue, withTiming, Easing, runOnJS, useAnimatedReaction, cancelAnimation } from 'react-native-reanimated'
44

55
import React, { JSX, forwardRef, useRef, useEffect, ReactNode, ReactElement, useMemo, createElement } from 'react'
@@ -26,8 +26,12 @@ import Portal from './mpx-portal'
2626
*/
2727
type EaseType = 'default' | 'linear' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic'
2828
type StrAbsoType = 'absoluteX' | 'absoluteY'
29+
type StrVelocityType = 'velocityX' | 'velocityY'
2930
type EventDataType = {
31+
// 和上一帧offset值的对比
3032
translation: number
33+
// onUpdate时根据上一个判断方向,onFinalize根据transformStart判断
34+
transdir: number
3135
}
3236

3337
interface SwiperProps {
@@ -46,15 +50,15 @@ interface SwiperProps {
4650
vertical?: boolean
4751
style: {
4852
[key: string]: any
49-
};
53+
}
5054
'easing-function'?: EaseType
5155
'previous-margin'?: string
5256
'next-margin'?: string
5357
'enable-offset'?: boolean
5458
'enable-var': boolean
5559
'parent-font-size'?: number
5660
'parent-width'?: number
57-
'parent-height'?: number;
61+
'parent-height'?: number
5862
'external-var-context'?: Record<string, any>
5963
'wait-for'?: Array<GestureHandler>
6064
'simultaneous-handlers'?: Array<GestureHandler>
@@ -199,14 +203,13 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
199203
// 记录元素的偏移量
200204
const offset = useSharedValue(getOffset(props.current || 0, initStep))
201205
const strAbso = 'absolute' + dir.toUpperCase() as StrAbsoType
206+
const strVelocity = 'velocity' + dir.toUpperCase() as StrVelocityType
202207
// 标识手指触摸和抬起, 起点在onBegin
203208
const touchfinish = useSharedValue(true)
204209
// 记录上一帧的绝对定位坐标
205210
const preAbsolutePos = useSharedValue(0)
206211
// 记录从onBegin 到 onTouchesUp 时移动的距离
207212
const moveTranstion = useSharedValue(0)
208-
// 记录从onBegin 到 onTouchesUp 的时间
209-
const moveTime = useSharedValue(0)
210213
const timerId = useRef(0 as number | ReturnType<typeof setTimeout>)
211214
const intervalTimer = props.interval || 500
212215

@@ -505,7 +508,11 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
505508
}, [children.length])
506509

507510
useEffect(() => {
508-
updateCurrent(props.current || 0, step.value)
511+
// 1. 如果用户在touch的过程中, 外部更新了current以外部为准(小程序表现)
512+
// 2. 手指滑动过程中更新索引,外部会把current再传入进来,导致offset直接更新,增加判断不同才更新
513+
if (props.current !== currentIndex.value) {
514+
updateCurrent(props.current || 0, step.value)
515+
}
509516
}, [props.current])
510517

511518
useEffect(() => {
@@ -529,17 +536,17 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
529536
function getTargetPosition (eventData: EventDataType) {
530537
'worklet'
531538
// 移动的距离
532-
const { translation } = eventData
539+
const { transdir } = eventData
533540
let resetOffsetPos = 0
534541
let selectedIndex = currentIndex.value
535542
// 是否临界点
536543
let isCriticalItem = false
537544
// 真实滚动到的偏移量坐标
538545
let moveToTargetPos = 0
539546
const tmp = !circularShared.value ? 0 : preMarginShared.value
540-
const currentOffset = translation < 0 ? offset.value - tmp : offset.value + tmp
547+
const currentOffset = transdir < 0 ? offset.value - tmp : offset.value + tmp
541548
const computedIndex = Math.abs(currentOffset) / step.value
542-
const moveToIndex = translation < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex)
549+
const moveToIndex = transdir < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex)
543550
// 实际应该定位的索引值
544551
if (!circularShared.value) {
545552
selectedIndex = moveToIndex
@@ -569,13 +576,17 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
569576
}
570577
function canMove (eventData: EventDataType) {
571578
'worklet'
572-
const { translation } = eventData
573-
const currentOffset = Math.abs(offset.value)
579+
// 旧版:如果在快速多次滑动时,只根据当前的offset判断,会出现offset没超出,加上translation后越界的场景(如在倒数第二个元素快速滑动)
580+
// 新版:会加上translation
581+
const { translation, transdir } = eventData
582+
const gestureMovePos = offset.value + translation
574583
if (!circularShared.value) {
575-
if (translation < 0) {
576-
return currentOffset < step.value * (childrenLength.value - 1)
584+
// 如果只判断区间,中间非滑动状态(handleResistanceMove)向左滑动,突然改为向右滑动,但是还在非滑动态,本应该可滑动判断为了不可滑动
585+
const posEnd = -step.value * (childrenLength.value - 1)
586+
if (transdir < 0) {
587+
return gestureMovePos > posEnd
577588
} else {
578-
return currentOffset > 0
589+
return gestureMovePos < 0
579590
}
580591
} else {
581592
return true
@@ -607,25 +618,16 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
607618
})
608619
}
609620
}
610-
function handleBackInit () {
611-
'worklet'
612-
// 微信的效果
613-
// 1. 只有一个元素,即使设置了circular,也不会产生循环的效果,2. 可以响应手势,但是会有回弹的效果
614-
offset.value = withTiming(0, {
615-
duration: easeDuration,
616-
easing: easeMap[easeingFunc]
617-
})
618-
}
619621
function handleBack (eventData: EventDataType) {
620622
'worklet'
621-
const { translation } = eventData
623+
const { transdir } = eventData
622624
// 向右滑动的back:trans < 0, 向左滑动的back: trans < 0
623625
let currentOffset = Math.abs(offset.value)
624626
if (circularShared.value) {
625-
currentOffset += translation < 0 ? preMarginShared.value : -preMarginShared.value
627+
currentOffset += transdir < 0 ? preMarginShared.value : -preMarginShared.value
626628
}
627629
const curIndex = currentOffset / step.value
628-
const moveToIndex = (translation < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElmNumShared.value
630+
const moveToIndex = (transdir < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElmNumShared.value
629631
const targetOffset = -(moveToIndex + patchElmNumShared.value) * step.value + (circularShared.value ? preMarginShared.value : 0)
630632
offset.value = withTiming(targetOffset, {
631633
duration: easeDuration,
@@ -637,112 +639,190 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
637639
}
638640
})
639641
}
640-
function handleLongPress () {
642+
// 当前的offset和index多对应的offset进行对比,判断是否超过一半
643+
function computeHalf (eventData: EventDataType) {
641644
'worklet'
645+
const { transdir } = eventData
642646
const currentOffset = Math.abs(offset.value)
643647
let preOffset = (currentIndex.value + patchElmNumShared.value) * step.value
644648
if (circularShared.value) {
645649
preOffset -= preMarginShared.value
646650
}
647-
// 正常事件中拿到的transition值(正向滑动<0,倒着滑>0)
651+
// 正常事件中拿到的translation值(正向滑动<0,倒着滑>0)
648652
const diffOffset = preOffset - currentOffset
649653
const half = Math.abs(diffOffset) > step.value / 2
654+
const isTriggerUpdateHalf = (transdir < 0 && currentOffset < preOffset) || (transdir > 0 && currentOffset > preOffset)
655+
return {
656+
diffOffset,
657+
half,
658+
isTriggerUpdateHalf
659+
}
660+
}
661+
function handleLongPress (eventData: EventDataType) {
662+
'worklet'
663+
const { diffOffset, half, isTriggerUpdateHalf } = computeHalf(eventData)
650664
if (+diffOffset === 0) {
651665
runOnJS(resumeLoop)()
666+
} else if (isTriggerUpdateHalf) {
667+
// 如果触发了onUpdate时的索引变更,则直接以update时的index为准
668+
const targetIndex = !circularShared.value ? currentIndex.value : currentIndex.value + patchElmNumShared.value - 1
669+
offset.value = withTiming(-targetIndex * step.value, {
670+
duration: easeDuration,
671+
easing: easeMap[easeingFunc]
672+
}, () => {
673+
if (touchfinish.value !== false) {
674+
currentIndex.value = targetIndex
675+
runOnJS(resumeLoop)()
676+
}
677+
})
652678
} else if (half) {
653-
handleEnd({ translation: diffOffset })
679+
handleEnd(eventData)
654680
} else {
655-
handleBack({ translation: diffOffset })
681+
handleBack(eventData)
656682
}
657683
}
658684
function reachBoundary (eventData: EventDataType) {
659685
'worklet'
660-
// 移动的距离
686+
// 1. 基于当前的offset和translation判断是否超过当前边界值
661687
const { translation } = eventData
662-
const elementsLength = step.value * childrenLength.value
688+
const boundaryStart = -patchElmNumShared.value * step.value
689+
const boundaryEnd = -(childrenLength.value + patchElmNumShared.value) * step.value
690+
const moveToOffset = offset.value + translation
663691
let isBoundary = false
664692
let resetOffset = 0
665-
// Y轴向下滚动, transDistance > 0, 向上滚动 < 0 X轴向左滚动, transDistance > 0
666-
const currentOffset = offset.value
667-
const moveStep = Math.ceil(translation / elementsLength)
668-
if (translation < 0) {
669-
const posEnd = (childrenLength.value + patchElmNumShared.value + 1) * step.value
670-
const posReverseEnd = (patchElmNumShared.value - 1) * step.value
671-
if (currentOffset < -posEnd + step.value) {
672-
isBoundary = true
673-
resetOffset = Math.abs(moveStep) === 0 ? patchElmNumShared.value * step.value + translation : moveStep * elementsLength
674-
}
675-
if (currentOffset > -posReverseEnd) {
676-
isBoundary = true
677-
resetOffset = moveStep * elementsLength
678-
}
679-
} else if (translation > 0) {
680-
const posEnd = (patchElmNumShared.value - 1) * step.value
681-
const posReverseEnd = (patchElmNumShared.value + childrenLength.value) * step.value
682-
if (currentOffset > -posEnd) {
683-
isBoundary = true
684-
resetOffset = moveStep * elementsLength + step.value + (moveStep === 1 ? translation : 0)
685-
}
686-
if (currentOffset < -posReverseEnd) {
687-
isBoundary = true
688-
resetOffset = moveStep * elementsLength + patchElmNumShared.value * step.value
689-
}
693+
if (moveToOffset < boundaryEnd) {
694+
isBoundary = true
695+
// 超过边界的距离
696+
const exceedLength = Math.abs(moveToOffset) - Math.abs(boundaryEnd)
697+
// 计算对标正常元素所在的offset
698+
resetOffset = patchElmNumShared.value * step.value + exceedLength
699+
}
700+
if (moveToOffset > boundaryStart) {
701+
isBoundary = true
702+
// 超过边界的距离
703+
const exceedLength = Math.abs(boundaryStart) - Math.abs(moveToOffset)
704+
// 计算对标正常元素所在的offset
705+
resetOffset = (patchElmNumShared.value + childrenLength.value - 1) * step.value + (step.value - exceedLength)
690706
}
691707
return {
692708
isBoundary,
693709
resetOffset: -resetOffset
694710
}
695711
}
712+
// 非循环超出边界,应用阻力; 开始滑动少阻力小,滑动越长阻力越大
713+
function handleResistanceMove (eventData: EventDataType) {
714+
'worklet'
715+
const { translation, transdir } = eventData
716+
const moveToOffset = offset.value + translation
717+
const maxOverDrag = Math.floor(step.value / 2)
718+
const maxOffset = translation < 0 ? -(childrenLength.value - 1) * step.value : 0
719+
let resistance = 0.1
720+
let overDrag = 0
721+
let finalOffset = 0
722+
// 向右向下小于0, 向左向上大于0;
723+
if (transdir < 0) {
724+
overDrag = Math.abs(moveToOffset - maxOffset)
725+
} else {
726+
overDrag = Math.abs(moveToOffset)
727+
}
728+
// 滑动越多resistance越小
729+
resistance = 1 - overDrag / maxOverDrag
730+
// 确保阻力在合理范围内
731+
resistance = Math.min(0.5, resistance)
732+
// 限制在最大拖拽范围内
733+
if (transdir < 0) {
734+
const adjustOffset = offset.value + translation * resistance
735+
finalOffset = Math.max(adjustOffset, maxOffset - maxOverDrag)
736+
} else {
737+
const adjustOffset = offset.value + translation * resistance
738+
finalOffset = Math.min(adjustOffset, maxOverDrag)
739+
}
740+
return finalOffset
741+
}
696742
const gesturePan = Gesture.Pan()
697-
.onBegin((e) => {
743+
.onBegin((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
698744
'worklet'
699745
if (!step.value) return
700746
touchfinish.value = false
701747
cancelAnimation(offset)
702748
runOnJS(pauseLoop)()
703749
preAbsolutePos.value = e[strAbso]
704750
moveTranstion.value = e[strAbso]
705-
moveTime.value = new Date().getTime()
706751
})
707-
.onTouchesMove((e) => {
752+
.onUpdate((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
708753
'worklet'
709754
if (touchfinish.value) return
710-
const touchEventData = e.changedTouches[0]
711-
const moveDistance = touchEventData[strAbso] - preAbsolutePos.value
755+
const moveDistance = e[strAbso] - preAbsolutePos.value
712756
const eventData = {
713-
translation: moveDistance
757+
translation: moveDistance,
758+
transdir: moveDistance !== 0 ? moveDistance : e[strAbso] - moveTranstion.value
714759
}
715-
// 处理用户一直拖拽到临界点的场景, 不会执行onEnd
716-
if (!circularShared.value && !canMove(eventData)) {
760+
// 1. 支持滑动中超出一半更新索引的能力:只更新索引并不会影响onFinalize依据当前offset计算的索引
761+
const { half } = computeHalf(eventData)
762+
if (childrenLength.value > 1 && half) {
763+
const { selectedIndex } = getTargetPosition(eventData)
764+
currentIndex.value = selectedIndex
765+
}
766+
// 2. 非循环: 处理用户一直拖拽到临界点的场景,如果放到onFinalize无法阻止offset.value更新为越界的值
767+
if (!circularShared.value) {
768+
if (canMove(eventData)) {
769+
offset.value = moveDistance + offset.value
770+
} else {
771+
const finalOffset = handleResistanceMove(eventData)
772+
offset.value = finalOffset
773+
}
774+
preAbsolutePos.value = e[strAbso]
717775
return
718776
}
777+
// 3. 循环更新: 只有一个元素时可滑动,加入阻力
778+
if (circularShared.value && childrenLength.value === 1) {
779+
const finalOffset = handleResistanceMove(eventData)
780+
offset.value = finalOffset
781+
preAbsolutePos.value = e[strAbso]
782+
return
783+
}
784+
// 4. 循环更新:正常
719785
const { isBoundary, resetOffset } = reachBoundary(eventData)
720786
if (childrenLength.value > 1 && isBoundary && circularShared.value) {
721787
offset.value = resetOffset
722788
} else {
723789
offset.value = moveDistance + offset.value
724790
}
725-
preAbsolutePos.value = touchEventData[strAbso]
791+
preAbsolutePos.value = e[strAbso]
726792
})
727-
.onTouchesUp((e) => {
793+
.onFinalize((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
728794
'worklet'
729795
if (touchfinish.value) return
730-
const touchEventData = e.changedTouches[0]
731-
const moveDistance = touchEventData[strAbso] - moveTranstion.value
732796
touchfinish.value = true
797+
// 触发过onUpdate正常情况下e[strAbso] - preAbsolutePos.value=0; 未触发过onUpdate的情况下e[strAbso] - preAbsolutePos.value 不为0
798+
const moveDistance = e[strAbso] - preAbsolutePos.value
733799
const eventData = {
734-
translation: moveDistance
800+
translation: moveDistance,
801+
transdir: moveDistance !== 0 ? moveDistance : e[strAbso] - moveTranstion.value
735802
}
803+
// 1. 只有一个元素:循环 和 非循环状态,都走回弹效果
736804
if (childrenLength.value === 1) {
737-
return handleBackInit()
805+
offset.value = withTiming(0, {
806+
duration: easeDuration,
807+
easing: easeMap[easeingFunc]
808+
})
809+
return
738810
}
739-
// 用户手指按下起来, 需要计算正确的位置, 比如在滑动过程中突然按下然后起来,需要计算到正确的位置
811+
// 2.非循环状态不可移动态:最后一个元素 和 第一个元素
812+
// 非循环支持最后元素可滑动能力后,向左快速移动未超过最大可移动范围一半,因为offset为正值,向左滑动handleBack,默认向上取整
813+
// 但是在offset大于0时,取0。[-100, 0](back取0), [0, 100](back取1), 所以handleLongPress里的处理逻辑需要兼容支持,因此这里直接单独处理,不耦合下方公共的判断逻辑。
740814
if (!circularShared.value && !canMove(eventData)) {
815+
if (eventData.transdir < 0) {
816+
handleBack(eventData)
817+
} else {
818+
handleEnd(eventData)
819+
}
741820
return
742821
}
743-
const strVelocity = moveDistance / (new Date().getTime() - moveTime.value) * 1000
744-
if (Math.abs(strVelocity) < longPressRatio) {
745-
handleLongPress()
822+
// 3. 非循环状态可移动态、循环状态, 正常逻辑处理
823+
const velocity = e[strVelocity]
824+
if (Math.abs(velocity) < longPressRatio) {
825+
handleLongPress(eventData)
746826
} else {
747827
handleEnd(eventData)
748828
}

0 commit comments

Comments
 (0)