Skip to content

Commit

Permalink
feat(PageView): add new component PageView
Browse files Browse the repository at this point in the history
  • Loading branch information
mahaaoo committed Sep 8, 2024
1 parent 172986c commit 2af4183
Show file tree
Hide file tree
Showing 8 changed files with 2,419 additions and 670 deletions.
2,746 changes: 2,082 additions & 664 deletions example/package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
"@react-navigation/native": "^6.0.10",
"@react-navigation/native-stack": "^6.6.2",
"crypto": "^1.0.1",
"expo": "~51.0.4",
"expo-splash-screen": "~0.27.4",
"expo": "~51.0.31",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"expo-updates": "~0.25.11",
"jest-expo": "~51.0.1",
"expo-updates": "~0.25.24",
"jest-expo": "~51.0.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.1",
"react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1",
"react-native-reanimated": "~3.10.1",
"react-native-redash": "^16.2.4",
"react-native-safe-area-context": "4.10.1",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-svg-transformer": "^1.3.0",
Expand Down
86 changes: 86 additions & 0 deletions example/src/pages/PageViewExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useRef } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { PageView } from 'react-native-maui';

interface PageViewExampleProps {}

const PageViewExample: React.FC<PageViewExampleProps> = (props) => {
const {} = props;
const ref = useRef(null);

return (
<PageView
ref={ref}
style={styles.pagerView}
initialPage={1}
onPageSelected={(currentPage) => {
console.log('currentPage:', currentPage);
}}
onPageScrollStateChanged={(state) => {
console.log('state:', state);
}}
onPageScroll={(translate) => {
console.log('translate', translate);
}}
scrollEnabled={true}
bounces={false}
>
<View key="1" style={{ flex: 1, backgroundColor: 'orange' }}>
<Text>first page</Text>
<Text
onPress={() => {
ref.current && ref.current?.setPage(2);
}}
>
go page 3
</Text>
<Text
onPress={() => {
ref.current && ref.current?.setPageWithoutAnimation(2);
}}
>
go page 3 widthout animate
</Text>
<Text
onPress={() => {
const page = ref.current && ref.current?.getCurrentPage();
console.log(page);
}}
>
getCurrentPage
</Text>
</View>
<View key="2" style={{ flex: 1, backgroundColor: 'pink' }}>
<Text>Second page</Text>
<Text
onPress={() => {
const page = ref.current && ref.current?.getCurrentPage();
console.log(page);
}}
>
getCurrentPage
</Text>
</View>
<View key="3" style={{ flex: 1, backgroundColor: 'blue' }}>
<Text>Second page</Text>
<Text
onPress={() => {
const page = ref.current && ref.current?.getCurrentPage();
console.log(page);
}}
>
getCurrentPage
</Text>
</View>
</PageView>
);
};

const styles = StyleSheet.create({
pagerView: {
flex: 1,
width: 200,
},
});

export default PageViewExample;
2 changes: 2 additions & 0 deletions example/src/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import SwipeActionExample from './SwipeActionExample';
import RefreshControlExample from './RefreshControlExample';
import TabViewExample from './TabViewExample';
import HeadTabViewExample from './HeadTabViewExample';
import PageViewExample from './PageViewExample';

export default {
ButtonExample,
Expand Down Expand Up @@ -58,4 +59,5 @@ export default {
RefreshControlExample,
TabViewExample,
HeadTabViewExample,
PageViewExample,
};
4 changes: 4 additions & 0 deletions example/src/thumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,10 @@ const exampleList: Example[] = [
title: 'HeadTabView',
content: null,
},
{
title: 'PageView',
content: null,
},
];

export { exampleList };
235 changes: 235 additions & 0 deletions src/components/PageView/PageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import { Dimensions, View, ViewStyle } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { snapPoint, clamp } from '../../index';

interface PageViewProps {
children: React.ReactElement;

style?: ViewStyle;
initialPage?: number;
scrollEnabled?: boolean;
bounces?: boolean;

onPageScroll?: (translate: number) => void;
onPageSelected?: (currentPage: number) => void;
onPageScrollStateChanged?: (state: PageStateType) => void;
}

interface PageViewRef {
setPage: (index: number) => void;
setPageWithoutAnimation: (index: number) => void;
getCurrentPage: () => number;
}

const { width, height } = Dimensions.get('window');
const DURATION = 350;

type PageStateType = 'dragging' | 'settling' | 'idle';

interface PageViewVerifyProps extends PageViewProps {
pageSize: number;
contentSize: number;
snapPoints: number[];
}

const verifyProps = (props: PageViewProps): PageViewVerifyProps => {
const { children, style } = props;
const pageSize = React.Children.count(children);
if (pageSize === 0) {
throw new Error('PageView must be contains at least one chid');
}
let contentSize: number = width;
if (style && style.width) {
if (typeof style.width === 'number') {
contentSize = style.width;
} else {
throw new Error('PageView width only support number');
}
}

const snapPoints = new Array(pageSize)
.fill(0)
.map((_, index) => -index * contentSize);

return {
...props,
pageSize,
contentSize,
snapPoints,
};
};

const PageView = forwardRef<PageViewRef, PageViewProps>((props, ref) => {
const {
children,
style,
contentSize,
pageSize,
snapPoints,
onPageSelected,
initialPage = 0,
onPageScrollStateChanged,
onPageScroll,
scrollEnabled = true,
bounces = true,
} = verifyProps(props);

const pageMove = useSharedValue(-initialPage * contentSize);
const offset = useSharedValue(0);
const currentPage = useSharedValue(initialPage);
const pageState = useSharedValue<PageStateType>('idle');

const pageSelected = (nextPage: number) => {
onPageSelected && onPageSelected(nextPage);
};

const pageScrollStateChanged = (state: PageStateType) => {
onPageScrollStateChanged && onPageScrollStateChanged(state);
};

const pageScroll = (translate: number) => {
onPageScroll && onPageScroll(Math.abs(translate));
};

const setPage = (index: number) => {
moveTo(index);
};

const setPageWithoutAnimation = (index: number) => {
pageMove.value = -index * contentSize;
currentPage.value = index;
pageState.value = 'idle';
runOnJS(pageSelected)(index);
};

const getCurrentPage = () => {
return currentPage.value;
};

useImperativeHandle(
ref,
() => ({
setPage,
setPageWithoutAnimation,
getCurrentPage,
}),
[]
);

useAnimatedReaction(
() => pageState.value,
(value) => {
runOnJS(pageScrollStateChanged)(value);
}
);

useAnimatedReaction(
() => pageMove.value,
(value) => {
runOnJS(pageScroll)(value);
}
);

const moveTo = (page: number) => {
'worklet';
pageMove.value = withTiming(
-page * contentSize,
{ duration: DURATION },
() => {
const nextPage = Math.abs(Math.round(pageMove.value / contentSize));
currentPage.value = nextPage;
pageState.value = 'idle';
runOnJS(pageSelected)(nextPage);
}
);
};

const panGesture = Gesture.Pan()
.activeOffsetX([-10, 10])
.onTouchesDown((event, stateManager) => {
if (scrollEnabled === false) {
stateManager.fail();
}
})
.onBegin(() => {
offset.value = pageMove.value;
})
.onUpdate(({ translationX }) => {
pageState.value = 'dragging';
if (bounces) {
pageMove.value = translationX + offset.value;
} else {
pageMove.value = clamp(
translationX + offset.value,
-(pageSize - 1) * contentSize,
0
);
}
})
.onEnd(({ velocityX }) => {
pageState.value = 'settling';
const dest = snapPoint(pageMove.value, velocityX, snapPoints);
// 每次移动只能切换一个page
const willToPage = Math.abs(Math.round(dest / contentSize));
const toValue = clamp(
willToPage,
currentPage.value - 1,
currentPage.value + 1
);

moveTo(toValue);
});

const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: pageMove.value,
},
],
};
});

return (
<View style={{ flex: 1, width: contentSize, overflow: 'hidden' }}>
<GestureDetector gesture={panGesture}>
<Animated.View
style={[
style,
{
flexDirection: 'row',
width: contentSize * pageSize,
},
animatedStyle,
]}
>
{React.Children.map(children, (child) => {
return (
<PageContainer contentSize={contentSize}>{child}</PageContainer>
);
})}
</Animated.View>
</GestureDetector>
</View>
);
});

interface PageContainerProps {
children: React.ReactElement;
contentSize: number;
}

const PageContainer: React.FC<PageContainerProps> = (props) => {
const { children, contentSize } = props;
return <View style={{ width: contentSize }}>{children}</View>;
};

export default PageView;
3 changes: 3 additions & 0 deletions src/components/PageView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PageView from './PageView';

export { PageView };
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ export { Freeze } from './utils/Freeze';

export { TabView, DefaultTabBar } from './components/TabView';
export { HeadTabView } from './components/HeadTabView';
export { PageView } from './components/PageView';

0 comments on commit 2af4183

Please sign in to comment.