1 | import React, { Component } from 'react';
|
2 | import {
|
3 | Dimensions,
|
4 | NativeScrollEvent,
|
5 | NativeSyntheticEvent,
|
6 | Platform,
|
7 | ScrollView,
|
8 | ScrollViewProps,
|
9 | StyleProp,
|
10 | View,
|
11 | ViewStyle,
|
12 | } from 'react-native';
|
13 |
|
14 | type Props = typeof PageSlider.defaultProps & {
|
15 | children?: React.ReactNode;
|
16 | contentPaddingVertical?: number;
|
17 | selectedPage?: number;
|
18 | style?: StyleProp<ViewStyle>;
|
19 | onCurrentPageChange: (currentPage: number) => void;
|
20 | onSelectedPageChange: (selectedPage: number) => void;
|
21 | };
|
22 |
|
23 | export class PageSlider extends Component<Props> {
|
24 | static defaultProps = {
|
25 | mode: 'page' as 'page' | 'card',
|
26 | pageMargin: 8,
|
27 | peek: 24,
|
28 | };
|
29 |
|
30 | private offsetX = 0;
|
31 |
|
32 | private initialSelectedPage: number | undefined;
|
33 |
|
34 | private hasDoneInitialScroll = false;
|
35 |
|
36 | private scrollView: ScrollView | null = null;
|
37 |
|
38 | constructor(props: Props) {
|
39 | super(props);
|
40 |
|
41 |
|
42 | this.initialSelectedPage = this.props.selectedPage;
|
43 | }
|
44 |
|
45 | componentDidMount(): void {
|
46 | if (Platform.OS === 'ios' && this.props.selectedPage) {
|
47 |
|
48 | this.scrollToPage(this.props.selectedPage, false);
|
49 | }
|
50 | }
|
51 |
|
52 | componentDidUpdate(prevProps: Props): void {
|
53 | const currentPage = this.getCurrentPage();
|
54 |
|
55 | if (
|
56 | prevProps.selectedPage !== this.props.selectedPage &&
|
57 | this.props.selectedPage !== currentPage &&
|
58 | this.props.selectedPage !== undefined
|
59 | ) {
|
60 | this.scrollToPage(this.props.selectedPage);
|
61 | }
|
62 | }
|
63 |
|
64 | private onContentSizeChange = (width: number, height: number): void => {
|
65 | if (
|
66 | Platform.OS === 'android' &&
|
67 | width &&
|
68 | height &&
|
69 | this.initialSelectedPage &&
|
70 | !this.hasDoneInitialScroll
|
71 | ) {
|
72 | this.scrollToPage(this.initialSelectedPage, false);
|
73 |
|
74 | this.hasDoneInitialScroll = true;
|
75 | }
|
76 | };
|
77 |
|
78 | private onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>): void => {
|
79 | this.offsetX = e.nativeEvent.contentOffset.x;
|
80 |
|
81 | const currentPage = this.getCurrentPage();
|
82 | this.props.onCurrentPageChange(currentPage);
|
83 | };
|
84 |
|
85 | private onMomentumScrollEnd = (): void => {
|
86 | const currentPage = this.getCurrentPage();
|
87 | if (this.props.selectedPage !== currentPage) {
|
88 | this.props.onSelectedPageChange(currentPage);
|
89 | }
|
90 | };
|
91 |
|
92 |
|
93 | private getPageWidth = (): number => {
|
94 | const { width } = Dimensions.get('screen');
|
95 | if (this.props.mode === 'page') {
|
96 | return width;
|
97 | }
|
98 |
|
99 | const { peek, pageMargin } = this.props;
|
100 |
|
101 | return width - 2 * peek + 2 * pageMargin;
|
102 | };
|
103 |
|
104 |
|
105 | private getCurrentPage = (): number => {
|
106 | const pageWidth = this.getPageWidth();
|
107 |
|
108 | return Math.floor(this.offsetX / pageWidth - 0.5) + 1;
|
109 | };
|
110 |
|
111 |
|
112 | private scrollToPage = (index: number, animated = true): void => {
|
113 | const pageWidth = this.getPageWidth();
|
114 |
|
115 | this.scrollView?.scrollTo({ y: 0, x: index * pageWidth, animated });
|
116 | };
|
117 |
|
118 | render(): JSX.Element {
|
119 | const { children, mode, style } = this.props;
|
120 | const { width } = Dimensions.get('screen');
|
121 |
|
122 | const pageStyle = {
|
123 | height: '100%',
|
124 | };
|
125 | let scrollViewProps: ScrollViewProps = {};
|
126 |
|
127 |
|
128 | if (mode === 'page') {
|
129 | Object.assign(pageStyle, {
|
130 | width,
|
131 | });
|
132 | } else if (mode === 'card') {
|
133 | const { contentPaddingVertical, peek, pageMargin } = this.props;
|
134 |
|
135 | scrollViewProps = {
|
136 | contentContainerStyle: {
|
137 | paddingHorizontal: peek - pageMargin,
|
138 | paddingVertical: contentPaddingVertical,
|
139 | },
|
140 | decelerationRate: 'fast',
|
141 | snapToAlignment: 'start',
|
142 | snapToInterval: this.getPageWidth(),
|
143 | };
|
144 |
|
145 | if (Platform.OS === 'ios') {
|
146 | |
147 |
|
148 |
|
149 |
|
150 | scrollViewProps.pagingEnabled = false;
|
151 | }
|
152 |
|
153 | Object.assign(pageStyle, {
|
154 | marginHorizontal: pageMargin,
|
155 | width: width - 2 * peek,
|
156 | });
|
157 | }
|
158 |
|
159 |
|
160 | const pages = React.Children.map(children, (page) => {
|
161 |
|
162 | if (!React.isValidElement(page)) {
|
163 | return null;
|
164 | }
|
165 |
|
166 | return (
|
167 | <View key={page.key} style={pageStyle}>
|
168 | {page}
|
169 | </View>
|
170 | );
|
171 | });
|
172 |
|
173 | return (
|
174 | <ScrollView
|
175 | style={style}
|
176 | ref={(scrollView) => {
|
177 | this.scrollView = scrollView;
|
178 | }}
|
179 | horizontal
|
180 | pagingEnabled
|
181 | showsHorizontalScrollIndicator={false}
|
182 | onContentSizeChange={this.onContentSizeChange}
|
183 | onScroll={this.onScroll}
|
184 | onMomentumScrollEnd={this.onMomentumScrollEnd}
|
185 | scrollEventThrottle={8}
|
186 | {...scrollViewProps}
|
187 | >
|
188 | {pages}
|
189 | </ScrollView>
|
190 | );
|
191 | }
|
192 | }
|