Skip to content

Commit c956690

Browse files
authored
Merge pull request #652 from bdtren/main
add Pagination.Custom dot animation
2 parents 8c3d526 + 2916a26 commit c956690

File tree

4 files changed

+347
-1
lines changed

4 files changed

+347
-1
lines changed

example/app/src/pages/parallax/index.tsx

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from "react";
22
import { View } from "react-native";
3-
import { useSharedValue } from "react-native-reanimated";
3+
import { useSharedValue, interpolate, Extrapolation } from "react-native-reanimated";
44
import Carousel, {
55
ICarouselInstance,
66
Pagination,
@@ -156,6 +156,7 @@ function Index() {
156156
activeDotStyle={{
157157
borderRadius: 100,
158158
overflow: "hidden",
159+
backgroundColor: "rgba(0,0,0,0.2)",
159160
}}
160161
containerStyle={[
161162
isVertical
@@ -166,6 +167,10 @@ function Index() {
166167
top: 40,
167168
}
168169
: undefined,
170+
{
171+
gap: 5,
172+
marginBottom: 10,
173+
},
169174
]}
170175
horizontal={!isVertical}
171176
renderItem={(item) => (
@@ -179,6 +184,118 @@ function Index() {
179184
onPress={onPressPagination}
180185
/>
181186

187+
<Pagination.Custom<{ color: string }>
188+
progress={progress}
189+
data={colors.map((color) => ({ color }))}
190+
size={20}
191+
dotStyle={{
192+
borderRadius: 16,
193+
backgroundColor: "rgba(0,0,0,0.2)",
194+
}}
195+
activeDotStyle={{
196+
borderRadius: 8,
197+
width: 40,
198+
height: 30,
199+
overflow: "hidden",
200+
backgroundColor: 'black',
201+
}}
202+
containerStyle={[
203+
isVertical
204+
? {
205+
position: "absolute",
206+
width: 20,
207+
right: 5,
208+
top: 40,
209+
}
210+
: undefined,
211+
{
212+
gap: 5,
213+
marginBottom: 10,
214+
alignItems: "center",
215+
},
216+
]}
217+
horizontal={!isVertical}
218+
onPress={onPressPagination}
219+
customReanimatedStyle={(progress, index, length) => {
220+
let val = Math.abs(progress - index);
221+
if (index === 0 && progress > length - 1) {
222+
val = Math.abs(progress - length);
223+
}
224+
225+
return {
226+
transform: [
227+
{
228+
translateY: interpolate(
229+
val,
230+
[0, 1],
231+
[10, 0],
232+
Extrapolation.CLAMP,
233+
),
234+
}
235+
]
236+
}
237+
}}
238+
/>
239+
240+
<Pagination.Custom<{ color: string }>
241+
progress={progress}
242+
data={colors.map((color) => ({ color }))}
243+
size={20}
244+
dotStyle={{
245+
borderRadius: 16,
246+
backgroundColor: "rgba(0,0,0,0.2)",
247+
}}
248+
activeDotStyle={{
249+
borderRadius: 8,
250+
width: 40,
251+
height: 30,
252+
overflow: "hidden",
253+
}}
254+
containerStyle={[
255+
isVertical
256+
? {
257+
position: "absolute",
258+
width: 20,
259+
right: 5,
260+
top: 40,
261+
}
262+
: undefined,
263+
{
264+
gap: 5,
265+
alignItems: "center",
266+
},
267+
]}
268+
horizontal={!isVertical}
269+
onPress={onPressPagination}
270+
customReanimatedStyle={(progress, index, length) => {
271+
let val = Math.abs(progress - index);
272+
if (index === 0 && progress > length - 1) {
273+
val = Math.abs(progress - length);
274+
}
275+
276+
return {
277+
transform: [
278+
{
279+
translateY: interpolate(
280+
val,
281+
[0, 1],
282+
[10, 0],
283+
Extrapolation.CLAMP,
284+
),
285+
}
286+
]
287+
}
288+
}}
289+
renderItem={(item) => (
290+
<View
291+
style={{
292+
backgroundColor: item.color,
293+
flex: 1,
294+
}}
295+
/>
296+
)}
297+
/>
298+
182299
<SButton
183300
onPress={() => setAutoPlay(!autoPlay)}
184301
>{`${ElementsText.AUTOPLAY}:${autoPlay}`}</SButton>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { PropsWithChildren } from "react";
2+
import React from "react";
3+
import type { ViewStyle } from "react-native";
4+
import Animated, {
5+
Extrapolation,
6+
interpolate,
7+
interpolateColor,
8+
SharedValue,
9+
useAnimatedStyle,
10+
runOnJS,
11+
useSharedValue,
12+
useDerivedValue,
13+
} from "react-native-reanimated";
14+
import type { DefaultStyle } from "react-native-reanimated/lib/typescript/hook/commonTypes";
15+
16+
export type DotStyle = Omit<ViewStyle, "width" | "height"> & {
17+
width?: number
18+
height?: number
19+
};
20+
21+
export const PaginationItem: React.FC<
22+
PropsWithChildren<{
23+
index: number
24+
count: number
25+
size?: number
26+
animValue: SharedValue<number>
27+
horizontal?: boolean
28+
dotStyle?: DotStyle
29+
activeDotStyle?: DotStyle
30+
customReanimatedStyle?: (
31+
progress: number,
32+
index: number,
33+
length: number,
34+
) => DefaultStyle
35+
}>
36+
> = (props) => {
37+
const defaultDotSize = 10;
38+
const {
39+
animValue,
40+
dotStyle,
41+
activeDotStyle,
42+
index,
43+
count,
44+
size,
45+
horizontal,
46+
children,
47+
customReanimatedStyle,
48+
} = props;
49+
const customReanimatedStyleRef = useSharedValue<DefaultStyle>({});
50+
const handleCustomAnimation = (progress: number) => {
51+
customReanimatedStyleRef.value = customReanimatedStyle?.(progress, index, count) ?? {};
52+
}
53+
54+
useDerivedValue(() => {
55+
runOnJS(handleCustomAnimation)(animValue?.value);
56+
});
57+
58+
const animStyle = useAnimatedStyle(() => {
59+
const {
60+
width = size || defaultDotSize,
61+
height = size || defaultDotSize,
62+
borderRadius,
63+
backgroundColor = "#FFF",
64+
...restDotStyle
65+
} = dotStyle ?? {};
66+
const {
67+
width: activeWidth = width,
68+
height: activeHeight = height,
69+
borderRadius: activeBorderRadius,
70+
backgroundColor: activeBackgroundColor = "#000",
71+
...restActiveDotStyle
72+
} = activeDotStyle ?? {};
73+
let val = Math.abs(animValue?.value - index);
74+
if (index === 0 && animValue?.value > count - 1) {
75+
val = Math.abs(animValue?.value - count);
76+
}
77+
const inputRange = [0, 1, 2];
78+
const restStyle = (val === 0 ? restActiveDotStyle : restDotStyle) ?? {};
79+
80+
return {
81+
width: interpolate(
82+
val,
83+
inputRange,
84+
[activeWidth, width, width],
85+
Extrapolation.CLAMP,
86+
),
87+
height: interpolate(
88+
val,
89+
inputRange,
90+
[activeHeight, height, height],
91+
Extrapolation.CLAMP,
92+
),
93+
borderRadius: interpolate(
94+
val,
95+
inputRange,
96+
[activeBorderRadius, borderRadius, borderRadius],
97+
Extrapolation.CLAMP,
98+
),
99+
backgroundColor: interpolateColor(
100+
val,
101+
inputRange,
102+
[activeBackgroundColor, backgroundColor, backgroundColor],
103+
),
104+
...restStyle,
105+
...(customReanimatedStyleRef.value ?? {}),
106+
transform: [
107+
...restStyle?.transform ?? [],
108+
...customReanimatedStyleRef.value?.transform ?? [],
109+
]
110+
};
111+
}, [animValue, index, count, horizontal, dotStyle, activeDotStyle, customReanimatedStyle]);
112+
113+
return (
114+
<Animated.View
115+
style={[
116+
{
117+
overflow: "hidden",
118+
transform: [
119+
{
120+
rotateZ: horizontal ? "90deg" : "0deg",
121+
},
122+
],
123+
},
124+
dotStyle,
125+
animStyle,
126+
]}
127+
>
128+
{children}
129+
</Animated.View>
130+
);
131+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from "react";
2+
import type { StyleProp, ViewStyle } from "react-native";
3+
import { View } from "react-native";
4+
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
5+
import type { SharedValue } from "react-native-reanimated";
6+
import type { DefaultStyle } from "react-native-reanimated/lib/typescript/hook/commonTypes";
7+
8+
import type { DotStyle } from "./PaginationItem";
9+
import { PaginationItem } from "./PaginationItem";
10+
11+
export interface ShapeProps<T extends {} = {}> {
12+
progress: SharedValue<number>
13+
horizontal?: boolean
14+
data: Array<T>
15+
renderItem?: (item: T, index: number) => React.ReactNode
16+
containerStyle?: StyleProp<ViewStyle>
17+
dotStyle?: DotStyle
18+
activeDotStyle?: DotStyle
19+
size?: number
20+
onPress?: (index: number) => void
21+
customReanimatedStyle?: (
22+
progress: number,
23+
index: number,
24+
length: number,
25+
) => DefaultStyle
26+
}
27+
28+
export const Custom = <T extends {}>(props: ShapeProps<T>) => {
29+
const {
30+
activeDotStyle,
31+
dotStyle,
32+
progress,
33+
horizontal = true,
34+
data,
35+
size,
36+
containerStyle,
37+
renderItem,
38+
onPress,
39+
customReanimatedStyle,
40+
} = props;
41+
42+
if (
43+
typeof size === "string" ||
44+
typeof dotStyle?.width === "string" ||
45+
typeof dotStyle?.height === "string" ||
46+
typeof activeDotStyle?.width === "string" ||
47+
typeof activeDotStyle?.height === "string"
48+
)
49+
throw new Error("size/width/height must be a number");
50+
51+
const maxItemWidth = Math.max(size ?? 0, dotStyle?.width ?? 0, activeDotStyle?.width ?? 0);
52+
const maxItemHeight = Math.max(size ?? 0, dotStyle?.height ?? 0, activeDotStyle?.height ?? 0);
53+
54+
return (
55+
<View
56+
style={[
57+
{
58+
justifyContent: "space-between",
59+
alignSelf: "center",
60+
minWidth: maxItemWidth,
61+
minHeight: maxItemHeight,
62+
},
63+
horizontal
64+
? {
65+
flexDirection: "row",
66+
}
67+
: {
68+
flexDirection: "column",
69+
},
70+
containerStyle,
71+
]}
72+
>
73+
{data.map((item, index) => {
74+
return (
75+
<TouchableWithoutFeedback
76+
key={index}
77+
onPress={() => onPress?.(index)}
78+
>
79+
<PaginationItem
80+
index={index}
81+
size={size}
82+
count={data.length}
83+
dotStyle={dotStyle}
84+
animValue={progress}
85+
horizontal={!horizontal}
86+
activeDotStyle={activeDotStyle}
87+
customReanimatedStyle={customReanimatedStyle}
88+
>
89+
{renderItem?.(item, index)}
90+
</PaginationItem>
91+
</TouchableWithoutFeedback>
92+
);
93+
})}
94+
</View>
95+
);
96+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Basic } from "./Basic";
2+
import { Custom } from "./Custom";
23

34
export const Pagination = {
45
Basic,
6+
Custom,
57
};

0 commit comments

Comments
 (0)