@fountain-ui/lab 2.0.0-beta.14 → 2.0.0-beta.15

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 (95) 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 +15 -46
  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 +70 -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.map +1 -1
  24. package/build/commonjs/ViewPager/ChildrenMemoizedPage.js +44 -35
  25. package/build/commonjs/ViewPager/ChildrenMemoizedPage.js.map +1 -1
  26. package/build/commonjs/ViewPager/ViewPagerNative.js +2 -3
  27. package/build/commonjs/ViewPager/ViewPagerNative.js.map +1 -1
  28. package/build/commonjs/ViewPager/usePageStore.js +5 -0
  29. package/build/commonjs/ViewPager/usePageStore.js.map +1 -1
  30. package/build/commonjs/ViewabilityTrackerView/measureViewability.js +6 -6
  31. package/build/commonjs/ViewabilityTrackerView/measureViewability.js.map +1 -1
  32. package/build/module/Carousel/Carousel.js +27 -32
  33. package/build/module/Carousel/Carousel.js.map +1 -1
  34. package/build/module/Carousel/animation/createDefaultScrollAnimation.js +2 -2
  35. package/build/module/Carousel/animation/createDefaultScrollAnimation.js.map +1 -1
  36. package/build/module/Carousel/animation/parallaxItemStyleFactory.js +2 -2
  37. package/build/module/Carousel/animation/parallaxItemStyleFactory.js.map +1 -1
  38. package/build/module/Carousel/components/ItemView.js +1 -1
  39. package/build/module/Carousel/components/ItemView.js.map +1 -1
  40. package/build/module/Carousel/components/ScrollViewGesture.js +6 -6
  41. package/build/module/Carousel/components/ScrollViewGesture.js.map +1 -1
  42. package/build/module/Carousel/{hooks → components}/useItemInterpolation.js +3 -3
  43. package/build/module/Carousel/components/useItemInterpolation.js.map +1 -0
  44. package/build/module/Carousel/hooks/index.js +0 -2
  45. package/build/module/Carousel/hooks/index.js.map +1 -1
  46. package/build/module/Carousel/hooks/useIndexController.js +14 -39
  47. package/build/module/Carousel/hooks/useIndexController.js.map +1 -1
  48. package/build/module/Carousel/hooks/useItemVisibilityStore.js +10 -11
  49. package/build/module/Carousel/hooks/useItemVisibilityStore.js.map +1 -1
  50. package/build/module/Carousel/hooks/usePagingAnimation.js +71 -56
  51. package/build/module/Carousel/hooks/usePagingAnimation.js.map +1 -1
  52. package/build/module/Carousel/tick.js +6 -0
  53. package/build/module/Carousel/tick.js.map +1 -0
  54. package/build/module/Carousel/types.js.map +1 -1
  55. package/build/module/ViewPager/ChildrenMemoizedPage.js +44 -35
  56. package/build/module/ViewPager/ChildrenMemoizedPage.js.map +1 -1
  57. package/build/module/ViewPager/ViewPagerNative.js +2 -3
  58. package/build/module/ViewPager/ViewPagerNative.js.map +1 -1
  59. package/build/module/ViewPager/usePageStore.js +5 -0
  60. package/build/module/ViewPager/usePageStore.js.map +1 -1
  61. package/build/module/ViewabilityTrackerView/measureViewability.js +2 -2
  62. package/build/module/ViewabilityTrackerView/measureViewability.js.map +1 -1
  63. package/build/typescript/Carousel/components/ScrollViewGesture.d.ts +2 -2
  64. package/build/typescript/Carousel/{hooks → components}/useItemInterpolation.d.ts +0 -0
  65. package/build/typescript/Carousel/hooks/index.d.ts +0 -2
  66. package/build/typescript/Carousel/hooks/useIndexController.d.ts +0 -2
  67. package/build/typescript/Carousel/hooks/useItemVisibilityStore.d.ts +5 -2
  68. package/build/typescript/Carousel/hooks/usePagingAnimation.d.ts +5 -7
  69. package/build/typescript/Carousel/tick.d.ts +2 -0
  70. package/build/typescript/Carousel/types.d.ts +1 -2
  71. package/package.json +3 -3
  72. package/src/Carousel/Carousel.tsx +25 -34
  73. package/src/Carousel/animation/createDefaultScrollAnimation.ts +2 -2
  74. package/src/Carousel/animation/parallaxItemStyleFactory.ts +1 -1
  75. package/src/Carousel/components/ItemView.tsx +1 -1
  76. package/src/Carousel/components/ScrollViewGesture.tsx +8 -7
  77. package/src/Carousel/{hooks → components}/useItemInterpolation.ts +3 -3
  78. package/src/Carousel/hooks/index.ts +0 -2
  79. package/src/Carousel/hooks/useIndexController.tsx +14 -44
  80. package/src/Carousel/hooks/useItemVisibilityStore.ts +17 -13
  81. package/src/Carousel/hooks/usePagingAnimation.ts +95 -64
  82. package/src/Carousel/tick.ts +6 -0
  83. package/src/Carousel/types.ts +1 -2
  84. package/src/ViewPager/ChildrenMemoizedPage.tsx +46 -39
  85. package/src/ViewPager/ViewPagerNative.tsx +3 -3
  86. package/src/ViewPager/usePageStore.ts +6 -0
  87. package/src/ViewabilityTrackerView/measureViewability.ts +1 -3
  88. package/build/commonjs/Carousel/hooks/useDimensionChangeReaction.js +0 -23
  89. package/build/commonjs/Carousel/hooks/useDimensionChangeReaction.js.map +0 -1
  90. package/build/commonjs/Carousel/hooks/useItemInterpolation.js.map +0 -1
  91. package/build/module/Carousel/hooks/useDimensionChangeReaction.js +0 -14
  92. package/build/module/Carousel/hooks/useDimensionChangeReaction.js.map +0 -1
  93. package/build/module/Carousel/hooks/useItemInterpolation.js.map +0 -1
  94. package/build/typescript/Carousel/hooks/useDimensionChangeReaction.d.ts +0 -7
  95. package/src/Carousel/hooks/useDimensionChangeReaction.ts +0 -25
@@ -0,0 +1,2 @@
1
+ declare const _default: (key: string, message?: string | undefined) => void;
2
+ export default _default;
@@ -20,10 +20,9 @@ export interface GetCurrentIndex {
20
20
  (): number;
21
21
  }
22
22
  export interface IndexController {
23
- currentIndex: number;
24
23
  getCurrentIndex: GetCurrentIndex;
25
24
  lastIndex: number;
26
- monitorElement: ReactElement;
25
+ notifyOffsetHasChanged: (offset: number) => void;
27
26
  }
28
27
  export declare type PagingAnimationType = 'directional' | 'index';
29
28
  export interface BasePagingAnimationConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fountain-ui/lab",
3
- "version": "2.0.0-beta.14",
3
+ "version": "2.0.0-beta.15",
4
4
  "private": false,
5
5
  "author": "Fountain-UI Team",
6
6
  "description": "Incubator for Fountain-UI React components.",
@@ -18,7 +18,7 @@
18
18
  "@emotion/react": "^11.10.0",
19
19
  "@emotion/styled": "^11.10.0",
20
20
  "@fountain-ui/icons": "^2.0.0-beta.6",
21
- "@fountain-ui/utils": "^2.0.0-beta.3",
21
+ "@fountain-ui/utils": "^2.0.0-beta.4",
22
22
  "react-native-calendars": "1.1267.0"
23
23
  },
24
24
  "peerDependencies": {
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "bd0ccbf7db51f1418dffb2da36fe167654a58f7e"
73
+ "gitHead": "9c4af6d69493666d86b7736892f778af775a3c80"
74
74
  }
@@ -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: 220,
10
+ easing: Easing.bezier(0.35, 0.5, 0.5, 0.75),
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,46 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { Animated } from 'react-native';
1
+ import React, { useCallback, useRef } from 'react';
2
+ import { mod } from '@fountain-ui/utils';
3
3
  import type { 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);
20
+ const currentIndexRef = useRef<number>(initialIndex);
27
21
 
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
- ]);
22
+ const getCurrentIndex = useCallback(() => currentIndexRef.current, []);
38
23
 
39
- useEffect(() => {
40
- const subscriptionId = maybeIndex.addListener((observedValue) => {
41
- const newIndex = normalizeIndex(observedValue.value, numberOfOriginalData);
24
+ const notifyOffsetHasChanged = useCallback((offset: number) => {
25
+ const roundedOffset = Math.round(offset / itemWidth) * itemWidth;
42
26
 
43
- if (indexRef.current !== newIndex) {
44
- indexRef.current = newIndex;
45
- setIndex(newIndex);
27
+ // To prevent floating point problem, make sure index is integer type.
28
+ const nextIndex = Math.floor(mod((-roundedOffset / itemWidth), numberOfOriginalData));
46
29
 
47
- onIndexChange?.(newIndex);
48
- }
49
- });
30
+ if (nextIndex !== currentIndexRef.current) {
31
+ currentIndexRef.current = nextIndex;
50
32
 
51
- return () => {
52
- maybeIndex.removeListener(subscriptionId);
53
- };
33
+ onIndexChange?.(nextIndex);
34
+ }
54
35
  }, [
55
- maybeIndex,
36
+ itemWidth,
56
37
  numberOfOriginalData,
57
38
  onIndexChange,
58
39
  ]);
59
40
 
60
- const getCurrentIndex = useCallback(() => indexRef.current, []);
61
-
62
41
  return {
63
- currentIndex: index,
64
42
  getCurrentIndex,
65
43
  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
- ),
44
+ notifyOffsetHasChanged,
75
45
  };
76
46
  };
@@ -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,104 @@ 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
+ notifyOffsetHasChanged,
72
+ } = indexController;
58
73
 
59
- const globalInterpolation = useMemo(
60
- () => Animated.add(controlledTx, offsetTx),
61
- [controlledTx, offsetTx],
62
- );
74
+ const toValueRef = useRef<number>(0);
75
+ const currentOffsetRef = useRef<number>(0);
63
76
 
64
- useEffect(() => {
65
- const subscriptionId = controlledTx.addListener((value) => {
66
- const currentTx = value.value;
77
+ const isAnimatingRef = useRef<boolean>(false);
67
78
 
68
- // Prevent infinite loop
69
- if (currentTx !== 0) {
70
- const maxWidth = numberOfData * itemWidth;
79
+ const maxWidth = Math.abs(numberOfData * itemWidth);
71
80
 
72
- if (Math.abs(Math.round(currentTx)) === Math.round(maxWidth)) {
73
- // reset position
74
- controlledTx.setValue(0);
75
- }
81
+ const ensureOffsetBoundary: (offset: number) => number = useCallback((offset: number) => {
82
+ if (loop) {
83
+ const isCloseToEnd = Math.abs(offset) >= (maxWidth - itemWidth);
84
+ if (isCloseToEnd) {
85
+ const signOfOffset = offset > 0 ? 1 : -1;
86
+ return offset + (-signOfOffset * maxWidth);
76
87
  }
77
- });
88
+ }
78
89
 
79
- return () => {
80
- controlledTx.removeListener(subscriptionId);
81
- };
82
- }, [numberOfData, itemWidth]);
90
+ return offset % maxWidth;
91
+ }, [itemWidth, loop, maxWidth]);
83
92
 
84
- const finalizeAnimation = useCallback(() => {
85
- const stopUnfinishedSnapAnimation = () => {
86
- if (animationRef.current) {
87
- animationRef.current?.stop();
88
- animationRef.current = null;
89
- }
90
- };
93
+ const requireNewOffset = useCallback((newOffset: number) => {
94
+ const nextOffset = ensureOffsetBoundary(newOffset);
91
95
 
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
- }
96
+ currentOffsetRef.current = nextOffset;
97
+ offsetX.setValue(nextOffset);
104
98
 
105
- offsetTx.setValue(0);
106
- toValueRef.current = 0;
107
- };
99
+ toValueRef.current = 0;
100
+ translateX.setValue(0);
101
+ }, [
102
+ ensureOffsetBoundary,
103
+ offsetX,
104
+ translateX,
105
+ ]);
106
+
107
+ const interruptAnimation = useCallback(() => {
108
+ if (toValueRef.current === 0) {
109
+ // Performance optimization
110
+ return;
111
+ }
112
+
113
+ translateX.stopAnimation(lastValue => {
114
+ isAnimatingRef.current = false;
108
115
 
109
- stopUnfinishedSnapAnimation();
116
+ const prevOffset = currentOffsetRef.current;
117
+ const totalOffset = prevOffset + lastValue;
110
118
 
111
- resetBoundary();
112
- }, [controlledTx]);
119
+ notifyOffsetHasChanged(totalOffset);
120
+
121
+ requireNewOffset(totalOffset);
122
+ });
123
+ }, [requireNewOffset, translateX]);
124
+
125
+ const finalizeAnimation = useCallback(() => {
126
+ isAnimatingRef.current = false;
127
+
128
+ const prevOffset = currentOffsetRef.current;
129
+ const toValue = toValueRef.current;
130
+ const totalOffset = prevOffset + toValue;
131
+
132
+ requireNewOffset(totalOffset);
133
+ }, [requireNewOffset]);
113
134
 
114
135
  const startPagingAnimation = useCallback((type: PagingAnimationType, config: PagingAnimationConfig) => {
136
+ if (isAnimatingRef.current) {
137
+ return;
138
+ }
139
+
115
140
  const configWithDefaults: PagingAnimationConfig = {
116
141
  animated: true,
117
142
  ...config,
@@ -120,6 +145,7 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
120
145
  const currentIndex = getCurrentIndex();
121
146
 
122
147
  const getValueByDirectionOnAllAdjacentItemsVisible = directionToValue(itemWidth);
148
+ const compensateToValue = toValueCompensator(itemWidth);
123
149
 
124
150
  const getValueByDirectionalPagingOnLoopDisabled = (_config: DirectionalPagingAnimationConfig): number => {
125
151
  const { direction, isOriginatedFromGesture } = _config;
@@ -159,17 +185,20 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
159
185
  return distance * direction;
160
186
  };
161
187
 
162
- const toValue = type === 'directional'
188
+ const wantedToValue = type === 'directional'
163
189
  // @ts-ignore
164
190
  ? getValueByDirectionalPaging(configWithDefaults)
165
191
  // @ts-ignore
166
192
  : getValueByIndexPaging(configWithDefaults);
167
193
 
194
+ const toValue = compensateToValue(wantedToValue, currentOffsetRef.current);
195
+
168
196
  toValueRef.current = toValue;
197
+ isAnimatingRef.current = true;
169
198
 
170
199
  if (configWithDefaults.animated) {
171
- const animation = createScrollAnimation(offsetTx, toValue);
172
- animationRef.current = animation;
200
+ const animation = createScrollAnimation(translateX, toValue);
201
+
173
202
  animation.start(({ finished }) => {
174
203
  if (finished) {
175
204
  finalizeAnimation();
@@ -178,6 +207,8 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
178
207
  } else {
179
208
  finalizeAnimation();
180
209
  }
210
+
211
+ notifyOffsetHasChanged(currentOffsetRef.current + toValue);
181
212
  }, [
182
213
  createScrollAnimation,
183
214
  getCurrentIndex,
@@ -185,11 +216,11 @@ export default function usePagingAnimation(params: PagingAnimationParameters): U
185
216
  itemWidth,
186
217
  lastIndex,
187
218
  loop,
219
+ notifyOffsetHasChanged,
188
220
  ]);
189
221
 
190
222
  return {
191
- globalInterpolation,
192
- finalizeAnimation,
223
+ interruptAnimation,
193
224
  startPagingAnimation,
194
225
  };
195
226
  };