@fountain-ui/lab 2.0.0-beta.13 → 2.0.0-beta.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/build/commonjs/Carousel/Carousel.js +27 -30
  2. package/build/commonjs/Carousel/Carousel.js.map +1 -1
  3. package/build/commonjs/Carousel/animation/createDefaultScrollAnimation.js +2 -2
  4. package/build/commonjs/Carousel/animation/createDefaultScrollAnimation.js.map +1 -1
  5. package/build/commonjs/Carousel/animation/parallaxItemStyleFactory.js +2 -2
  6. package/build/commonjs/Carousel/animation/parallaxItemStyleFactory.js.map +1 -1
  7. package/build/commonjs/Carousel/components/ItemView.js +2 -2
  8. package/build/commonjs/Carousel/components/ItemView.js.map +1 -1
  9. package/build/commonjs/Carousel/components/ScrollViewGesture.js +6 -6
  10. package/build/commonjs/Carousel/components/ScrollViewGesture.js.map +1 -1
  11. package/build/commonjs/Carousel/{hooks → components}/useItemInterpolation.js +6 -4
  12. package/build/commonjs/Carousel/components/useItemInterpolation.js.map +1 -0
  13. package/build/commonjs/Carousel/hooks/index.js +0 -16
  14. package/build/commonjs/Carousel/hooks/index.js.map +1 -1
  15. package/build/commonjs/Carousel/hooks/useIndexController.js +23 -45
  16. package/build/commonjs/Carousel/hooks/useIndexController.js.map +1 -1
  17. package/build/commonjs/Carousel/hooks/useItemVisibilityStore.js +12 -12
  18. package/build/commonjs/Carousel/hooks/useItemVisibilityStore.js.map +1 -1
  19. package/build/commonjs/Carousel/hooks/usePagingAnimation.js +72 -56
  20. package/build/commonjs/Carousel/hooks/usePagingAnimation.js.map +1 -1
  21. package/build/commonjs/Carousel/tick.js +16 -0
  22. package/build/commonjs/Carousel/tick.js.map +1 -0
  23. package/build/commonjs/Carousel/types.js +1 -0
  24. package/build/commonjs/Carousel/types.js.map +1 -1
  25. package/build/commonjs/ViewPager/ChildrenMemoizedPage.js +53 -47
  26. package/build/commonjs/ViewPager/ChildrenMemoizedPage.js.map +1 -1
  27. package/build/commonjs/ViewPager/InternalContext.js +17 -0
  28. package/build/commonjs/ViewPager/InternalContext.js.map +1 -0
  29. package/build/commonjs/ViewPager/ViewPagerNative.js +40 -17
  30. package/build/commonjs/ViewPager/ViewPagerNative.js.map +1 -1
  31. package/build/commonjs/ViewPager/ViewPagerProps.js.map +1 -1
  32. package/build/commonjs/ViewPager/ViewPagerWeb.js +19 -8
  33. package/build/commonjs/ViewPager/ViewPagerWeb.js.map +1 -1
  34. package/build/commonjs/ViewPager/index.js.map +1 -1
  35. package/build/commonjs/ViewPager/types.js +6 -0
  36. package/build/commonjs/ViewPager/types.js.map +1 -0
  37. package/build/commonjs/ViewPager/usePageStore.js +35 -0
  38. package/build/commonjs/ViewPager/usePageStore.js.map +1 -0
  39. package/build/commonjs/ViewPager/utils.js.map +1 -1
  40. package/build/commonjs/ViewabilityTrackerView/measureViewability.js +6 -6
  41. package/build/commonjs/ViewabilityTrackerView/measureViewability.js.map +1 -1
  42. package/build/commonjs/hooks/useUnstableCollapsibleAppBar.js +1 -1
  43. package/build/commonjs/hooks/useUnstableCollapsibleAppBar.js.map +1 -1
  44. package/build/module/Carousel/Carousel.js +27 -32
  45. package/build/module/Carousel/Carousel.js.map +1 -1
  46. package/build/module/Carousel/animation/createDefaultScrollAnimation.js +2 -2
  47. package/build/module/Carousel/animation/createDefaultScrollAnimation.js.map +1 -1
  48. package/build/module/Carousel/animation/parallaxItemStyleFactory.js +2 -2
  49. package/build/module/Carousel/animation/parallaxItemStyleFactory.js.map +1 -1
  50. package/build/module/Carousel/components/ItemView.js +1 -1
  51. package/build/module/Carousel/components/ItemView.js.map +1 -1
  52. package/build/module/Carousel/components/ScrollViewGesture.js +6 -6
  53. package/build/module/Carousel/components/ScrollViewGesture.js.map +1 -1
  54. package/build/module/Carousel/{hooks → components}/useItemInterpolation.js +3 -3
  55. package/build/module/Carousel/components/useItemInterpolation.js.map +1 -0
  56. package/build/module/Carousel/hooks/index.js +0 -2
  57. package/build/module/Carousel/hooks/index.js.map +1 -1
  58. package/build/module/Carousel/hooks/useIndexController.js +23 -39
  59. package/build/module/Carousel/hooks/useIndexController.js.map +1 -1
  60. package/build/module/Carousel/hooks/useItemVisibilityStore.js +10 -11
  61. package/build/module/Carousel/hooks/useItemVisibilityStore.js.map +1 -1
  62. package/build/module/Carousel/hooks/usePagingAnimation.js +73 -56
  63. package/build/module/Carousel/hooks/usePagingAnimation.js.map +1 -1
  64. package/build/module/Carousel/tick.js +6 -0
  65. package/build/module/Carousel/tick.js.map +1 -0
  66. package/build/module/Carousel/types.js +1 -0
  67. package/build/module/Carousel/types.js.map +1 -1
  68. package/build/module/ViewPager/ChildrenMemoizedPage.js +53 -47
  69. package/build/module/ViewPager/ChildrenMemoizedPage.js.map +1 -1
  70. package/build/module/ViewPager/InternalContext.js +7 -0
  71. package/build/module/ViewPager/InternalContext.js.map +1 -0
  72. package/build/module/ViewPager/ViewPagerNative.js +38 -17
  73. package/build/module/ViewPager/ViewPagerNative.js.map +1 -1
  74. package/build/module/ViewPager/ViewPagerProps.js.map +1 -1
  75. package/build/module/ViewPager/ViewPagerWeb.js +16 -8
  76. package/build/module/ViewPager/ViewPagerWeb.js.map +1 -1
  77. package/build/module/ViewPager/index.js.map +1 -1
  78. package/build/module/ViewPager/types.js +2 -0
  79. package/build/module/ViewPager/types.js.map +1 -0
  80. package/build/module/ViewPager/usePageStore.js +25 -0
  81. package/build/module/ViewPager/usePageStore.js.map +1 -0
  82. package/build/module/ViewPager/utils.js.map +1 -1
  83. package/build/module/ViewabilityTrackerView/measureViewability.js +2 -2
  84. package/build/module/ViewabilityTrackerView/measureViewability.js.map +1 -1
  85. package/build/module/hooks/useUnstableCollapsibleAppBar.js +1 -1
  86. package/build/module/hooks/useUnstableCollapsibleAppBar.js.map +1 -1
  87. package/build/typescript/Carousel/components/ScrollViewGesture.d.ts +2 -2
  88. package/build/typescript/Carousel/{hooks → components}/useItemInterpolation.d.ts +0 -0
  89. package/build/typescript/Carousel/hooks/index.d.ts +0 -2
  90. package/build/typescript/Carousel/hooks/useIndexController.d.ts +0 -2
  91. package/build/typescript/Carousel/hooks/useItemVisibilityStore.d.ts +5 -2
  92. package/build/typescript/Carousel/hooks/usePagingAnimation.d.ts +5 -7
  93. package/build/typescript/Carousel/tick.d.ts +2 -0
  94. package/build/typescript/Carousel/types.d.ts +4 -2
  95. package/build/typescript/ViewPager/ChildrenMemoizedPage.d.ts +1 -1
  96. package/build/typescript/ViewPager/InternalContext.d.ts +7 -0
  97. package/build/typescript/ViewPager/ViewPagerNative.d.ts +2 -2
  98. package/build/typescript/ViewPager/ViewPagerProps.d.ts +4 -22
  99. package/build/typescript/ViewPager/ViewPagerWeb.d.ts +2 -2
  100. package/build/typescript/ViewPager/index.d.ts +2 -1
  101. package/build/typescript/ViewPager/types.d.ts +19 -0
  102. package/build/typescript/ViewPager/usePageStore.d.ts +2 -0
  103. package/build/typescript/ViewPager/utils.d.ts +1 -1
  104. package/package.json +3 -3
  105. package/src/Carousel/Carousel.tsx +25 -34
  106. package/src/Carousel/animation/createDefaultScrollAnimation.ts +2 -2
  107. package/src/Carousel/animation/parallaxItemStyleFactory.ts +1 -1
  108. package/src/Carousel/components/ItemView.tsx +1 -1
  109. package/src/Carousel/components/ScrollViewGesture.tsx +8 -7
  110. package/src/Carousel/{hooks → components}/useItemInterpolation.ts +3 -3
  111. package/src/Carousel/hooks/index.ts +0 -2
  112. package/src/Carousel/hooks/useIndexController.tsx +25 -47
  113. package/src/Carousel/hooks/useItemVisibilityStore.ts +17 -13
  114. package/src/Carousel/hooks/usePagingAnimation.ts +104 -64
  115. package/src/Carousel/tick.ts +6 -0
  116. package/src/Carousel/types.ts +6 -2
  117. package/src/ViewPager/ChildrenMemoizedPage.tsx +53 -50
  118. package/src/ViewPager/InternalContext.ts +13 -0
  119. package/src/ViewPager/ViewPagerNative.tsx +53 -39
  120. package/src/ViewPager/ViewPagerProps.ts +4 -27
  121. package/src/ViewPager/ViewPagerWeb.tsx +23 -18
  122. package/src/ViewPager/index.ts +2 -1
  123. package/src/ViewPager/types.ts +24 -0
  124. package/src/ViewPager/usePageStore.ts +30 -0
  125. package/src/ViewPager/utils.tsx +1 -1
  126. package/src/ViewabilityTrackerView/measureViewability.ts +1 -3
  127. package/src/hooks/useUnstableCollapsibleAppBar.ts +1 -1
  128. package/build/commonjs/Carousel/hooks/useDimensionChangeReaction.js +0 -23
  129. package/build/commonjs/Carousel/hooks/useDimensionChangeReaction.js.map +0 -1
  130. package/build/commonjs/Carousel/hooks/useItemInterpolation.js.map +0 -1
  131. package/build/module/Carousel/hooks/useDimensionChangeReaction.js +0 -14
  132. package/build/module/Carousel/hooks/useDimensionChangeReaction.js.map +0 -1
  133. package/build/module/Carousel/hooks/useItemInterpolation.js.map +0 -1
  134. package/build/typescript/Carousel/hooks/useDimensionChangeReaction.d.ts +0 -7
  135. package/src/Carousel/hooks/useDimensionChangeReaction.ts +0 -25
@@ -1,11 +1,10 @@
1
- import React, { forwardRef, memo, useImperativeHandle, useMemo, useRef } from 'react';
1
+ import React, { forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
2
2
  import { Animated } from 'react-native';
3
3
  import ViewabilityTrackerView from '../ViewabilityTrackerView';
4
4
  import type CarouselProps from './CarouselProps';
5
5
  import type { CarouselInstance } from './types';
6
6
  import {
7
7
  useAutoplayController,
8
- useDimensionChangeReaction,
9
8
  useIndexController,
10
9
  useItemVisibilityStore,
11
10
  useLoopedData,
@@ -26,7 +25,7 @@ const Carousel = forwardRef<CarouselInstance, CarouselProps>(function Carousel(p
26
25
  itemHeight,
27
26
  itemWidth,
28
27
  loop = false,
29
- onIndexChange,
28
+ onIndexChange: onIndexChangeProp,
30
29
  renderItem,
31
30
  scrollEnabled = true,
32
31
  style,
@@ -36,41 +35,41 @@ const Carousel = forwardRef<CarouselInstance, CarouselProps>(function Carousel(p
36
35
  const data = useLoopedData(originalData, loop);
37
36
 
38
37
  const initialTx = itemWidth * initialIndex;
39
- const controlledTx = useRef(new Animated.Value(initialTx)).current;
40
- const offsetTx = useRef(new Animated.Value(0)).current;
38
+ const offsetX = useRef(new Animated.Value(initialTx)).current;
39
+ const translateX = useRef(new Animated.Value(0)).current;
40
+ const globalInterpolation = Animated.add(offsetX, translateX);
41
41
 
42
- const {
43
- currentIndex,
44
- getCurrentIndex,
45
- lastIndex,
46
- monitorElement,
47
- } = useIndexController({
48
- controlledTx,
42
+ const [itemVisibilityStore, onIndexChange] = useItemVisibilityStore({
43
+ initialIndex,
44
+ numberOfData: data.length,
45
+ windowSize,
46
+ });
47
+
48
+ const handleIndexChange = useCallback((newIndex: number) => {
49
+ onIndexChange(newIndex);
50
+ onIndexChangeProp?.(newIndex);
51
+ }, [onIndexChange, onIndexChangeProp]);
52
+
53
+ const indexController = useIndexController({
49
54
  initialIndex,
50
55
  itemWidth,
51
56
  numberOfOriginalData: originalData.length,
52
- onIndexChange,
57
+ onIndexChange: handleIndexChange,
53
58
  });
54
59
 
55
- const itemVisibilityStore = useItemVisibilityStore({
56
- currentIndex,
57
- numberOfData: data.length,
58
- windowSize,
59
- });
60
+ const { getCurrentIndex } = indexController;
60
61
 
61
62
  const {
62
- finalizeAnimation,
63
- globalInterpolation,
63
+ interruptAnimation,
64
64
  startPagingAnimation,
65
65
  } = usePagingAnimation({
66
- controlledTx,
67
66
  createScrollAnimation,
68
- getCurrentIndex,
69
67
  itemWidth,
70
- lastIndex,
68
+ indexController,
71
69
  loop,
72
70
  numberOfData: data.length,
73
- offsetTx,
71
+ offsetX,
72
+ translateX,
74
73
  });
75
74
 
76
75
  const autoplayController = useAutoplayController({
@@ -79,12 +78,6 @@ const Carousel = forwardRef<CarouselInstance, CarouselProps>(function Carousel(p
79
78
  startPagingAnimation,
80
79
  });
81
80
 
82
- useDimensionChangeReaction({
83
- controlledTx,
84
- currentIndex,
85
- itemWidth,
86
- });
87
-
88
81
  useImperativeHandle(
89
82
  ref,
90
83
  () => ({
@@ -116,8 +109,6 @@ const Carousel = forwardRef<CarouselInstance, CarouselProps>(function Carousel(p
116
109
 
117
110
  return (
118
111
  <InternalContext.Provider value={contextValue}>
119
- {monitorElement}
120
-
121
112
  <ViewabilityTrackerView
122
113
  enabled={autoplay && !disableSmartAutoplay}
123
114
  measurementIntervalMillis={Math.max(3000, autoplayInterval)}
@@ -131,8 +122,8 @@ const Carousel = forwardRef<CarouselInstance, CarouselProps>(function Carousel(p
131
122
  >
132
123
  <ScrollViewGesture
133
124
  autoplayController={autoplayController}
134
- finalizeAnimation={finalizeAnimation}
135
- offsetTx={offsetTx}
125
+ interruptAnimation={interruptAnimation}
126
+ translateX={translateX}
136
127
  scrollEnabled={scrollEnabled}
137
128
  startPagingAnimation={startPagingAnimation}
138
129
  >
@@ -6,8 +6,8 @@ export default function createDefaultScrollAnimation(
6
6
  ): Animated.CompositeAnimation {
7
7
  return Animated.timing(animatedValue, {
8
8
  toValue: toValue,
9
- duration: 350,
10
- easing: Easing.bezier(0.25, 1, 0.5, 1),
9
+ duration: 180,
10
+ easing: Easing.bezier(0.2, 0.2, 0.2, 1),
11
11
  useNativeDriver: true,
12
12
  });
13
13
  };
@@ -25,8 +25,8 @@ export default function parallaxItemStyleFactory(config: ParallaxAnimationConfig
25
25
  adjacentItemScale,
26
26
  scrollingOffset,
27
27
  }: Required<ParallaxAnimationConfig> = {
28
- ...config,
29
28
  ...defaultParallaxAnimationConfig,
29
+ ...config,
30
30
  };
31
31
 
32
32
  const createItemStyle: CreateItemStyle = (itemInterpolation, itemWidth) => {
@@ -3,7 +3,7 @@ import React, { useContext, useEffect, useMemo, useState } from 'react';
3
3
  import type { ViewProps } from 'react-native';
4
4
  import { Animated } from 'react-native';
5
5
  import { StyleSheet } from '@fountain-ui/core';
6
- import { useItemInterpolation } from '../hooks';
6
+ import useItemInterpolation from './useItemInterpolation';
7
7
  import InternalContext from './InternalContext';
8
8
 
9
9
  export interface ItemViewProps {
@@ -8,8 +8,8 @@ import type { AutoplayController, PagingDirection, StartPagingAnimation } from '
8
8
  export interface ScrollViewGestureProps {
9
9
  autoplayController: AutoplayController;
10
10
  children: ReactNode;
11
- finalizeAnimation: () => void;
12
- offsetTx: Animated.Value,
11
+ interruptAnimation: () => void;
12
+ translateX: Animated.Value,
13
13
  scrollEnabled: boolean;
14
14
  startPagingAnimation: StartPagingAnimation;
15
15
  }
@@ -23,6 +23,7 @@ const activeOffsetX: number[] = [-ACTIVE_OFFSET_ABS_X, ACTIVE_OFFSET_ABS_X];
23
23
  const endAnimationStates: Readonly<GestureHandlerState[]> = [
24
24
  GestureHandlerState.CANCELLED,
25
25
  GestureHandlerState.END,
26
+ GestureHandlerState.FAILED,
26
27
  ];
27
28
 
28
29
  function shouldScrollToAdjacent(translationX: number, velocityX: number): boolean {
@@ -38,8 +39,8 @@ export default function ScrollViewGesture(props: ScrollViewGestureProps) {
38
39
  const {
39
40
  autoplayController,
40
41
  children,
41
- finalizeAnimation,
42
- offsetTx,
42
+ interruptAnimation,
43
+ translateX,
43
44
  scrollEnabled,
44
45
  startPagingAnimation,
45
46
  } = props;
@@ -49,11 +50,11 @@ export default function ScrollViewGesture(props: ScrollViewGestureProps) {
49
50
  const handleGestureBegin = useCallback(() => {
50
51
  pauseAutoplay();
51
52
 
52
- finalizeAnimation();
53
- }, [finalizeAnimation, pauseAutoplay]);
53
+ interruptAnimation();
54
+ }, [interruptAnimation, pauseAutoplay]);
54
55
 
55
56
  const handleGestureEvent = useCallback(Animated.event(
56
- [{ nativeEvent: { translationX: offsetTx } }],
57
+ [{ nativeEvent: { translationX: translateX } }],
57
58
  { useNativeDriver: true },
58
59
  ), []);
59
60
 
@@ -1,6 +1,6 @@
1
1
  import { useContext, useMemo } from 'react';
2
2
  import { Animated } from 'react-native';
3
- import { InternalContext } from '../components';
3
+ import InternalContext from './InternalContext';
4
4
 
5
5
  const OVERSCROLL_FRICTION_FACTOR = 4;
6
6
 
@@ -94,9 +94,9 @@ export default function useItemInterpolation(index: number): Animated.AnimatedIn
94
94
  ? interpolationConfigOnLoop
95
95
  : interpolationConfigOnNoLoop;
96
96
 
97
- const offsetX = globalInterpolation.interpolate(interpolationConfig);
97
+ const localOffsetX = globalInterpolation.interpolate(interpolationConfig);
98
98
 
99
- return Animated.divide(offsetX, itemWidth);
99
+ return Animated.divide(localOffsetX, itemWidth);
100
100
  }, [
101
101
  globalInterpolation,
102
102
  interpolationConfigOnLoop,
@@ -1,7 +1,5 @@
1
1
  export { default as useAutoplayController } from './useAutoplayController';
2
- export { default as useDimensionChangeReaction } from './useDimensionChangeReaction';
3
2
  export { default as useIndexController } from './useIndexController';
4
- export { default as useItemInterpolation } from './useItemInterpolation';
5
3
  export { default as useLoopedData } from './useLoopedData';
6
4
  export { default as usePagingAnimation } from './usePagingAnimation';
7
5
  export { default as useItemVisibilityStore } from './useItemVisibilityStore';
@@ -1,76 +1,54 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { Animated } from 'react-native';
3
- import type { IndexController } from '../types';
1
+ import React, { useCallback, useRef } from 'react';
2
+ import { mod } from '@fountain-ui/utils';
3
+ import type { AnimationState, IndexController } from '../types';
4
4
 
5
5
  export interface UseIndexControllerParameters {
6
- controlledTx: Animated.AnimatedValue;
7
6
  initialIndex: number;
8
7
  itemWidth: number;
9
8
  numberOfOriginalData: number;
10
9
  onIndexChange?: (newIndex: number) => void;
11
10
  }
12
11
 
13
- const normalizeIndex = (maybeIndex: number, numberOfData: number): number =>
14
- Math.abs(Math.floor(maybeIndex)) % numberOfData;
15
-
16
12
  export default function useIndexController(params: UseIndexControllerParameters): IndexController {
17
13
  const {
18
- controlledTx,
19
14
  initialIndex,
20
15
  itemWidth,
21
16
  numberOfOriginalData,
22
17
  onIndexChange,
23
18
  } = params;
24
19
 
25
- const indexRef = useRef<number>(initialIndex);
26
- const [index, setIndex] = useState<number>(indexRef.current);
27
-
28
- const maybeIndex = useMemo(() => {
29
- const negative = new Animated.Value(-1);
30
- const reversedTx = Animated.multiply(controlledTx, negative);
31
- const normalized = Animated.divide(reversedTx, itemWidth);
32
- return Animated.modulo(normalized, numberOfOriginalData);
33
- }, [
34
- controlledTx,
35
- itemWidth,
36
- numberOfOriginalData,
37
- ]);
38
-
39
- useEffect(() => {
40
- const subscriptionId = maybeIndex.addListener((observedValue) => {
41
- const newIndex = normalizeIndex(observedValue.value, numberOfOriginalData);
20
+ const currentIndexRef = useRef<number>(initialIndex);
21
+ const indexChangedRef = useRef<boolean>(false);
42
22
 
43
- if (indexRef.current !== newIndex) {
44
- indexRef.current = newIndex;
45
- setIndex(newIndex);
23
+ const getCurrentIndex = useCallback(() => currentIndexRef.current, []);
46
24
 
47
- onIndexChange?.(newIndex);
25
+ const notifyAnimationState = useCallback((state: AnimationState) => {
26
+ if (state === 'finished' || state === 'interrupted') {
27
+ if (indexChangedRef.current) {
28
+ onIndexChange?.(getCurrentIndex());
48
29
  }
49
- });
50
-
51
- return () => {
52
- maybeIndex.removeListener(subscriptionId);
53
- };
30
+ }
54
31
  }, [
55
- maybeIndex,
56
- numberOfOriginalData,
32
+ getCurrentIndex,
57
33
  onIndexChange,
58
34
  ]);
59
35
 
60
- const getCurrentIndex = useCallback(() => indexRef.current, []);
36
+ const notifyOffsetHasChanged = useCallback((offset: number) => {
37
+ const roundedOffset = Math.round(offset / itemWidth) * itemWidth;
38
+
39
+ // To prevent floating point problem, make sure index is integer type.
40
+ const nextIndex = Math.floor(mod((-roundedOffset / itemWidth), numberOfOriginalData));
41
+
42
+ if (nextIndex !== currentIndexRef.current) {
43
+ currentIndexRef.current = nextIndex;
44
+ indexChangedRef.current = true;
45
+ }
46
+ }, [itemWidth, numberOfOriginalData]);
61
47
 
62
48
  return {
63
- currentIndex: index,
64
49
  getCurrentIndex,
65
50
  lastIndex: numberOfOriginalData - 1,
66
- monitorElement: (
67
- <Animated.View
68
- collapsable={false}
69
- style={[
70
- { zIndex: maybeIndex },
71
- { width: 1, height: 1, position: 'absolute' },
72
- ]}
73
- />
74
- ),
51
+ notifyAnimationState,
52
+ notifyOffsetHasChanged,
75
53
  };
76
54
  };
@@ -1,12 +1,17 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { mod } from '@fountain-ui/utils';
2
3
  import type { ItemVisibilityStore, VisibleIndexRanges } from '../types';
3
4
 
4
5
  export interface Parameters {
5
- currentIndex: number;
6
+ initialIndex: number;
6
7
  numberOfData: number;
7
8
  windowSize: number;
8
9
  }
9
10
 
11
+ export interface OnIndexChange {
12
+ (newIndex: number): void;
13
+ }
14
+
10
15
  class SimpleItemVisibilityStore implements ItemVisibilityStore {
11
16
 
12
17
  private store: VisibleIndexRanges;
@@ -52,10 +57,6 @@ function normalize(windowSize: number, numberOfData: number): number {
52
57
  return windowSize;
53
58
  }
54
59
 
55
- function mod(value: number, modulo: number): number {
56
- return ((value % modulo) + modulo) % modulo;
57
- }
58
-
59
60
  function makeVisibleIndexRanges(numberOfData: number, windowSize: number, index: number): VisibleIndexRanges {
60
61
  const ws = normalize(windowSize, numberOfData);
61
62
 
@@ -81,21 +82,24 @@ function makeVisibleIndexRanges(numberOfData: number, windowSize: number, index:
81
82
  ];
82
83
  }
83
84
 
84
- export default function useItemVisibilityStore(params: Parameters): ItemVisibilityStore {
85
+ export default function useItemVisibilityStore(params: Parameters): [ItemVisibilityStore, OnIndexChange] {
85
86
  const {
86
- currentIndex,
87
+ initialIndex,
87
88
  numberOfData,
88
89
  windowSize,
89
90
  } = params;
90
91
 
91
- const [initialRange] = useState(() => makeVisibleIndexRanges(numberOfData, windowSize, currentIndex));
92
+ const [initialRange] = useState(() => {
93
+ return makeVisibleIndexRanges(numberOfData, windowSize, initialIndex);
94
+ });
95
+
92
96
  const store = useRef(new SimpleItemVisibilityStore(initialRange)).current;
93
97
 
94
- useEffect(() => {
95
- const newRanges = makeVisibleIndexRanges(numberOfData, windowSize, currentIndex);
98
+ const onIndexChange: OnIndexChange = useCallback((newIndex) => {
99
+ const newRanges = makeVisibleIndexRanges(numberOfData, windowSize, newIndex);
96
100
 
97
101
  store.dispatch(newRanges);
98
- }, [currentIndex, numberOfData, windowSize]);
102
+ }, [numberOfData, windowSize]);
99
103
 
100
104
  useEffect(() => {
101
105
  return () => {
@@ -103,5 +107,5 @@ export default function useItemVisibilityStore(params: Parameters): ItemVisibili
103
107
  };
104
108
  }, []);
105
109
 
106
- return store;
110
+ return [store, onIndexChange];
107
111
  };
@@ -1,9 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useRef } from 'react';
2
- import { Animated, Platform } from 'react-native';
1
+ import { useCallback, useRef } from 'react';
2
+ import { Animated } from 'react-native';
3
3
  import type {
4
4
  CreateScrollAnimation,
5
5
  DirectionalPagingAnimationConfig,
6
- GetCurrentIndex,
6
+ IndexController,
7
7
  IndexPagingAnimationConfig,
8
8
  PagingAnimationConfig,
9
9
  PagingAnimationType,
@@ -12,19 +12,17 @@ import type {
12
12
  } from '../types';
13
13
 
14
14
  export interface PagingAnimationParameters {
15
- controlledTx: Animated.Value;
16
15
  createScrollAnimation: CreateScrollAnimation;
17
- getCurrentIndex: GetCurrentIndex;
18
16
  itemWidth: number;
19
- lastIndex: number;
17
+ indexController: IndexController;
20
18
  loop: boolean;
21
19
  numberOfData: number;
22
- offsetTx: Animated.Value;
20
+ offsetX: Animated.Value;
21
+ translateX: Animated.Value;
23
22
  }
24
23
 
25
24
  export interface UsePagingAnimation {
26
- finalizeAnimation: () => void;
27
- globalInterpolation: Animated.AnimatedInterpolation;
25
+ interruptAnimation: () => void;
28
26
  startPagingAnimation: StartPagingAnimation;
29
27
  }
30
28
 
@@ -41,77 +39,110 @@ function directionToValue(itemWidth: number) {
41
39
  };
42
40
  }
43
41
 
42
+ function toValueCompensator(itemWidth: number) {
43
+ return function (toValue: number, currentOffset: number): number {
44
+ const remainder = Math.abs(currentOffset % itemWidth);
45
+
46
+ const halfOfItemWidth = Math.abs(itemWidth / 2);
47
+ const compensateVector = remainder > halfOfItemWidth
48
+ ? remainder - itemWidth
49
+ : remainder;
50
+
51
+ const direction = currentOffset > 0 ? -1 : 1;
52
+
53
+ return toValue + (direction * compensateVector);
54
+ };
55
+ }
56
+
44
57
  export default function usePagingAnimation(params: PagingAnimationParameters): UsePagingAnimation {
45
58
  const {
46
- controlledTx,
47
59
  createScrollAnimation,
48
- getCurrentIndex,
49
60
  itemWidth,
50
- lastIndex,
61
+ indexController,
51
62
  loop,
52
63
  numberOfData,
53
- offsetTx,
64
+ offsetX,
65
+ translateX,
54
66
  } = params;
55
67
 
56
- const animationRef = useRef<Animated.CompositeAnimation | null>(null);
57
- const toValueRef = useRef<number>(0);
68
+ const {
69
+ getCurrentIndex,
70
+ lastIndex,
71
+ notifyAnimationState,
72
+ notifyOffsetHasChanged,
73
+ } = indexController;
58
74
 
59
- const globalInterpolation = useMemo(
60
- () => Animated.add(controlledTx, offsetTx),
61
- [controlledTx, offsetTx],
62
- );
75
+ const toValueRef = useRef<number>(0);
76
+ const currentOffsetRef = useRef<number>(0);
63
77
 
64
- useEffect(() => {
65
- const subscriptionId = controlledTx.addListener((value) => {
66
- const currentTx = value.value;
78
+ const isAnimatingRef = useRef<boolean>(false);
67
79
 
68
- // Prevent infinite loop
69
- if (currentTx !== 0) {
70
- const maxWidth = numberOfData * itemWidth;
80
+ const maxWidth = Math.abs(numberOfData * itemWidth);
71
81
 
72
- if (Math.abs(Math.round(currentTx)) === Math.round(maxWidth)) {
73
- // reset position
74
- controlledTx.setValue(0);
75
- }
82
+ const ensureOffsetBoundary: (offset: number) => number = useCallback((offset: number) => {
83
+ if (loop) {
84
+ const isCloseToEnd = Math.abs(offset) >= (maxWidth - itemWidth);
85
+ if (isCloseToEnd) {
86
+ const signOfOffset = offset > 0 ? 1 : -1;
87
+ return offset + (-signOfOffset * maxWidth);
76
88
  }
77
- });
89
+ }
78
90
 
79
- return () => {
80
- controlledTx.removeListener(subscriptionId);
81
- };
82
- }, [numberOfData, itemWidth]);
91
+ return offset % maxWidth;
92
+ }, [itemWidth, loop, maxWidth]);
83
93
 
84
- const finalizeAnimation = useCallback(() => {
85
- const stopUnfinishedSnapAnimation = () => {
86
- if (animationRef.current) {
87
- animationRef.current?.stop();
88
- animationRef.current = null;
89
- }
90
- };
94
+ const requireNewOffset = useCallback((newOffset: number) => {
95
+ const nextOffset = ensureOffsetBoundary(newOffset);
91
96
 
92
- const resetBoundary = () => {
93
- controlledTx.setOffset(toValueRef.current);
94
- controlledTx.flattenOffset();
95
-
96
- // FIXME: react-native-web bug maybe?
97
- // `AnimatedValue.flattenOffset()` does not trigger any event listener.
98
- // Accessing value directly via `_value` is dangerous but working on web (`useNativeDriver` always false).
99
- // So setting same value with `value.setValue(value._value)` will trigger event listener.
100
- if (Platform.OS === 'web') {
101
- // @ts-ignore
102
- controlledTx.setValue(controlledTx._value);
103
- }
97
+ currentOffsetRef.current = nextOffset;
98
+ offsetX.setValue(nextOffset);
104
99
 
105
- offsetTx.setValue(0);
106
- toValueRef.current = 0;
107
- };
100
+ toValueRef.current = 0;
101
+ translateX.setValue(0);
102
+ }, [
103
+ ensureOffsetBoundary,
104
+ offsetX,
105
+ translateX,
106
+ ]);
107
+
108
+ const interruptAnimation = useCallback(() => {
109
+ if (!isAnimatingRef.current) {
110
+ return;
111
+ }
112
+
113
+ translateX.stopAnimation(lastValue => {
114
+ isAnimatingRef.current = false;
115
+
116
+ const prevOffset = currentOffsetRef.current;
117
+ const totalOffset = prevOffset + lastValue;
118
+
119
+ notifyOffsetHasChanged(totalOffset);
120
+ notifyAnimationState('interrupted');
121
+
122
+ requireNewOffset(totalOffset);
123
+ });
124
+ }, [requireNewOffset, translateX]);
125
+
126
+ const finalizeAnimation = useCallback(() => {
127
+ notifyAnimationState('finished');
108
128
 
109
- stopUnfinishedSnapAnimation();
129
+ isAnimatingRef.current = false;
110
130
 
111
- resetBoundary();
112
- }, [controlledTx]);
131
+ const prevOffset = currentOffsetRef.current;
132
+ const toValue = toValueRef.current;
133
+ const totalOffset = prevOffset + toValue;
134
+
135
+ requireNewOffset(totalOffset);
136
+ }, [
137
+ notifyAnimationState,
138
+ requireNewOffset,
139
+ ]);
113
140
 
114
141
  const startPagingAnimation = useCallback((type: PagingAnimationType, config: PagingAnimationConfig) => {
142
+ if (isAnimatingRef.current) {
143
+ return;
144
+ }
145
+
115
146
  const configWithDefaults: PagingAnimationConfig = {
116
147
  animated: true,
117
148
  ...config,
@@ -120,6 +151,7 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
120
151
  const currentIndex = getCurrentIndex();
121
152
 
122
153
  const getValueByDirectionOnAllAdjacentItemsVisible = directionToValue(itemWidth);
154
+ const compensateToValue = toValueCompensator(itemWidth);
123
155
 
124
156
  const getValueByDirectionalPagingOnLoopDisabled = (_config: DirectionalPagingAnimationConfig): number => {
125
157
  const { direction, isOriginatedFromGesture } = _config;
@@ -159,22 +191,29 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
159
191
  return distance * direction;
160
192
  };
161
193
 
162
- const toValue = type === 'directional'
194
+ const wantedToValue = type === 'directional'
163
195
  // @ts-ignore
164
196
  ? getValueByDirectionalPaging(configWithDefaults)
165
197
  // @ts-ignore
166
198
  : getValueByIndexPaging(configWithDefaults);
167
199
 
200
+ const toValue = compensateToValue(wantedToValue, currentOffsetRef.current);
201
+
168
202
  toValueRef.current = toValue;
203
+ isAnimatingRef.current = true;
204
+
205
+ notifyOffsetHasChanged(currentOffsetRef.current + toValue);
169
206
 
170
207
  if (configWithDefaults.animated) {
171
- const animation = createScrollAnimation(offsetTx, toValue);
172
- animationRef.current = animation;
208
+ const animation = createScrollAnimation(translateX, toValue);
209
+
173
210
  animation.start(({ finished }) => {
174
211
  if (finished) {
175
212
  finalizeAnimation();
176
213
  }
177
214
  });
215
+
216
+ notifyAnimationState('started');
178
217
  } else {
179
218
  finalizeAnimation();
180
219
  }
@@ -185,11 +224,12 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
185
224
  itemWidth,
186
225
  lastIndex,
187
226
  loop,
227
+ notifyAnimationState,
228
+ notifyOffsetHasChanged,
188
229
  ]);
189
230
 
190
231
  return {
191
- globalInterpolation,
192
- finalizeAnimation,
232
+ interruptAnimation,
193
233
  startPagingAnimation,
194
234
  };
195
235
  };
@@ -0,0 +1,6 @@
1
+ import { logger } from '@fountain-ui/utils';
2
+
3
+ export default logger('Carousel', {
4
+ enabled: __DEV__,
5
+ format: 'diff',
6
+ });
@@ -5,6 +5,10 @@ const directions = ['next', 'prev', 'stay'] as const;
5
5
 
6
6
  export type PagingDirection = (typeof directions)[number];
7
7
 
8
+ const animationStates = ['started', 'finished', 'interrupted'] as const;
9
+
10
+ export type AnimationState = (typeof animationStates)[number];
11
+
8
12
  export type ItemHeight = number | 'auto';
9
13
 
10
14
  export interface RenderItem<T> {
@@ -24,10 +28,10 @@ export interface GetCurrentIndex {
24
28
  }
25
29
 
26
30
  export interface IndexController {
27
- currentIndex: number;
28
31
  getCurrentIndex: GetCurrentIndex;
29
32
  lastIndex: number;
30
- monitorElement: ReactElement;
33
+ notifyAnimationState: (state: AnimationState) => void;
34
+ notifyOffsetHasChanged: (offset: number) => void;
31
35
  }
32
36
 
33
37
  export type PagingAnimationType = 'directional' | 'index';